Naman712 commited on
Commit
fd44c97
·
verified ·
1 Parent(s): b10bb3b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +281 -173
app.py CHANGED
@@ -93,28 +93,29 @@ html_content = """
93
  <meta charset="UTF-8">
94
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
95
  <title>OCR Studio</title>
96
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
97
  <script src="https://unpkg.com/@phosphor-icons/web"></script>
98
 
99
  <style>
100
  :root {
101
- --bg-dark: #0f172a;
102
- --bg-card: rgba(30, 41, 59, 0.75);
103
- --primary: #818cf8;
104
- --primary-glow: rgba(129, 140, 248, 0.5);
105
- --text-main: #f8fafc;
106
- --text-muted: #94a3b8;
107
- --border: rgba(255, 255, 255, 0.1);
108
- --glass: blur(16px);
 
 
109
  }
110
 
111
  * { box-sizing: border-box; outline: none; }
112
 
113
  body {
114
- font-family: 'Inter', sans-serif;
115
- background-color: var(--bg-dark);
116
- background-image: radial-gradient(at 0% 0%, rgba(99, 102, 241, 0.15) 0px, transparent 50%), radial-gradient(at 100% 100%, rgba(168, 85, 247, 0.15) 0px, transparent 50%);
117
- color: var(--text-main);
118
  margin: 0;
119
  height: 100vh;
120
  display: flex;
@@ -123,235 +124,290 @@ html_content = """
123
  }
124
 
125
  /* --- ANIMATIONS --- */
126
- @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
127
- @keyframes scanLaser {
 
128
  0% { top: 0%; opacity: 0; }
129
- 10% { opacity: 1; box-shadow: 0 0 15px var(--primary); }
130
- 90% { opacity: 1; box-shadow: 0 0 15px var(--primary); }
131
  100% { top: 100%; opacity: 0; }
132
  }
 
133
 
134
- /* --- LAYOUT --- */
135
  header {
136
- padding: 15px 30px;
 
137
  border-bottom: 1px solid var(--border);
138
- background: rgba(15, 23, 42, 0.85);
139
- backdrop-filter: var(--glass);
140
  display: flex;
141
  justify-content: space-between;
142
  align-items: center;
 
143
  z-index: 50;
144
- box-shadow: 0 4px 20px rgba(0,0,0,0.2);
145
  }
146
 
