shethjenil commited on
Commit
21db406
·
1 Parent(s): 6e39831
app.py ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ import time
3
+ import json
4
+ import gradio as gr
5
+ import pandas as pd
6
+ from gocr import ScreenAIClient
7
+ from gocr.models import int_to_hex_rgb
8
+
9
+ # =========================================================
10
+ # Lazy Singleton OCR Client
11
+ # =========================================================
12
+
13
+ _client = None
14
+
15
+
16
+ def get_client():
17
+ global _client
18
+ if _client is None:
19
+ from huggingface_hub import snapshot_download
20
+ from pathlib import Path
21
+ assets_dir = Path(snapshot_download(repo_id="shethjenil/Google-OCR"))
22
+ lib_path = assets_dir / "libchromescreenai.so"
23
+ _client = ScreenAIClient(library_path=lib_path, assets_dir=assets_dir)
24
+ _client.init_ocr()
25
+
26
+ return _client
27
+
28
+
29
+ # =========================================================
30
+ # HTML Templates (loaded once from files)
31
+ # =========================================================
32
+
33
+ _TEMPLATES_DIR = Path(__file__).resolve().parent / "templates"
34
+
35
+ STATS_GRID_TPL = (_TEMPLATES_DIR / "stats_grid.html").read_text()
36
+ TREE_BLOCK_TPL = (_TEMPLATES_DIR / "tree_block.html").read_text()
37
+ TREE_PARA_TPL = (_TEMPLATES_DIR / "tree_paragraph.html").read_text()
38
+ TREE_LINE_TPL = (_TEMPLATES_DIR / "tree_line.html").read_text()
39
+ TREE_WORD_BADGE_TPL = (_TEMPLATES_DIR / "tree_word_badge.html").read_text()
40
+ EMPTY_STATE_TPL = (_TEMPLATES_DIR / "empty_state.html").read_text()
41
+ ERROR_MSG_TPL = (_TEMPLATES_DIR / "error_msg.html").read_text()
42
+ HEADER_HTML = (_TEMPLATES_DIR / "header.html").read_text()
43
+
44
+ # =========================================================
45
+ # Auxiliary Helper Functions
46
+ # =========================================================
47
+
48
+
49
+
50
+ def make_stats_html(metrics_dict, latency="N/A") -> str:
51
+ if not metrics_dict:
52
+ return EMPTY_STATE_TPL.format(message="No stats. Run OCR.")
53
+ return STATS_GRID_TPL.format(
54
+ avg_conf=f"{metrics_dict.get('average_confidence', 0.0):.1%}",
55
+ latency=latency,
56
+ blocks=metrics_dict.get("total_blocks", 0),
57
+ paragraphs=metrics_dict.get("total_paragraphs", 0),
58
+ lines=metrics_dict.get("total_lines", 0),
59
+ words=metrics_dict.get("total_words", 0),
60
+ symbols=metrics_dict.get("total_symbols", 0),
61
+ lang=metrics_dict.get("primary_language", "und"),
62
+ )
63
+
64
+
65
+ def make_tree_html(parsed_data) -> str:
66
+ if not parsed_data:
67
+ return EMPTY_STATE_TPL.format(message="No layout structure. Run OCR first!")
68
+
69
+ parts = ['<div class="layout-tree">']
70
+ for block in parsed_data.get("blocks", []):
71
+ b_header = f"Block #{block['block_id']} - Conf: {block['confidence']:.1%} | BBox: [{block['bounding_box']['x']}, {block['bounding_box']['y']}, {block['bounding_box']['width']}×{block['bounding_box']['height']}]"
72
+
73
+ paras = []
74
+ for para in block.get("paragraphs", []):
75
+ pbox = para["bounding_box"]
76
+ p_header = f"Paragraph #{para['paragraph_id']} - Conf: {para['confidence']:.1%} | BBox: [{pbox['x']}, {pbox['y']}, {pbox['width']}×{pbox['height']}]"
77
+
78
+ lines = []
79
+ for line in para.get("lines", []):
80
+ l_header = f"Line: \"{line['utf8_string']}\" - Conf: {line['confidence']:.1%} | Lang: {line['language']} | Dir: {line['direction']}"
81
+
82
+ words_html = []
83
+ for word in line.get("words", []):
84
+ fg_hex = int_to_hex_rgb(word["foreground_rgb_value"])
85
+ bg_hex = int_to_hex_rgb(word["background_rgb_value"])
86
+ tooltip = f"Confidence: {word['confidence']:.1%}\\nLanguage: {word['language']}\\nDirection: {word['direction']}\\nType: {word['content_type']}\\nColors - FG: {fg_hex} | BG: {bg_hex}\\nSpace After: {word['has_space_after']}"
87
+
88
+ symbols_badge = ""
89
+ if word.get("symbols"):
90
+ sym_list = [
91
+ f"'{sym['utf8_string']}' ({sym['confidence']:.1%})"
92
+ for sym in word["symbols"]
93
+ ]
94
+ symbols_badge = f' <span title="{"\\n".join(sym_list)}" class="ml-1 border-l border-emerald-500/30 pl-1.5 text-xs opacity-60">{len(word["symbols"])} sym</span>'
95
+
96
+ words_html.append(
97
+ TREE_WORD_BADGE_TPL.format(
98
+ tooltip=tooltip,
99
+ text=word["utf8_string"],
100
+ symbols_badge=symbols_badge,
101
+ )
102
+ )
103
+
104
+ lines.append(
105
+ TREE_LINE_TPL.format(header=l_header, content="\n".join(words_html))
106
+ )
107
+
108
+ paras.append(
109
+ TREE_PARA_TPL.format(header=p_header, content="\n".join(lines))
110
+ )
111
+
112
+ parts.append(
113
+ TREE_BLOCK_TPL.format(header=b_header, content="\n".join(paras))
114
+ )
115
+
116
+ parts.append("</div>")
117
+ return "\n".join(parts)
118
+
119
+
120
+ def update_explorer_table(parsed_data, level) -> pd.DataFrame:
121
+ if not parsed_data or not level:
122
+ return pd.DataFrame()
123
+ rows = []
124
+
125
+ if level == "Blocks":
126
+ for b in parsed_data.get("blocks", []):
127
+ rows.append(
128
+ {
129
+ "Block ID": b["block_id"],
130
+ "Text": b["utf8_string"][:100] + "..."
131
+ if len(b["utf8_string"]) > 100
132
+ else b["utf8_string"],
133
+ "Confidence": f"{b['confidence']:.2%}",
134
+ "Language": b["language"],
135
+ "Paragraphs": b["paragraph_count"],
136
+ "Lines": b["line_count"],
137
+ }
138
+ )
139
+ elif level == "Paragraphs":
140
+ for p in parsed_data.get("paragraphs", []):
141
+ rows.append(
142
+ {
143
+ "Paragraph ID": p["paragraph_id"],
144
+ "Block ID": p["block_id"],
145
+ "Text": p["utf8_string"][:100] + "..."
146
+ if len(p["utf8_string"]) > 100
147
+ else p["utf8_string"],
148
+ "Confidence": f"{p['confidence']:.2%}",
149
+ "Language": p["language"],
150
+ "Lines": p["line_count"],
151
+ }
152
+ )
153
+ elif level == "Lines":
154
+ for ln in parsed_data.get("lines", []):
155
+ rows.append(
156
+ {
157
+ "Line Index": ln["index"],
158
+ "Block ID": ln["block_id"],
159
+ "Paragraph ID": ln["paragraph_id"],
160
+ "Text": ln["utf8_string"],
161
+ "Confidence": f"{ln['confidence']:.2%}",
162
+ "Language": ln["language"],
163
+ "Direction": ln["direction"],
164
+ "Content Type": ln["content_type"],
165
+ }
166
+ )
167
+ elif level == "Words":
168
+ for ln in parsed_data.get("lines", []):
169
+ for w in ln["words"]:
170
+ fg_color = int_to_hex_rgb(w["foreground_rgb_value"])
171
+ bg_color = int_to_hex_rgb(w["background_rgb_value"])
172
+ rows.append(
173
+ {
174
+ "Word Index": w["index"],
175
+ "Line Index": ln["index"],
176
+ "Word Text": w["utf8_string"],
177
+ "Confidence": f"{w['confidence']:.2%}",
178
+ "Language": w["language"],
179
+ "FG Color": fg_color,
180
+ "BG Color": bg_color,
181
+ "Has Space After": "Yes" if w["has_space_after"] else "No",
182
+ }
183
+ )
184
+ elif level == "Symbols":
185
+ for ln in parsed_data.get("lines", []):
186
+ for w in ln["words"]:
187
+ for s in w["symbols"]:
188
+ rows.append(
189
+ {
190
+ "Symbol Index": s["index"],
191
+ "Word Index": w["index"],
192
+ "Line Index": ln["index"],
193
+ "Char": s["utf8_string"],
194
+ "Confidence": f"{s['confidence']:.2%}",
195
+ }
196
+ )
197
+ return pd.DataFrame(rows)
198
+
199
+
200
+ # =========================================================
201
+ # OCR Core Pipeline Handler
202
+ # =========================================================
203
+
204
+
205
+ def process_image(image, explorer_level):
206
+ if image is None:
207
+ return (
208
+ "",
209
+ "",
210
+ EMPTY_STATE_TPL.format(message="No image."),
211
+ EMPTY_STATE_TPL.format(message="N/A"),
212
+ pd.DataFrame(),
213
+ {},
214
+ {},
215
+ )
216
+
217
+ start = time.time()
218
+ try:
219
+ client = get_client()
220
+ annotation = client.perform_ocr(image)
221
+ if not annotation:
222
+ return (
223
+ "",
224
+ "No text detected.",
225
+ EMPTY_STATE_TPL.format(message="No text found."),
226
+ EMPTY_STATE_TPL.format(message="N/A"),
227
+ pd.DataFrame(),
228
+ {},
229
+ {},
230
+ )
231
+
232
+ latency = f"{time.time() - start:.2f}s"
233
+ parsed_data = annotation.model_dump()
234
+ plain_text = annotation.text
235
+ tree_html = make_tree_html(parsed_data)
236
+ stats_html = make_stats_html(parsed_data["metrics"], latency)
237
+ df_explorer = update_explorer_table(parsed_data, explorer_level)
238
+
239
+ # Dump JSON payload for browser interactive canvas (image is loaded directly from Gradio via client-side JS)
240
+ payload = json.dumps({"annotation": parsed_data})
241
+
242
+ return (
243
+ payload,
244
+ plain_text,
245
+ tree_html,
246
+ stats_html,
247
+ df_explorer,
248
+ parsed_data,
249
+ parsed_data,
250
+ )
251
+ except Exception as e:
252
+ err_msg = f"ERROR occurred during processing:\n{e}"
253
+ return (
254
+ "",
255
+ err_msg,
256
+ ERROR_MSG_TPL.format(message=err_msg),
257
+ "Failed",
258
+ pd.DataFrame(),
259
+ {},
260
+ {},
261
+ )
262
+
263
+
264
+ # =========================================================
265
+ # Gradio Blocks UI Definition
266
+ # =========================================================
267
+
268
+ with gr.Blocks(
269
+ title="Google Chrome Screen AI OCR Dashboard",
270
+ ) as demo:
271
+ parsed_dict_state = gr.State(None)
272
+
273
+ gr.HTML(HEADER_HTML)
274
+
275
+ with gr.Row():
276
+ with gr.Column(scale=4, elem_classes=["sidebar-panel"]):
277
+ input_image = gr.Image(
278
+ type="pil",
279
+ label="Upload Image",
280
+ sources=["upload", "clipboard", "webcam"],
281
+ elem_id="input-image",
282
+ )
283
+ run_btn = gr.Button(
284
+ "Execute OCR", variant="primary", elem_id="run-btn"
285
+ )
286
+
287
+ with gr.Column(scale=7):
288
+ stats_output = gr.HTML(
289
+ value=EMPTY_STATE_TPL.format(message="Run OCR to see statistics.")
290
+ )
291
+
292
+ with gr.Tabs():
293
+ with gr.Tab("Interactive Canvas"):
294
+ gr.HTML(value=Path("canvas.html").read_text(), js_on_load=Path("canvas.js").read_text())
295
+ # Hidden text transfer element populated by python to update canvas
296
+ data_transfer_el = gr.Textbox(
297
+ visible=False, elem_id="ocr-data-transfer"
298
+ )
299
+
300
+ with gr.Tab("Layout Tree"):
301
+ tree_output = gr.HTML(label="Layout Tree")
302
+
303
+ with gr.Tab("Tabular Explorer"):
304
+ explorer_level = gr.Radio(
305
+ choices=["Blocks", "Paragraphs", "Lines", "Words", "Symbols"],
306
+ value="Paragraphs",
307
+ label="View Level",
308
+ )
309
+ table_output = gr.DataFrame(interactive=False, wrap=True)
310
+
311
+ with gr.Tab("Plain Text"):
312
+ text_output = gr.Textbox(
313
+ lines=18, label="Extracted Text"
314
+ )
315
+
316
+ with gr.Tab("Developer JSON"):
317
+ json_output = gr.JSON(
318
+ label="Raw OCR Data"
319
+ )
320
+
321
+ explorer_level.change(
322
+ fn=update_explorer_table,
323
+ inputs=[parsed_dict_state, explorer_level],
324
+ outputs=table_output,
325
+ )
326
+
327
+ run_btn.click(
328
+ fn=process_image,
329
+ inputs=[input_image, explorer_level],
330
+ outputs=[
331
+ data_transfer_el,
332
+ text_output,
333
+ tree_output,
334
+ stats_output,
335
+ table_output,
336
+ json_output,
337
+ parsed_dict_state,
338
+ ],
339
+ ).then(
340
+ fn=None,
341
+ inputs=[data_transfer_el],
342
+ outputs=[],
343
+ js="""
344
+ (payload_str) => {
345
+ if (window.loadStreamedData) {
346
+ window.loadStreamedData(payload_str);
347
+ }
348
+ }
349
+ """,
350
+ )
351
+
352
+ if __name__ == "__main__":
353
+ demo.launch(
354
+ css=Path("custom.css").read_text(),
355
+ head='<script>tailwind.config={theme:{extend:{colors:{border:"hsl(var(--bd))",input:"hsl(var(--bd))",ring:"hsl(var(--ri))",background:"hsl(var(--bg))",foreground:"hsl(var(--fg))",primary:{DEFAULT:"hsl(var(--pr))",foreground:"hsl(var(--fg))"},secondary:{DEFAULT:"hsl(var(--bd))",foreground:"hsl(var(--fg))"},destructive:{DEFAULT:"hsl(0 62.8% 30.6%)",foreground:"hsl(var(--fg))"},muted:{DEFAULT:"hsl(var(--mu))",foreground:"hsl(var(--mu-fg))"},accent:{DEFAULT:"hsl(var(--ac))",foreground:"hsl(var(--ac-fg))"},card:{DEFAULT:"hsl(var(--ca))",foreground:"hsl(var(--ca-fg))"},popover:{DEFAULT:"hsl(var(--ca))",foreground:"hsl(var(--ca-fg))"}},borderRadius:{lg:"var(--rd)",md:"calc(var(--rd)-2px)",sm:"calc(var(--rd)-4px)"}}}}</script><script src="https://cdn.tailwindcss.com"></script>',
356
+ )
canvas.html ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="flex flex-col gap-3 rounded-xl border border-border bg-card p-4">
2
+ <div class="flex flex-wrap items-center justify-between gap-2 rounded-lg border border-border bg-muted/50 p-2.5">
3
+ <div class="canvas-toggles flex flex-wrap gap-1.5">
4
+ <label class="has-[:checked]:bg-pink-500/20 has-[:checked]:text-pink-300 has-[:checked]:ring-1 has-[:checked]:ring-pink-500/40 inline-flex cursor-pointer items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium text-muted-foreground transition-all hover:bg-white/5">
5
+ <input type="checkbox" id="cb-blocks" class="sr-only">
6
+ <span class="inline-block h-2 w-2 rounded-full bg-pink-400"></span>Blocks
7
+ </label>
8
+ <label class="has-[:checked]:bg-cyan-500/20 has-[:checked]:text-cyan-300 has-[:checked]:ring-1 has-[:checked]:ring-cyan-500/40 inline-flex cursor-pointer items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium text-muted-foreground transition-all hover:bg-white/5">
9
+ <input type="checkbox" id="cb-paragraphs" checked class="sr-only">
10
+ <span class="inline-block h-2 w-2 rounded-full bg-cyan-400"></span>Paragraphs
11
+ </label>
12
+ <label class="has-[:checked]:bg-violet-500/20 has-[:checked]:text-violet-300 has-[:checked]:ring-1 has-[:checked]:ring-violet-500/40 inline-flex cursor-pointer items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium text-muted-foreground transition-all hover:bg-white/5">
13
+ <input type="checkbox" id="cb-lines" checked class="sr-only">
14
+ <span class="inline-block h-2 w-2 rounded-full bg-violet-400"></span>Lines
15
+ </label>
16
+ <label class="has-[:checked]:bg-emerald-500/20 has-[:checked]:text-emerald-300 has-[:checked]:ring-1 has-[:checked]:ring-emerald-500/40 inline-flex cursor-pointer items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium text-muted-foreground transition-all hover:bg-white/5">
17
+ <input type="checkbox" id="cb-words" checked class="sr-only">
18
+ <span class="inline-block h-2 w-2 rounded-full bg-emerald-400"></span>Words
19
+ </label>
20
+ <label class="has-[:checked]:bg-amber-500/20 has-[:checked]:text-amber-300 has-[:checked]:ring-1 has-[:checked]:ring-amber-500/40 inline-flex cursor-pointer items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium text-muted-foreground transition-all hover:bg-white/5">
21
+ <input type="checkbox" id="cb-symbols" class="sr-only">
22
+ <span class="inline-block h-2 w-2 rounded-full bg-amber-400"></span>Symbols
23
+ </label>
24
+ <label class="has-[:checked]:bg-red-500/20 has-[:checked]:text-red-300 has-[:checked]:ring-1 has-[:checked]:ring-red-500/40 inline-flex cursor-pointer items-center gap-1.5 rounded-full px-3 py-1 text-xs font-medium text-muted-foreground transition-all hover:bg-white/5">
25
+ <input type="checkbox" id="cb-whitespace" class="sr-only">
26
+ <span class="inline-block h-2 w-2 rounded-full bg-red-400"></span>Whitespace
27
+ </label>
28
+ </div>
29
+ <div class="flex items-center gap-2">
30
+ <div class="flex items-center gap-1 rounded-lg border border-border bg-background px-2 py-1">
31
+ <button id="btn-zoom-out" class="flex h-6 w-6 items-center justify-center rounded text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground" title="Zoom Out">−</button>
32
+ <span id="zoom-level-text" class="min-w-[3ch] text-center text-xs font-semibold text-foreground">100%</span>
33
+ <button id="btn-zoom-in" class="flex h-6 w-6 items-center justify-center rounded text-xs text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground" title="Zoom In">+</button>
34
+ </div>
35
+ <button id="btn-zoom-reset" class="rounded-md border border-border bg-background px-2 py-1 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground">Reset</button>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="grid grid-cols-1 gap-3 lg:grid-cols-[3fr_2fr]">
40
+ <div id="canvas-viewport" class="canvas-viewport-container relative flex min-h-[400px] max-h-[600px] cursor-grab select-none items-center justify-center overflow-hidden rounded-lg border border-border bg-background">
41
+ <div class="viewport-wrapper relative inline-block max-w-full" style="transform-origin:center center">
42
+ <img id="ocr-svg-image" class="block max-w-full select-none" style="-webkit-user-drag:none">
43
+ <svg id="ocr-svg-overlay" class="pointer-events-auto absolute inset-0 h-full w-full"></svg>
44
+ </div>
45
+ <div id="canvas-placeholder-msg" class="absolute text-sm font-medium text-muted-foreground select-none pointer-events-none">No image loaded</div>
46
+ </div>
47
+
48
+ <div class="flex max-h-[600px] flex-col gap-3 overflow-y-auto rounded-lg border border-border bg-card p-4">
49
+ <div class="border-b border-border pb-2">
50
+ <span class="text-sm font-semibold text-foreground">Properties</span>
51
+ </div>
52
+ <div id="details-content-area" class="flex-1">
53
+ <div class="flex h-full min-h-[200px] items-center justify-center px-2">
54
+ <p class="text-center text-xs text-muted-foreground">Click any element inside the canvas to inspect its properties</p>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ </div>
canvas.js ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ (function () {
2
+ const $ = (s, c) => (c || document).querySelector(s),
3
+ $$ = (s, c) => (c || document).querySelectorAll(s),
4
+ O = $('#ocr-svg-overlay'),
5
+ I = $('#ocr-svg-image'),
6
+ P = $('#canvas-placeholder-msg'),
7
+ D = $('#details-content-area'),
8
+ V = $('.canvas-viewport-container'),
9
+ W = $('.viewport-wrapper'),
10
+ Z = $('#zoom-level-text'),
11
+ btn = id => $(`#btn-zoom-${id}`);
12
+
13
+ let z = 1, px = 0, py = 0, dr = 0, dragging = false, sx = 0, sy = 0;
14
+
15
+ const upd = () => { W && (W.style.transform = `translate(${px}px,${py}px) scale(${z})`); Z && (Z.innerText = `${Math.round(z * 100)}%`); },
16
+ zoom = f => { z = Math.min(5, Math.max(.5, z * f)); upd(); },
17
+ reset = () => { z = 1; px = 0; py = 0; upd(); };
18
+
19
+ [['In', () => zoom(1.2)], ['Out', () => zoom(1 / 1.2)], ['Reset', reset]].forEach(([id, fn]) => {
20
+ const b = btn(id.toLowerCase());
21
+ b && b.addEventListener('click', e => { e.stopPropagation(); fn(); });
22
+ });
23
+
24
+ V && (I.addEventListener('dragstart', e => e.preventDefault()),
25
+ V.addEventListener('wheel', e => { e.preventDefault(); zoom(e.deltaY < 0 ? 1.15 : .85); }, { passive: !1 }),
26
+ V.addEventListener('mousedown', e => {
27
+ if (e.target.closest('button,input')) return;
28
+ dragging = !0; dr = 0; sx = e.clientX - px; sy = e.clientY - py;
29
+ V.style.cursor = 'grabbing'; e.preventDefault();
30
+ }),
31
+ window.addEventListener('mousemove', e => {
32
+ if (!dragging) return;
33
+ const dx = e.clientX - (sx + px), dy = e.clientY - (sy + py);
34
+ dr += Math.sqrt(dx * dx + dy * dy);
35
+ px = e.clientX - sx; py = e.clientY - sy; upd();
36
+ }),
37
+ window.addEventListener('mouseup', () => { dragging && (dragging = !1, V.style.cursor = 'grab'); }));
38
+
39
+ const src = () => { const c = $('#input-image'); return c && c.querySelector('img')?.src || null; },
40
+ CELL = (k, v, c = 'text-foreground') => `<span class="text-xs font-medium text-muted-foreground">${k}</span><span class="text-xs font-semibold ${c}">${v}</span>`,
41
+ EMPTY = '<div class="flex h-full min-h-[200px] items-center justify-center px-2"><p class="text-center text-xs text-muted-foreground">Click any element inside the canvas to inspect its properties</p></div>';
42
+
43
+ function populate(data) {
44
+ O.innerHTML = '';
45
+ if (!data) return;
46
+ const el = [];
47
+ ['blocks', 'paragraphs'].forEach(k => (data[k] || []).forEach(v => el.push({ t: k == 'blocks' ? 'Block' : 'Paragraph', d: v })));
48
+ (data.lines || []).forEach(l => {
49
+ el.push({ t: 'Line', d: l });
50
+ (l.words || []).forEach(w => {
51
+ el.push({ t: 'Word', d: w });
52
+ (w.symbols || []).forEach(s => el.push({ t: 'Symbol', d: s }));
53
+ w.whitespace_bounding_box && el.push({ t: 'Whitespace', d: { bounding_box: w.whitespace_bounding_box } });
54
+ });
55
+ });
56
+ const order = ['Block', 'Paragraph', 'Line', 'Word', 'Symbol', 'Whitespace'];
57
+ el.sort((a, b) => order.indexOf(a.t) - order.indexOf(b.t));
58
+ window.ocr = el;
59
+ el.forEach((e, i) => {
60
+ const b = e.d.bounding_box;
61
+ if (!b) return;
62
+ const r = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
63
+ r.setAttribute('class', `ocr-box box-${e.t.toLowerCase()}`);
64
+ r.setAttribute('x', b.x); r.setAttribute('y', b.y);
65
+ r.setAttribute('width', b.width); r.setAttribute('height', b.height);
66
+ b.angle && r.setAttribute('transform', `rotate(${b.angle},${b.x + b.width / 2},${b.y + b.height / 2})`);
67
+ r.setAttribute('data-idx', i);
68
+ O.appendChild(r);
69
+ });
70
+ }
71
+
72
+ const C = { Block: ['#f472b6', 'bg-pink-500/20 text-pink-400'], Paragraph: ['#22d3ee', 'bg-cyan-500/20 text-cyan-400'], Line: ['#c084fc', 'bg-violet-500/20 text-violet-400'], Word: ['#34d399', 'bg-emerald-500/20 text-emerald-400'], Symbol: ['#fbbf24', 'bg-amber-500/20 text-amber-400'], Whitespace: ['#f87171', 'bg-red-500/20 text-red-400'] };
73
+
74
+ function meta(item) {
75
+ const d = item.d, b = d.bounding_box, t = item.t, cl = (C[t] || ['#818cf8', 'bg-indigo-500/20 text-indigo-400'])[1];
76
+ let rows = [CELL('Type', `<span class="${cl} rounded px-2 py-0.5 text-xs font-semibold">${t}</span>`),
77
+ CELL('Confidence', d.confidence !== void 0 ? (d.confidence * 100).toFixed(1) + '%' : 'N/A', 'text-emerald-400')];
78
+ if (t == 'Line' || t == 'Word') rows.push(CELL('Language', d.language || 'N/A'), CELL('Direction', d.direction || 'N/A'), CELL('Content Type', d.content_type || 'N/A'));
79
+ if (t == 'Word') {
80
+ const f = d.foreground_rgb_hex || 'N/A', g = d.background_rgb_hex || 'N/A';
81
+ rows.push(CELL('FG Color', `<span class="inline-flex items-center gap-1.5"><span class="inline-block h-3 w-3 rounded border border-white/20" style="background:${f}"></span>${f}</span>`),
82
+ CELL('BG Color', `<span class="inline-flex items-center gap-1.5"><span class="inline-block h-3 w-3 rounded border border-white/20" style="background:${g}"></span>${g}</span>`),
83
+ CELL('Gray', `${d.foreground_gray_value} / ${d.background_gray_value}`),
84
+ CELL('Space After', d.has_space_after ? 'Yes' : 'No'));
85
+ }
86
+ rows.push(CELL('BBox', `<span class="font-mono text-[11px]">${b.x}, ${b.y}, ${b.width}×${b.height}</span>`),
87
+ CELL('Rotation', `<span class="font-mono text-[11px] text-amber-400">${(b.angle || 0).toFixed(1)}°</span>`));
88
+ D.innerHTML = `<div class="flex flex-col gap-2">${rows.map(r => `<div class="flex items-center justify-between border-b border-border pb-1.5">${r}</div>`).join('')}</div>
89
+ <div class="mt-3 flex flex-col gap-1.5"><span class="text-[11px] font-medium text-muted-foreground">Extracted text</span><div class="rounded-lg border border-primary/30 bg-primary/10 px-3 py-2 text-sm font-bold text-foreground">${d.utf8_string || 'N/A'}</div></div>`;
90
+ }
91
+
92
+ O.addEventListener('click', e => {
93
+ if (dr > 5 || !window.ocr) return;
94
+ const el = e.target.closest('.ocr-box');
95
+ if (!el) return;
96
+ $$('.ocr-box.selected').forEach(x => x.classList.remove('selected'));
97
+ el.classList.add('selected');
98
+ const item = window.ocr[+el.getAttribute('data-idx')];
99
+ item && meta(item);
100
+ });
101
+
102
+ function loadImg(src) {
103
+ reset();
104
+ if (!src) { I.removeAttribute('src'); O.innerHTML = ''; P.style.display = 'block'; D.innerHTML = EMPTY; return; }
105
+ P.style.display = 'none';
106
+ I.onload = () => O.setAttribute('viewBox', `0 0 ${I.naturalWidth || I.width} ${I.naturalHeight || I.height}`);
107
+ I.src = src;
108
+ }
109
+
110
+ window.loadStreamedData = function (raw) {
111
+ if (!raw || !raw.trim()) { loadImg(null); I.style.filter = 'none'; return; }
112
+ try {
113
+ const p = JSON.parse(raw);
114
+ I.style.filter = 'none'; P.style.display = 'none'; D.innerHTML = EMPTY;
115
+ populate(p.annotation);
116
+ loadImg(src());
117
+ } catch (e) { console.error(e); }
118
+ };
119
+
120
+ (function init() {
121
+ const el = $('#input-image');
122
+ if (!el) { setTimeout(init, 150); return; }
123
+ new MutationObserver(() => {
124
+ const s = src();
125
+ if (s && s !== I.src) { window.ocr = null; loadImg(s); I.style.filter = 'blur(8px)'; }
126
+ else if (!s) { loadImg(null); I.style.filter = 'none'; }
127
+ }).observe(el, { childList: true, subtree: true, attributes: true });
128
+ const s = src();
129
+ s && loadImg(s);
130
+ })();
131
+
132
+ $$('.canvas-toggles input').forEach(el => {
133
+ const fn = () => O.classList.toggle('show-' + el.id.replace('cb-', ''), el.checked);
134
+ el.addEventListener('change', fn); fn();
135
+ });
136
+ })();
custom.css ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg: 222.2 84% 4.9%;
3
+ --fg: 210 40% 98%;
4
+ --bd: 217.2 32.6% 17.5%;
5
+ --ri: 224.3 76.3% 48%;
6
+ --rd: .5rem;
7
+ --pr: 217.2 91.2% 59.8%;
8
+ --mu: 217.2 32.6% 17.5%;
9
+ --mu-fg: 215 20.2% 65.1%;
10
+ --ac: 217.2 32.6% 17.5%;
11
+ --ac-fg: 210 40% 98%;
12
+ --ca: 222.2 84% 4.9%;
13
+ --ca-fg: 210 40% 98%
14
+ }
15
+
16
+ body,
17
+ .gradio-container {
18
+ font-family: 'Outfit', sans-serif !important;
19
+ background: hsl(var(--bg)) !important;
20
+ color: hsl(var(--fg)) !important
21
+ }
22
+
23
+ .gradio-container {
24
+ background: radial-gradient(circle at top right, rgba(99, 102, 241, .08), transparent 40%), radial-gradient(circle at bottom left, rgba(236, 72, 153, .05), transparent 40%), hsl(var(--bg)) !important
25
+ }
26
+
27
+ .layout-tree {
28
+ max-height: 550px;
29
+ overflow-y: auto
30
+ }
31
+
32
+ .tree-summary::before {
33
+ content: '\25B6';
34
+ display: inline-block;
35
+ margin-right: 8px;
36
+ transition: transform .2s;
37
+ font-size: .75rem;
38
+ color: hsl(var(--mu-fg))
39
+ }
40
+
41
+ .tree-item[open]>.tree-summary::before {
42
+ transform: rotate(90deg);
43
+ color: hsl(var(--ri))
44
+ }
45
+
46
+ .tree-summary::-webkit-details-marker {
47
+ display: none
48
+ }
49
+
50
+ .ocr-box {
51
+ fill: transparent;
52
+ cursor: pointer;
53
+ transition: all .15s;
54
+ pointer-events: auto;
55
+ stroke-linejoin: round
56
+ }
57
+
58
+ .ocr-box:hover {
59
+ fill: rgba(255, 255, 255, .08);
60
+ stroke-width: 2.5px !important
61
+ }
62
+
63
+ .ocr-box.selected {
64
+ fill: rgba(251, 191, 36, .15) !important;
65
+ stroke: #fbbf24 !important;
66
+ stroke-width: 3px !important;
67
+ filter: drop-shadow(0 0 3px rgba(251, 191, 36, .5))
68
+ }
69
+
70
+ .box-block {
71
+ stroke: rgba(244, 114, 182, .75);
72
+ stroke-width: 2.2px
73
+ }
74
+
75
+ .box-paragraph {
76
+ stroke: rgba(34, 211, 238, .75);
77
+ stroke-width: 1.6px
78
+ }
79
+
80
+ .box-line {
81
+ stroke: rgba(139, 92, 246, .75);
82
+ stroke-width: 1.3px
83
+ }
84
+
85
+ .box-word {
86
+ stroke: rgba(16, 185, 129, .75);
87
+ stroke-width: 1px
88
+ }
89
+
90
+ .box-symbol {
91
+ stroke: rgba(245, 158, 11, .75);
92
+ stroke-width: .8px
93
+ }
94
+
95
+ .box-whitespace {
96
+ stroke: rgba(239, 68, 68, .65);
97
+ stroke-width: .8px
98
+ }
99
+
100
+ .box-block,
101
+ .box-paragraph,
102
+ .box-line,
103
+ .box-word,
104
+ .box-symbol,
105
+ .box-whitespace {
106
+ display: none
107
+ }
108
+
109
+ .show-blocks .box-block,
110
+ .show-paragraphs .box-paragraph,
111
+ .show-lines .box-line,
112
+ .show-words .box-word,
113
+ .show-symbols .box-symbol,
114
+ .show-whitespace .box-whitespace {
115
+ display: block
116
+ }
gocr-0.1.0-py2.py3-none-any.whl ADDED
Binary file (18.9 kB). View file
 
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ https://huggingface.co/spaces/shethjenil/Google-OCR/resolve/main/gocr-0.1.0-py2.py3-none-any.whl
templates/empty_state.html ADDED
@@ -0,0 +1 @@
 
 
1
+ <div class="py-6 text-center text-sm text-gray-500">{message}</div>
templates/error_msg.html ADDED
@@ -0,0 +1 @@
 
 
1
+ <div class="rounded-lg border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-400">{message}</div>
templates/header.html ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ <div class="relative mb-6 overflow-hidden rounded-2xl border border-white/10 bg-gradient-to-br from-slate-800/70 to-slate-900/80 p-6 text-center shadow-2xl backdrop-blur-md">
2
+ <div class="absolute inset-x-0 top-0 h-1 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500"></div>
3
+ <h1 class="bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400 bg-clip-text text-4xl font-extrabold tracking-tight text-transparent">Google Chrome Screen AI</h1>
4
+ <p class="mt-1 text-base font-light text-gray-400">Ultra-Precision Local OCR &amp; Layout Engine Diagnostic Dashboard</p>
5
+ </div>
templates/stats_grid.html ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
2
+ <div class="rounded-xl border border-white/10 bg-gradient-to-br from-emerald-900/20 to-emerald-950/10 p-3 text-center shadow-md transition-all duration-300 hover:-translate-y-1 hover:border-emerald-500/60">
3
+ <div class="mb-0.5 text-xl font-bold text-white">{avg_conf}</div>
4
+ <div class="text-[11px] font-semibold uppercase tracking-widest text-gray-400">Avg Confidence</div>
5
+ </div>
6
+ <div class="rounded-xl border border-white/10 bg-gradient-to-br from-amber-900/20 to-amber-950/10 p-3 text-center shadow-md transition-all duration-300 hover:-translate-y-1 hover:border-amber-500/60">
7
+ <div class="mb-0.5 text-xl font-bold text-amber-400">{latency}</div>
8
+ <div class="text-[11px] font-semibold uppercase tracking-widest text-gray-400">Latency</div>
9
+ </div>
10
+ <div class="rounded-xl border border-white/10 bg-slate-900/60 p-3 text-center shadow-md transition-all duration-300 hover:-translate-y-1 hover:border-indigo-500/40">
11
+ <div class="mb-0.5 text-xl font-bold text-white">{blocks}</div>
12
+ <div class="text-[11px] font-semibold uppercase tracking-widest text-gray-400">Blocks</div>
13
+ </div>
14
+ <div class="rounded-xl border border-white/10 bg-slate-900/60 p-3 text-center shadow-md transition-all duration-300 hover:-translate-y-1 hover:border-indigo-500/40">
15
+ <div class="mb-0.5 text-xl font-bold text-white">{paragraphs}</div>
16
+ <div class="text-[11px] font-semibold uppercase tracking-widest text-gray-400">Paragraphs</div>
17
+ </div>
18
+ <div class="rounded-xl border border-white/10 bg-slate-900/60 p-3 text-center shadow-md transition-all duration-300 hover:-translate-y-1 hover:border-indigo-500/40">
19
+ <div class="mb-0.5 text-xl font-bold text-white">{lines}</div>
20
+ <div class="text-[11px] font-semibold uppercase tracking-widest text-gray-400">Lines</div>
21
+ </div>
22
+ <div class="rounded-xl border border-white/10 bg-slate-900/60 p-3 text-center shadow-md transition-all duration-300 hover:-translate-y-1 hover:border-indigo-500/40">
23
+ <div class="mb-0.5 text-xl font-bold text-white">{words}</div>
24
+ <div class="text-[11px] font-semibold uppercase tracking-widest text-gray-400">Words</div>
25
+ </div>
26
+ <div class="rounded-xl border border-white/10 bg-slate-900/60 p-3 text-center shadow-md transition-all duration-300 hover:-translate-y-1 hover:border-indigo-500/40">
27
+ <div class="mb-0.5 text-xl font-bold text-white">{symbols}</div>
28
+ <div class="text-[11px] font-semibold uppercase tracking-widest text-gray-400">Symbols</div>
29
+ </div>
30
+ <div class="rounded-xl border border-white/10 bg-slate-900/60 p-3 text-center shadow-md transition-all duration-300 hover:-translate-y-1 hover:border-indigo-500/40">
31
+ <div class="mb-0.5 text-lg font-bold text-indigo-400">{lang}</div>
32
+ <div class="text-[11px] font-semibold uppercase tracking-widest text-gray-400">Lang</div>
33
+ </div>
34
+ </div>
templates/tree_block.html ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ <details class="tree-item ml-3 mt-1.5 border-l-2 border-dashed border-white/10 pl-3 [&[open]]:border-l-indigo-500/40">
2
+ <summary class="tree-summary cursor-pointer select-none rounded-md px-3 py-1.5 text-sm font-semibold text-pink-400 transition-all hover:bg-white/5">{header}</summary>
3
+ <div class="py-1">{content}</div>
4
+ </details>
templates/tree_line.html ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ <details class="tree-item ml-3 mt-1.5 border-l-2 border-dashed border-white/10 pl-3 [&[open]]:border-l-indigo-500/40">
2
+ <summary class="tree-summary cursor-pointer select-none rounded-md px-3 py-1.5 text-sm font-medium text-violet-400 transition-all hover:bg-white/5">{header}</summary>
3
+ <div class="flex flex-wrap gap-2 px-4 py-2">{content}</div>
4
+ </details>
templates/tree_paragraph.html ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ <details class="tree-item ml-3 mt-1.5 border-l-2 border-dashed border-white/10 pl-3 [&[open]]:border-l-indigo-500/40">
2
+ <summary class="tree-summary cursor-pointer select-none rounded-md px-3 py-1.5 text-sm font-semibold text-cyan-400 transition-all hover:bg-white/5">{header}</summary>
3
+ <div class="py-1">{content}</div>
4
+ </details>
templates/tree_word_badge.html ADDED
@@ -0,0 +1 @@
 
 
1
+ <span class="inline-block cursor-help rounded-md border border-emerald-500/40 bg-emerald-500/10 px-2.5 py-1 text-sm text-white transition-all hover:scale-105 hover:border-emerald-500/80 hover:shadow-lg hover:shadow-emerald-500/20" title="{tooltip}"><strong>{text}</strong>{symbols_badge}</span>