147
- .logo {
148
- font-weight: 800; font-size: 1.2rem;
149
- background: linear-gradient(90deg, #818cf8, #c084fc); -webkit-background-clip: text; -webkit-text-fill-color: transparent;
150
- display: flex; align-items: center; gap: 10px;
 
 
 
 
 
 
 
 
 
 
151
  }
152
 
153
  main { flex: 1; display: flex; overflow: hidden; }
154
 
 
155
  .sidebar {
156
- width: 280px;
157
- background: var(--bg-card);
158
  border-right: 1px solid var(--border);
159
- padding: 20px;
160
- display: flex; flex-direction: column; gap: 20px;
161
- backdrop-filter: var(--glass);
162
- z-index: 10;
163
  }
164
 
165
- .upload-area {
166
- border: 2px dashed var(--border);
167
- border-radius: 12px;
168
- padding: 30px 20px;
 
169
  text-align: center;
170
  cursor: pointer;
171
- transition: all 0.3s ease;
172
- background: rgba(255,255,255,0.02);
173
- position: relative; overflow: hidden;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  }
175
- .upload-area:hover { border-color: var(--primary); background: rgba(129, 140, 248, 0.1); transform: scale(1.02); }
176
- .upload-area i { font-size: 32px; color: var(--primary); margin-bottom: 10px; }
 
 
177
 
178
- /* WORKSPACE */
179
  .workspace {
180
  flex: 1;
181
  display: flex;
 
182
  position: relative;
183
- background: #020617;
184
  }
185
 
186
- .preview-pane {
 
187
  flex: 1;
188
  position: relative;
189
- overflow: auto; /* SCROLL ENABLED */
190
  display: grid;
191
  place-items: center;
192
- padding: 40px;
193
  }
194
-
195
- /* Scrollbar */
196
- ::-webkit-scrollbar { width: 8px; height: 8px; }
197
- ::-webkit-scrollbar-track { background: #0f172a; }
198
- ::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
199
- ::-webkit-scrollbar-thumb:hover { background: #475569; }
200
-
201
  .canvas-wrapper {
202
  position: relative;
203
- box-shadow: 0 20px 50px rgba(0,0,0,0.5);
204
  transition: width 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94), transform 0.3s ease;
205
- width: 80%; /* Default zoom */
206
  transform-origin: center center;
207
  }
208
 
209
- /* FIX: Canvas MUST be 100% width/height to scale with the image */
210
  canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
211
  img { display: block; width: 100%; height: auto; }
212
 
213
- /* SCAN EFFECT */
214
  .scan-line {
215
- position: absolute; width: 100%; height: 2px;
216
- background: var(--primary); z-index: 5;
217
- display: none; pointer-events: none;
218
- }
219
- .scan-line.active { display: block; animation: scanLaser 2s linear infinite; }
220
-
221
- /* PAGINATION */
222
- .pagination-bar {
223
- position: absolute; top: 20px; left: 50%; transform: translateX(-50%);
224
- background: rgba(15, 23, 42, 0.9); border: 1px solid var(--border);
225
- padding: 8px 16px; border-radius: 30px;
226
- display: flex; align-items: center; gap: 15px;
227
- z-index: 20; box-shadow: 0 4px 12px rgba(0,0,0,0.3);
228
- display: none;
229
- }
230
- .page-btn {
231
- background: transparent; border: none; color: white; cursor: pointer;
232
- display: flex; align-items: center; justify-content: center;
233
- padding: 5px; border-radius: 50%; transition: 0.2s;
234
- }
235
- .page-btn:hover { background: rgba(255,255,255,0.1); }
236
-
237
- /* RESULTS PANE */
238
- .results-pane {
239
- width: 400px;
240
- background: var(--bg-card);
241
  border-left: 1px solid var(--border);
242
  display: flex; flex-direction: column;
243
- backdrop-filter: var(--glass);
244
  z-index: 20;
245
  }
246
 
247
- .results-content { flex: 1; overflow-y: auto; padding: 15px; }
248
-
249
- .text-block {
250
- padding: 10px; margin-bottom: 6px;
251
- border-radius: 6px;
252
- border: 1px solid rgba(255,255,255,0.05);
253
- background: rgba(255,255,255,0.02);
254
- cursor: pointer; transition: 0.2s;
255
- animation: fadeIn 0.3s ease-out backwards;
256
  }
257
- .text-block:hover {
258
- background: rgba(129, 140, 248, 0.15);
259
- border-color: var(--primary);
260
- transform: translateX(5px);
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  }
 
 
 
262
 
263
  /* LOADER */
264
  .loader-overlay {
265
  position: fixed; top: 0; left: 0; width: 100%; height: 100%;
266
- background: rgba(15, 23, 42, 0.85); z-index: 100;
267
  display: flex; justify-content: center; align-items: center; flex-direction: column;
268
- backdrop-filter: blur(8px); opacity: 0; pointer-events: none; transition: 0.3s;
269
  }
270
  .loader-overlay.active { opacity: 1; pointer-events: all; }
271
- .spinner {
272
- width: 50px; height: 50px; border: 3px solid rgba(255,255,255,0.1);
273
- border-top-color: var(--primary); border-radius: 50%;
274
- animation: spin 1s linear infinite; margin-bottom: 15px;
275
- box-shadow: 0 0 20px var(--primary-glow);
276
  }
277
- @keyframes spin { 100% { transform: rotate(360deg); } }
278
-
279
- .raw-text-area {
280
- width: 100%; height: 100%; background: transparent; border: none;
281
- color: #cbd5e1; resize: none; font-family: 'JetBrains Mono', monospace; line-height: 1.5; white-space: pre-wrap;
282
- }
283
-
284
- .zoom-controls {
285
- position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
286
- background: rgba(0,0,0,0.8); padding: 8px; border-radius: 20px;
287
- display: flex; gap: 10px; border: 1px solid var(--border); z-index: 20;
288
  }
 
 
 
289
  </style>
290
  </head>
291
  <body>
292
 
293
  <header>
294
- <div class="logo"><i class="ph-bold ph-aperture"></i> OCR STUDIO</div>
295
- <div style="font-size: 0.8rem; color: var(--text-muted); display:flex; align-items:center; gap:6px;">
296
- <div style="width:8px; height:8px; background:#10b981; border-radius:50%; box-shadow:0 0 8px #10b981;"></div>
297
- Online
 
298
  </div>
299
  </header>
300
 
301
  <main>
 
302
  <aside class="sidebar">
303
- <div class="upload-area" id="drop-zone">
304
- <i class="ph-duotone ph-cloud-arrow-up"></i>
305
- <p><strong>Click to Upload</strong><br>or Drag & Drop</p>
306
  <input type="file" id="file-input" hidden accept="image/*,application/pdf">
307
  </div>
308
 
309
- <div style="padding: 15px; border-bottom: 1px solid var(--border);">
310
- <label style="font-size: 0.8rem; color: #94a3b8; display: block; margin-bottom: 5px;">Auto-Rotate</label>
311
- <input type="checkbox" id="angle-cls"> <span style="font-size: 0.8rem; color: #fff;">Enable</span>
 
 
 
 
 
 
 
312
  </div>
313
-
314
- <div style="padding: 15px;">
315
- <label style="font-size: 0.8rem; color: #94a3b8; display: block; margin-bottom: 5px;">Confidence Filter</label>
316
- <input type="range" id="conf-slider" min="0" max="100" value="0" style="width:100%">
317
  </div>
318
-
319
- <div style="margin-top: auto; display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
320
- <button onclick="downloadJSON()" style="padding: 10px; background: #334155; border: none; color: white; border-radius: 6px; cursor: pointer;">JSON</button>
321
- <button onclick="location.reload()" style="padding: 10px; background: #334155; border: none; color: white; border-radius: 6px; cursor: pointer;">Reset</button>
 
 
 
 
 
322
  </div>
323
  </aside>
324
 
 
325
  <div class="workspace">
326
- <div class="preview-pane">
327
- <!-- Pagination -->
328
- <div class="pagination-bar" id="pagination-bar">
329
- <button class="page-btn" onclick="changePage(-1)"><i class="ph-bold ph-caret-left"></i></button>
330
- <span id="page-indicator" style="font-variant-numeric: tabular-nums;">Page 1 / 1</span>
331
- <button class="page-btn" onclick="changePage(1)"><i class="ph-bold ph-caret-right"></i></button>
332
- </div>
333
-
334
  <div class="canvas-wrapper" id="canvas-wrapper">
335
  <img id="source-image" src="">
336
  <canvas id="overlay"></canvas>
337
  <div class="scan-line" id="scan-line"></div>
338
  </div>
339
 
340
- <div class="zoom-controls">
341
- <button class="page-btn" onclick="rotateView()" title="Rotate View"><i class="ph-bold ph-arrow-clockwise"></i></button>
342
- <div style="width:1px; background:rgba(255,255,255,0.2); margin:0 5px;"></div>
343
- <button class="page-btn" onclick="zoom(-10)"><i class="ph-bold ph-minus"></i></button>
344
- <button class="page-btn" onclick="resetZoom()">100%</button>
345
- <button class="page-btn" onclick="zoom(10)"><i class="ph-bold ph-plus"></i></button>
 
 
 
 
346
  </div>
347
  </div>
348
 
349
- <div class="results-pane">
350
- <div style="padding: 15px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items:center;">
351
- <strong>Extracted Text</strong>
352
- <div>
353
- <button onclick="setView('blocks')" id="btn-blocks" style="background:transparent; border:none; color:var(--primary); cursor:pointer; font-weight:600;">Blocks</button>
354
- <button onclick="setView('raw')" id="btn-raw" style="background:transparent; border:none; color:#64748b; cursor:pointer;">Raw</button>
355
  </div>
356
  </div>
357
  <div class="results-content" id="results-content"></div>
@@ -359,9 +415,12 @@ html_content = """
359
  </div>
360
  </main>
361
 
 
362
  <div class="loader-overlay" id="loader">
363
- <div class="spinner"></div>
364
- <h3 id="loader-msg" style="color: white;">Processing...</h3>
 
 
365
  </div>
366
 
367
  <script>
@@ -385,11 +444,16 @@ html_content = """
385
  const pageIndicator = document.getElementById('page-indicator');
386
  const ctx = canvas.getContext('2d');
387
  const scanLine = document.getElementById('scan-line');
 
 
388
 
389
  // --- EVENTS ---
390
  dropZone.addEventListener('click', () => fileInput.click());
391
  fileInput.addEventListener('change', (e) => startUpload(e.target.files[0]));
392
- document.getElementById('conf-slider').addEventListener('input', () => renderCurrentPage());
 
 
 
393
 
394
  // --- UPLOAD & STREAMING ---
395
  async function startUpload(file) {
@@ -404,7 +468,7 @@ html_content = """
404
  updateTransform();
405
 
406
  loader.classList.add('active');
407
- loaderMsg.textContent = "Initializing Stream...";
408
 
409
  const formData = new FormData();
410
  formData.append('file', file);
@@ -441,7 +505,7 @@ html_content = """
441
 
442
  function handleStreamData(data) {
443
  if (data.type === 'status') {
444
- loaderMsg.textContent = data.message;
445
  } else if (data.type === 'page') {
446
  allPages.push(data);
447
  if (allPages.length === 1) {
@@ -449,7 +513,7 @@ html_content = """
449
  paginationBar.style.display = 'flex';
450
  renderPage(0);
451
  scanLine.classList.add('active');
452
- setTimeout(() => scanLine.classList.remove('active'), 2000);
453
  }
454
  updatePaginationUI();
455
  }
@@ -474,7 +538,7 @@ html_content = """
474
  function renderCurrentPage(highlightId = -1) {
475
  if (allPages.length === 0) return;
476
  const pageData = allPages[currentPageIdx];
477
- const minConf = document.getElementById('conf-slider').value / 100;
478
 
479
  ctx.clearRect(0, 0, canvas.width, canvas.height);
480
 
@@ -489,13 +553,13 @@ html_content = """
489
  ctx.closePath();
490
 
491
  if (block.id === highlightId) {
492
- ctx.strokeStyle = '#818cf8';
493
  ctx.lineWidth = 4;
494
- ctx.fillStyle = 'rgba(129, 140, 248, 0.4)';
495
  } else {
496
- ctx.strokeStyle = 'rgba(129, 140, 248, 0.4)';
497
- ctx.lineWidth = 2;
498
- ctx.fillStyle = 'rgba(129, 140, 248, 0.05)';
499
  }
500
  ctx.fill();
501
  ctx.stroke();
@@ -510,18 +574,18 @@ html_content = """
510
  function renderList() {
511
  resultsContent.innerHTML = '';
512
  if (currentView === 'raw') {
513
- resultsContent.innerHTML = `<textarea class="raw-text-area" readonly>${allPages[currentPageIdx].full_text}</textarea>`;
514
  return;
515
  }
516
 
517
- const minConf = document.getElementById('conf-slider').value / 100;
518
  allPages[currentPageIdx].blocks.forEach((block, index) => {
519
  if (block.conf < minConf) return;
520
 
521
  const div = document.createElement('div');
522
- div.className = 'text-block';
523
- div.style.animationDelay = `${index * 0.03}s`;
524
- div.innerHTML = `<div style="display:flex; justify-content:space-between;"><span>${block.text}</span><small style="color:#64748b">${(block.conf*100).toFixed(0)}%</small></div>`;
525
 
526
  div.addEventListener('mouseenter', () => drawCanvas(block.id));
527
  div.addEventListener('mouseleave', () => drawCanvas(-1));
@@ -540,14 +604,14 @@ html_content = """
540
  }
541
 
542
  function updatePaginationUI() {
543
- pageIndicator.textContent = `Page ${currentPageIdx + 1} / ${allPages.length}`;
544
  renderList();
545
  }
546
 
547
  function setView(view) {
548
  currentView = view;
549
- document.getElementById('btn-blocks').style.color = view === 'blocks' ? '#818cf8' : '#64748b';
550
- document.getElementById('btn-raw').style.color = view === 'raw' ? '#818cf8' : '#64748b';
551
  renderList();
552
  if(view === 'raw') ctx.clearRect(0, 0, canvas.width, canvas.height);
553
  }
@@ -580,6 +644,50 @@ html_content = """
580
  node.remove();
581
  }
582
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
583
  </script>
584
  </body>
585
  </html>
 
93
  <meta charset="UTF-8">
94
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
95
  <title>OCR Studio</title>
96
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
97
  <script src="https://unpkg.com/@phosphor-icons/web"></script>
98
 
99
  <style>
100
  :root {
101
+ /* Minimalist Palette - No Gradients */
102
+ --bg-main: #09090b; /* Deepest black/gray */
103
+ --bg-panel: #18181b; /* Slightly lighter panel */
104
+ --border: #27272a; /* Subtle borders */
105
+ --accent: #ffffff; /* Stark white for key actions */
106
+ --accent-dim: #a1a1aa; /* Secondary text */
107
+ --highlight: #6366f1; /* Primary Action Color (Indigo) */
108
+ --success: #10b981;
109
+ --font-ui: 'Inter', sans-serif;
110
+ --font-mono: 'JetBrains Mono', monospace;
111
  }
112
 
113
  * { box-sizing: border-box; outline: none; }
114
 
115
  body {
116
+ font-family: var(--font-ui);
117
+ background-color: var(--bg-main);
118
+ color: #e4e4e7;
 
119
  margin: 0;
120
  height: 100vh;
121
  display: flex;
 
124
  }
125
 
126
  /* --- ANIMATIONS --- */
127
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
128
+ @keyframes slideInRight { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: translateX(0); } }
129
+ @keyframes scan {
130
  0% { top: 0%; opacity: 0; }
131
+ 10% { opacity: 1; box-shadow: 0 0 10px var(--highlight); }
132
+ 90% { opacity: 1; box-shadow: 0 0 10px var(--highlight); }
133
  100% { top: 100%; opacity: 0; }
134
  }
135
+ @keyframes pulse { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } }
136
 
137
+ /* --- HEADER --- */
138
  header {
139
+ height: 50px;
140
+ background: var(--bg-main);
141
  border-bottom: 1px solid var(--border);
 
 
142
  display: flex;
143
  justify-content: space-between;
144
  align-items: center;
145
+ padding: 0 20px;
146
  z-index: 50;
 
147
  }
148
 
149
+ .brand {
150
+ font-weight: 600;
151
+ font-size: 0.95rem;
152
+ letter-spacing: -0.02em;
153
+ display: flex; align-items: center; gap: 8px;
154
+ color: var(--accent);
155
+ }
156
+
157
+ .status-dot {
158
+ width: 6px; height: 6px;
159
+ background: var(--success);
160
+ border-radius: 50%;
161
+ box-shadow: 0 0 8px var(--success);
162
+ animation: pulse 2s infinite;
163
  }
164
 
165
  main { flex: 1; display: flex; overflow: hidden; }
166
 
167
+ /* --- SIDEBAR --- */
168
  .sidebar {
169
+ width: 260px;
170
+ background: var(--bg-main);
171
  border-right: 1px solid var(--border);
172
+ display: flex; flex-direction: column;
173
+ padding: 15px;
174
+ gap: 20px;
175
+ z-index: 20;
176
  }
177
 
178
+ .upload-zone {
179
+ border: 1px dashed var(--border);
180
+ background: var(--bg-panel);
181
+ border-radius: 6px;
182
+ padding: 25px 15px;
183
  text-align: center;
184
  cursor: pointer;
185
+ transition: all 0.2s ease;
186
+ }
187
+ .upload-zone:hover { border-color: var(--highlight); background: #1e1e24; }
188
+ .upload-zone i { font-size: 24px; color: var(--accent-dim); margin-bottom: 8px; display: block; }
189
+ .upload-zone span { font-size: 0.8rem; color: var(--accent-dim); font-weight: 500; }
190
+
191
+ .control-section { display: flex; flex-direction: column; gap: 10px; }
192
+ .control-label {
193
+ font-size: 0.7rem;
194
+ text-transform: uppercase;
195
+ letter-spacing: 0.05em;
196
+ color: #52525b;
197
+ font-weight: 600;
198
+ margin-bottom: 5px;
199
+ }
200
+
201
+ /* Minimal Inputs */
202
+ .toggle-row { display: flex; align-items: center; justify-content: space-between; font-size: 0.85rem; }
203
+ input[type="checkbox"] { accent-color: var(--highlight); }
204
+ input[type="range"] { width: 100%; height: 4px; background: var(--border); border-radius: 2px; -webkit-appearance: none; }
205
+ input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; background: var(--accent); border-radius: 50%; cursor: pointer; }
206
+
207
+ /* Buttons Grid */
208
+ .action-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
209
+ .btn {
210
+ background: var(--bg-panel);
211
+ border: 1px solid var(--border);
212
+ color: var(--accent-dim);
213
+ padding: 8px;
214
+ border-radius: 4px;
215
+ font-size: 0.8rem;
216
+ cursor: pointer;
217
+ transition: 0.2s;
218
+ display: flex; align-items: center; justify-content: center; gap: 6px;
219
  }
220
+ .btn:hover { border-color: var(--highlight); color: var(--highlight); }
221
+ .btn:active { transform: translateY(1px); }
222
+ .btn.primary { background: var(--highlight); color: white; border: none; }
223
+ .btn.primary:hover { background: #4f46e5; }
224
 
225
+ /* --- WORKSPACE --- */
226
  .workspace {
227
  flex: 1;
228
  display: flex;
229
+ background: #000; /* Pitch black for contrast */
230
  position: relative;
 
231
  }
232
 
233
+ /* LEFT: Canvas */
234
+ .preview-area {
235
  flex: 1;
236
  position: relative;
237
+ overflow: auto;
238
  display: grid;
239
  place-items: center;
240
+ padding: 40px;
241
  }
242
+
 
 
 
 
 
 
243
  .canvas-wrapper {
244
  position: relative;
245
+ box-shadow: 0 0 0 1px #333; /* Very subtle outline */
246
  transition: width 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94), transform 0.3s ease;
247
+ width: 80%;
248
  transform-origin: center center;
249
  }
250
 
 
251
  canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
252
  img { display: block; width: 100%; height: auto; }
253
 
 
254
  .scan-line {
255
+ position: absolute; width: 100%; height: 1px;
256
+ background: var(--highlight);
257
+ box-shadow: 0 0 10px var(--highlight);
258
+ z-index: 5; display: none; pointer-events: none;
259
+ }
260
+ .scan-line.active { display: block; animation: scan 1.5s linear infinite; }
261
+
262
+ /* Floating Controls */
263
+ .floating-bar {
264
+ position: absolute; bottom: 25px; left: 50%; transform: translateX(-50%);
265
+ background: rgba(24, 24, 27, 0.9);
266
+ border: 1px solid var(--border);
267
+ padding: 6px 12px;
268
+ border-radius: 8px;
269
+ display: flex; gap: 12px; align-items: center;
270
+ backdrop-filter: blur(8px);
271
+ z-index: 30;
272
+ }
273
+ .icon-btn { background: none; border: none; color: var(--accent-dim); cursor: pointer; padding: 4px; border-radius: 4px; display: flex; }
274
+ .icon-btn:hover { color: var(--accent); background: rgba(255,255,255,0.1); }
275
+
276
+ /* RIGHT: Results */
277
+ .results-panel {
278
+ width: 380px;
279
+ background: var(--bg-panel);
 
280
  border-left: 1px solid var(--border);
281
  display: flex; flex-direction: column;
 
282
  z-index: 20;
283
  }
284
 
285
+ .panel-header {
286
+ padding: 10px 15px;
287
+ border-bottom: 1px solid var(--border);
288
+ display: flex; justify-content: space-between; align-items: center;
289
+ font-size: 0.8rem; font-weight: 600; color: var(--accent-dim);
 
 
 
 
290
  }
291
+
292
+ .tab-group { display: flex; gap: 10px; }
293
+ .tab-btn { background: none; border: none; cursor: pointer; color: #52525b; font-size: 0.8rem; font-weight: 600; transition: 0.2s; }
294
+ .tab-btn:hover { color: var(--accent-dim); }
295
+ .tab-btn.active { color: var(--highlight); }
296
+
297
+ .results-content { flex: 1; overflow-y: auto; padding: 10px; }
298
+
299
+ .result-item {
300
+ display: flex; justify-content: space-between;
301
+ padding: 10px;
302
+ border-bottom: 1px solid var(--border);
303
+ font-size: 0.85rem;
304
+ color: var(--accent-dim);
305
+ cursor: pointer;
306
+ transition: 0.15s;
307
+ animation: slideInRight 0.3s backwards;
308
  }
309
+ .result-item:hover { background: #27272a; color: var(--accent); padding-left: 14px; }
310
+ .result-item span { font-family: var(--font-mono); }
311
+ .conf-tag { font-size: 0.7rem; color: #52525b; }
312
 
313
  /* LOADER */
314
  .loader-overlay {
315
  position: fixed; top: 0; left: 0; width: 100%; height: 100%;
316
+ background: rgba(9, 9, 11, 0.9); z-index: 100;
317
  display: flex; justify-content: center; align-items: center; flex-direction: column;
318
+ opacity: 0; pointer-events: none; transition: 0.3s;
319
  }
320
  .loader-overlay.active { opacity: 1; pointer-events: all; }
321
+ .loader-bar {
322
+ width: 200px; height: 2px; background: #333;
323
+ overflow: hidden; border-radius: 2px; margin-top: 10px;
 
 
324
  }
325
+ .loader-progress {
326
+ width: 100%; height: 100%; background: var(--highlight);
327
+ transform: translateX(-100%);
328
+ animation: loading 1.5s infinite ease-in-out;
 
 
 
 
 
 
 
329
  }
330
+ @keyframes loading { 0% { transform: translateX(-100%); } 50% { transform: translateX(0); } 100% { transform: translateX(100%); } }
331
+
332
+ .raw-text { width: 100%; height: 100%; background: transparent; border: none; color: var(--accent-dim); resize: none; font-family: var(--font-mono); font-size: 0.8rem; line-height: 1.6; padding: 5px; }
333
  </style>
334
  </head>
335
  <body>
336
 
337
  <header>
338
+ <div class="brand">
339
+ <i class="ph-bold ph-read-cv-logo"></i> OCR STUDIO <span style="font-weight:400; opacity:0.5; font-size:0.8em; margin-left:5px;">PRO</span>
340
+ </div>
341
+ <div style="display:flex; align-items:center; gap:8px; font-size:0.75rem; color:#71717a;">
342
+ <div class="status-dot"></div> READY
343
  </div>
344
  </header>
345
 
346
  <main>
347
+ <!-- SIDEBAR -->
348
  <aside class="sidebar">
349
+ <div class="upload-zone" id="drop-zone">
350
+ <i class="ph ph-upload-simple"></i>
351
+ <span>Open File</span>
352
  <input type="file" id="file-input" hidden accept="image/*,application/pdf">
353
  </div>
354
 
355
+ <div class="control-section">
356
+ <div class="control-label">Settings</div>
357
+ <div class="toggle-row">
358
+ <span>Auto-Rotate</span>
359
+ <input type="checkbox" id="angle-cls">
360
+ </div>
361
+ <div class="toggle-row" style="margin-top:10px;">
362
+ <span>Contrast Mode</span>
363
+ <input type="checkbox" id="contrast-mode" onchange="toggleContrast()">
364
+ </div>
365
  </div>
366
+
367
+ <div class="control-section">
368
+ <div class="control-label">Confidence Threshold <span id="conf-val" style="float:right">0%</span></div>
369
+ <input type="range" id="conf-slider" min="0" max="100" value="0">
370
  </div>
371
+
372
+ <div class="control-section" style="margin-top:auto">
373
+ <div class="control-label">Tools</div>
374
+ <div class="action-grid">
375
+ <button class="btn" onclick="readAloud()"><i class="ph-bold ph-speaker-high"></i> Read</button>
376
+ <button class="btn" onclick="copyText()"><i class="ph-bold ph-copy"></i> Copy</button>
377
+ <button class="btn" onclick="downloadJSON()"><i class="ph-bold ph-file-json"></i> JSON</button>
378
+ <button class="btn" onclick="resetUI()"><i class="ph-bold ph-trash"></i> Reset</button>
379
+ </div>
380
  </div>
381
  </aside>
382
 
383
+ <!-- WORKSPACE -->
384
  <div class="workspace">
385
+ <div class="preview-area">
 
 
 
 
 
 
 
386
  <div class="canvas-wrapper" id="canvas-wrapper">
387
  <img id="source-image" src="">
388
  <canvas id="overlay"></canvas>
389
  <div class="scan-line" id="scan-line"></div>
390
  </div>
391
 
392
+ <!-- Floating Toolbar -->
393
+ <div class="floating-bar" id="pagination-bar" style="display:none">
394
+ <button class="icon-btn" onclick="changePage(-1)"><i class="ph-bold ph-caret-left"></i></button>
395
+ <span id="page-indicator" style="font-size:0.8rem; font-variant-numeric: tabular-nums; color:white;">1 / 1</span>
396
+ <button class="icon-btn" onclick="changePage(1)"><i class="ph-bold ph-caret-right"></i></button>
397
+ <div style="width:1px; height:14px; background:#333; margin:0 5px;"></div>
398
+ <button class="icon-btn" onclick="rotateView()"><i class="ph-bold ph-arrow-clockwise"></i></button>
399
+ <button class="icon-btn" onclick="zoom(-10)"><i class="ph-bold ph-minus"></i></button>
400
+ <button class="icon-btn" onclick="resetZoom()" style="font-size:0.7rem; width:30px;">100%</button>
401
+ <button class="icon-btn" onclick="zoom(10)"><i class="ph-bold ph-plus"></i></button>
402
  </div>
403
  </div>
404
 
405
+ <div class="results-panel">
406
+ <div class="panel-header">
407
+ <span>DATA</span>
408
+ <div class="tab-group">
409
+ <button class="tab-btn active" id="tab-blocks" onclick="setView('blocks')">LIST</button>
410
+ <button class="tab-btn" id="tab-raw" onclick="setView('raw')">RAW</button>
411
  </div>
412
  </div>
413
  <div class="results-content" id="results-content"></div>
 
415
  </div>
416
  </main>
417
 
418
+ <!-- LOADER -->
419
  <div class="loader-overlay" id="loader">
420
+ <div style="text-align:center">
421
+ <div style="font-size:0.9rem; letter-spacing:0.1em; color:var(--accent); font-weight:600; margin-bottom:10px" id="loader-msg">PROCESSING</div>
422
+ <div class="loader-bar"><div class="loader-progress"></div></div>
423
+ </div>
424
  </div>
425
 
426
  <script>
 
444
  const pageIndicator = document.getElementById('page-indicator');
445
  const ctx = canvas.getContext('2d');
446
  const scanLine = document.getElementById('scan-line');
447
+ const confSlider = document.getElementById('conf-slider');
448
+ const confVal = document.getElementById('conf-val');
449
 
450
  // --- EVENTS ---
451
  dropZone.addEventListener('click', () => fileInput.click());
452
  fileInput.addEventListener('change', (e) => startUpload(e.target.files[0]));
453
+ confSlider.addEventListener('input', (e) => {
454
+ confVal.textContent = e.target.value + '%';
455
+ renderCurrentPage();
456
+ });
457
 
458
  // --- UPLOAD & STREAMING ---
459
  async function startUpload(file) {
 
468
  updateTransform();
469
 
470
  loader.classList.add('active');
471
+ loaderMsg.textContent = "INITIALIZING STREAM";
472
 
473
  const formData = new FormData();
474
  formData.append('file', file);
 
505
 
506
  function handleStreamData(data) {
507
  if (data.type === 'status') {
508
+ loaderMsg.textContent = data.message.toUpperCase();
509
  } else if (data.type === 'page') {
510
  allPages.push(data);
511
  if (allPages.length === 1) {
 
513
  paginationBar.style.display = 'flex';
514
  renderPage(0);
515
  scanLine.classList.add('active');
516
+ setTimeout(() => scanLine.classList.remove('active'), 1500);
517
  }
518
  updatePaginationUI();
519
  }
 
538
  function renderCurrentPage(highlightId = -1) {
539
  if (allPages.length === 0) return;
540
  const pageData = allPages[currentPageIdx];
541
+ const minConf = confSlider.value / 100;
542
 
543
  ctx.clearRect(0, 0, canvas.width, canvas.height);
544
 
 
553
  ctx.closePath();
554
 
555
  if (block.id === highlightId) {
556
+ ctx.strokeStyle = '#6366f1';
557
  ctx.lineWidth = 4;
558
+ ctx.fillStyle = 'rgba(99, 102, 241, 0.3)';
559
  } else {
560
+ ctx.strokeStyle = 'rgba(99, 102, 241, 0.5)';
561
+ ctx.lineWidth = 1;
562
+ ctx.fillStyle = 'rgba(99, 102, 241, 0.02)';
563
  }
564
  ctx.fill();
565
  ctx.stroke();
 
574
  function renderList() {
575
  resultsContent.innerHTML = '';
576
  if (currentView === 'raw') {
577
+ resultsContent.innerHTML = `<textarea class="raw-text" readonly>${allPages[currentPageIdx].full_text}</textarea>`;
578
  return;
579
  }
580
 
581
+ const minConf = confSlider.value / 100;
582
  allPages[currentPageIdx].blocks.forEach((block, index) => {
583
  if (block.conf < minConf) return;
584
 
585
  const div = document.createElement('div');
586
+ div.className = 'result-item';
587
+ div.style.animationDelay = `${index * 0.02}s`;
588
+ div.innerHTML = `<span>${block.text}</span><span class="conf-tag">${(block.conf*100).toFixed(0)}%</span>`;
589
 
590
  div.addEventListener('mouseenter', () => drawCanvas(block.id));
591
  div.addEventListener('mouseleave', () => drawCanvas(-1));
 
604
  }
605
 
606
  function updatePaginationUI() {
607
+ pageIndicator.textContent = `${currentPageIdx + 1} / ${allPages.length}`;
608
  renderList();
609
  }
610
 
611
  function setView(view) {
612
  currentView = view;
613
+ document.getElementById('tab-blocks').classList.toggle('active', view === 'blocks');
614
+ document.getElementById('tab-raw').classList.toggle('active', view === 'raw');
615
  renderList();
616
  if(view === 'raw') ctx.clearRect(0, 0, canvas.width, canvas.height);
617
  }
 
644
  node.remove();
645
  }
646
 
647
+ function readAloud() {
648
+ if (allPages.length === 0) return;
649
+ const text = currentView === 'raw' ?
650
+ allPages[currentPageIdx].full_text :
651
+ allPages[currentPageIdx].blocks.map(b => b.text).join(' ');
652
+
653
+ if (!text) return alert("No text to read");
654
+
655
+ window.speechSynthesis.cancel();
656
+ const utterance = new SpeechSynthesisUtterance(text);
657
+ window.speechSynthesis.speak(utterance);
658
+ }
659
+
660
+ function copyText() {
661
+ if (allPages.length === 0) return;
662
+ const text = allPages[currentPageIdx].full_text;
663
+ navigator.clipboard.writeText(text);
664
+ const btn = event.currentTarget;
665
+ const originalHTML = btn.innerHTML;
666
+ btn.innerHTML = `<i class="ph-bold ph-check"></i> Copied`;
667
+ setTimeout(() => btn.innerHTML = originalHTML, 2000);
668
+ }
669
+
670
+ function toggleContrast() {
671
+ const isContrast = document.getElementById('contrast-mode').checked;
672
+ if (isContrast) {
673
+ document.documentElement.style.setProperty('--bg-main', '#000000');
674
+ document.documentElement.style.setProperty('--bg-panel', '#000000');
675
+ document.documentElement.style.setProperty('--border', '#ffffff');
676
+ document.documentElement.style.setProperty('--accent', '#ffff00');
677
+ document.documentElement.style.setProperty('--accent-dim', '#ffffff');
678
+ } else {
679
+ document.documentElement.style.removeProperty('--bg-main');
680
+ document.documentElement.style.removeProperty('--bg-panel');
681
+ document.documentElement.style.removeProperty('--border');
682
+ document.documentElement.style.removeProperty('--accent');
683
+ document.documentElement.style.removeProperty('--accent-dim');
684
+ }
685
+ }
686
+
687
+ function resetUI() {
688
+ location.reload();
689
+ }
690
+
691
  </script>
692
  </body>
693
  </html>