Ankitkumar8987 commited on
Commit
1cd2b94
·
verified ·
1 Parent(s): 241f7ec

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +702 -0
app.py ADDED
@@ -0,0 +1,702 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import re
4
+ import tempfile
5
+ import pandas as pd
6
+ import fitz # PyMuPDF
7
+ import torch
8
+ import openpyxl
9
+ from PIL import Image, ImageDraw
10
+ from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
11
+ import gradio as gr
12
+
13
+ # Auto-detect device and precision
14
+ device = "cuda" if torch.cuda.is_available() else "cpu"
15
+ if device == "cuda":
16
+ dtype = torch.bfloat16
17
+ else:
18
+ dtype = torch.float32
19
+
20
+ print(f"Using device: {device}, dtype: {dtype}")
21
+
22
+ # Global model caches
23
+ layout_processor = None
24
+ layout_model = None
25
+ tatr_processor = None
26
+ tatr_model = None
27
+ glm_processor = None
28
+ glm_model = None
29
+
30
+ def load_layout_model():
31
+ global layout_processor, layout_model
32
+ if layout_model is None:
33
+ print("Loading PP-DocLayoutV3...")
34
+ from transformers import AutoImageProcessor, AutoModelForObjectDetection
35
+ model_path = "PaddlePaddle/PP-DocLayoutV3_safetensors"
36
+ layout_processor = AutoImageProcessor.from_pretrained(model_path, trust_remote_code=True)
37
+ layout_model = AutoModelForObjectDetection.from_pretrained(
38
+ model_path,
39
+ trust_remote_code=True
40
+ ).to(device)
41
+ return layout_processor, layout_model
42
+
43
+ def load_tatr_model():
44
+ global tatr_processor, tatr_model
45
+ if tatr_model is None:
46
+ print("Loading Table Transformer (TATR) Structure...")
47
+ from transformers import DetrImageProcessor, TableTransformerForObjectDetection
48
+ model_path = "microsoft/table-transformer-structure-recognition"
49
+ tatr_processor = DetrImageProcessor.from_pretrained(model_path)
50
+ tatr_model = TableTransformerForObjectDetection.from_pretrained(model_path).to(device)
51
+ return tatr_processor, tatr_model
52
+
53
+ def load_glm_model():
54
+ global glm_processor, glm_model
55
+ if glm_model is None:
56
+ print("Loading GLM-OCR...")
57
+ from transformers import AutoProcessor
58
+ try:
59
+ from transformers import GlmOcrForConditionalGeneration
60
+ except ImportError:
61
+ from transformers import AutoModelForCausalLM as GlmOcrForConditionalGeneration
62
+
63
+ model_path = "zai-org/GLM-OCR"
64
+ glm_processor = AutoProcessor.from_pretrained(model_path, trust_remote_code=True)
65
+
66
+ # Load model with correct dtype and device map
67
+ if device == "cuda":
68
+ glm_model = GlmOcrForConditionalGeneration.from_pretrained(
69
+ model_path,
70
+ trust_remote_code=True,
71
+ torch_dtype=dtype,
72
+ device_map="auto"
73
+ )
74
+ else:
75
+ glm_model = GlmOcrForConditionalGeneration.from_pretrained(
76
+ model_path,
77
+ trust_remote_code=True,
78
+ torch_dtype=dtype
79
+ ).to(device)
80
+
81
+ return glm_processor, glm_model
82
+
83
+ # -------------------------------------------------------------
84
+ # Core Pipeline Functions
85
+ # -------------------------------------------------------------
86
+
87
+ def convert_pdf_to_images(pdf_path, dpi=300):
88
+ """Convert PDF pages to PIL Images at 300 DPI."""
89
+ doc = fitz.open(pdf_path)
90
+ images = []
91
+ for page_num in range(len(doc)):
92
+ page = doc.load_page(page_num)
93
+ zoom = dpi / 72.0
94
+ matrix = fitz.Matrix(zoom, zoom)
95
+ pix = page.get_pixmap(matrix=matrix)
96
+ img_data = pix.tobytes("png")
97
+ img = Image.open(io.BytesIO(img_data)).convert("RGB")
98
+ images.append(img)
99
+ return images
100
+
101
+ def detect_tables(image, confidence_threshold=0.4):
102
+ """Detect tables using PP-DocLayoutV3."""
103
+ processor, model = load_layout_model()
104
+ inputs = processor(images=image, return_tensors="pt").to(device)
105
+
106
+ with torch.no_grad():
107
+ outputs = model(**inputs)
108
+
109
+ target_sizes = [image.size[::-1]]
110
+ results = processor.post_process_object_detection(
111
+ outputs,
112
+ target_sizes=target_sizes,
113
+ threshold=confidence_threshold
114
+ )[0]
115
+
116
+ id2label = model.config.id2label
117
+ table_label_ids = [k for k, v in id2label.items() if "table" in v.lower()]
118
+
119
+ tables = []
120
+ for score, label_id, box in zip(results["scores"], results["labels"], results["boxes"]):
121
+ if label_id.item() in table_label_ids:
122
+ box = [int(coord) for coord in box.tolist()] # [xmin, ymin, xmax, ymax]
123
+ tables.append({
124
+ "box": box,
125
+ "score": score.item()
126
+ })
127
+
128
+ # Sort tables top to bottom by ymin
129
+ tables = sorted(tables, key=lambda x: x["box"][1])
130
+ return tables
131
+
132
+ def extract_table_structure(table_img, confidence_threshold=0.8):
133
+ """Extract rows, columns, and spanning cells using Table Transformer."""
134
+ processor, model = load_tatr_model()
135
+ inputs = processor(images=table_img, return_tensors="pt").to(device)
136
+
137
+ with torch.no_grad():
138
+ outputs = model(**inputs)
139
+
140
+ target_sizes = [table_img.size[::-1]]
141
+ results = processor.post_process_object_detection(
142
+ outputs,
143
+ target_sizes=target_sizes,
144
+ threshold=confidence_threshold
145
+ )[0]
146
+
147
+ rows = []
148
+ columns = []
149
+ spanning_cells = []
150
+
151
+ id2label = model.config.id2label
152
+ for score, label_id, box in zip(results["scores"], results["labels"], results["boxes"]):
153
+ label = id2label[label_id.item()]
154
+ box = [int(coord) for coord in box.tolist()]
155
+ if label == "table row":
156
+ rows.append(box)
157
+ elif label == "table column":
158
+ columns.append(box)
159
+ elif label == "table spanning cell":
160
+ spanning_cells.append(box)
161
+
162
+ # Sort rows by ymin (top to bottom)
163
+ rows = sorted(rows, key=lambda x: x[1])
164
+ # Sort columns by xmin (left to right)
165
+ columns = sorted(columns, key=lambda x: x[0])
166
+
167
+ # Fallback to single cell if structure detection fails
168
+ if not rows:
169
+ rows = [[0, 0, table_img.width, table_img.height]]
170
+ if not columns:
171
+ columns = [[0, 0, table_img.width, table_img.height]]
172
+
173
+ return rows, columns, spanning_cells
174
+
175
+ def run_glm_ocr(image, prompt="Text Recognition:"):
176
+ """Run GLM-OCR on an image crop."""
177
+ processor, model = load_glm_model()
178
+
179
+ temp_dir = tempfile.gettempdir()
180
+ temp_file_path = os.path.join(temp_dir, f"temp_cell_{os.getpid()}_{id(image)}.png")
181
+ image.save(temp_file_path)
182
+
183
+ messages = [
184
+ {
185
+ "role": "user",
186
+ "content": [
187
+ {"type": "image", "url": temp_file_path},
188
+ {"type": "text", "text": prompt},
189
+ ],
190
+ }
191
+ ]
192
+
193
+ try:
194
+ if hasattr(processor, "apply_chat_template"):
195
+ inputs = processor.apply_chat_template(
196
+ messages,
197
+ tokenize=True,
198
+ add_generation_prompt=True,
199
+ return_dict=True,
200
+ return_tensors="pt"
201
+ ).to(device)
202
+ else:
203
+ inputs = processor(images=image, text=prompt, return_tensors="pt").to(device)
204
+
205
+ if "pixel_values" in inputs:
206
+ inputs["pixel_values"] = inputs["pixel_values"].to(dtype)
207
+
208
+ with torch.no_grad():
209
+ output = model.generate(**inputs, max_new_tokens=512)
210
+
211
+ text = processor.decode(output[0], skip_special_tokens=True)
212
+
213
+ # Clean up prompts or prefixes from visual outputs
214
+ if prompt in text:
215
+ text = text.split(prompt)[-1]
216
+ elif "assistant" in text.lower():
217
+ text = text.split("assistant")[-1]
218
+
219
+ return text.strip()
220
+ except Exception as e:
221
+ print(f"GLM-OCR error: {e}")
222
+ return ""
223
+ finally:
224
+ if os.path.exists(temp_file_path):
225
+ os.remove(temp_file_path)
226
+
227
+ # -------------------------------------------------------------
228
+ # Table Reconstruction & Layout Helpers
229
+ # -------------------------------------------------------------
230
+
231
+ def get_intersection_area(box1, box2):
232
+ x_left = max(box1[0], box2[0])
233
+ y_top = max(box1[1], box2[1])
234
+ x_right = min(box1[2], box2[2])
235
+ y_bottom = min(box1[3], box2[3])
236
+
237
+ if x_right < x_left or y_bottom < y_top:
238
+ return 0.0
239
+ return (x_right - x_left) * (y_bottom - y_top)
240
+
241
+ def get_area(box):
242
+ return (box[2] - box[0]) * (box[3] - box[1])
243
+
244
+ def build_grid_and_ocr(table_img, rows, columns, spanning_cells, progress_callback=None):
245
+ """Run OCR on cells and build cell-to-span mappings for openpyxl merges."""
246
+ num_rows = len(rows)
247
+ num_cols = len(columns)
248
+
249
+ data = [["" for _ in range(num_cols)] for _ in range(num_rows)]
250
+ grid_spanning_map = {}
251
+
252
+ # 1. Map spanning cells to grid cells
253
+ for s_idx, s_box in enumerate(spanning_cells):
254
+ covered_cells = []
255
+ for r_idx, row in enumerate(rows):
256
+ for c_idx, col in enumerate(columns):
257
+ g_box = [col[0], row[1], col[2], row[3]]
258
+ g_area = get_area(g_box)
259
+ if g_area <= 0:
260
+ continue
261
+ inter = get_intersection_area(g_box, s_box)
262
+ if inter / g_area > 0.5:
263
+ covered_cells.append((r_idx, c_idx))
264
+ if covered_cells:
265
+ rs = [c[0] for c in covered_cells]
266
+ cs = [c[1] for c in covered_cells]
267
+ r_min, r_max = min(rs), max(rs)
268
+ c_min, c_max = min(cs), max(cs)
269
+ for r in range(r_min, r_max + 1):
270
+ for c in range(c_min, c_max + 1):
271
+ grid_spanning_map[(r, c)] = {
272
+ "span_id": s_idx,
273
+ "r_min": r_min,
274
+ "r_max": r_max,
275
+ "c_min": c_min,
276
+ "c_max": c_max,
277
+ "box": s_box
278
+ }
279
+
280
+ # 2. Iterate grid and perform OCR
281
+ total_cells = num_rows * num_cols
282
+ cell_counter = 0
283
+
284
+ for r_idx, row in enumerate(rows):
285
+ r_ymin, r_ymax = row[1], row[3]
286
+ for c_idx, col in enumerate(columns):
287
+ c_xmin, c_xmax = col[0], col[2]
288
+ cell_counter += 1
289
+
290
+ if progress_callback:
291
+ progress_callback(cell_counter / total_cells, f"OCR on Cell {cell_counter}/{total_cells} (Row {r_idx+1}, Col {c_idx+1})")
292
+
293
+ # If part of a spanning cell, check if we are the top-left cell
294
+ if (r_idx, c_idx) in grid_spanning_map:
295
+ span = grid_spanning_map[(r_idx, c_idx)]
296
+ if r_idx == span["r_min"] and c_idx == span["c_min"]:
297
+ # Crop spanning box instead of grid cell
298
+ s_box = span["box"]
299
+ xmin = max(0, int(s_box[0]))
300
+ ymin = max(0, int(s_box[1]))
301
+ xmax = min(table_img.width, int(s_box[2]))
302
+ ymax = min(table_img.height, int(s_box[3]))
303
+
304
+ cell_crop = table_img.crop((xmin, ymin, xmax, ymax))
305
+ cell_text = run_glm_ocr(cell_crop, prompt="Text Recognition:")
306
+ data[r_idx][c_idx] = cell_text
307
+ else:
308
+ # Skip OCR for other merged cells (Excel merge will use top-left cell value)
309
+ data[r_idx][c_idx] = ""
310
+ else:
311
+ # Ordinary cell crop
312
+ xmin = max(0, int(c_xmin))
313
+ ymin = max(0, int(r_ymin))
314
+ xmax = min(table_img.width, int(c_xmax))
315
+ ymax = min(table_img.height, int(r_ymax))
316
+
317
+ if xmax > xmin and ymax > ymin:
318
+ cell_crop = table_img.crop((xmin, ymin, xmax, ymax))
319
+ cell_text = run_glm_ocr(cell_crop, prompt="Text Recognition:")
320
+ data[r_idx][c_idx] = cell_text
321
+ else:
322
+ data[r_idx][c_idx] = ""
323
+
324
+ df = pd.DataFrame(data)
325
+ return df, grid_spanning_map
326
+
327
+ def parse_markdown_table(md_text):
328
+ """Parse Markdown table outputted by GLM-OCR Table Recognition into DataFrame."""
329
+ lines = [line.strip() for line in md_text.split("\n") if line.strip()]
330
+ table_lines = []
331
+ in_table = False
332
+
333
+ for line in lines:
334
+ if line.startswith("|"):
335
+ in_table = True
336
+ table_lines.append(line)
337
+ elif in_table:
338
+ break
339
+
340
+ if not table_lines:
341
+ return pd.DataFrame([[md_text]])
342
+
343
+ data = []
344
+ for line in table_lines:
345
+ parts = [p.strip() for p in line.split("|")]
346
+ if parts and parts[0] == "":
347
+ parts = parts[1:]
348
+ if parts and parts[-1] == "":
349
+ parts = parts[:-1]
350
+ data.append(parts)
351
+
352
+ if not data:
353
+ return pd.DataFrame([[md_text]])
354
+
355
+ # Check for header separator line
356
+ if len(data) > 1 and all(re.match(r"^[-:\s|]+$", cell) for cell in data[1]):
357
+ header = data[0]
358
+ rows = data[2:]
359
+ else:
360
+ header = None
361
+ rows = data
362
+
363
+ if header:
364
+ # Check alignment length matching
365
+ if len(rows) > 0 and len(rows[0]) != len(header):
366
+ return pd.DataFrame(rows)
367
+ return pd.DataFrame(rows, columns=header)
368
+ else:
369
+ return pd.DataFrame(rows)
370
+
371
+ # -------------------------------------------------------------
372
+ # Excel Writing & Styling
373
+ # -------------------------------------------------------------
374
+
375
+ def generate_excel_workbook(sheets_data, output_path):
376
+ """Write parsed DataFrames with styling and merges to Excel."""
377
+ wb = openpyxl.Workbook()
378
+ wb.remove(wb.active) # Remove default sheet
379
+
380
+ header_font = Font(name="Calibri", size=11, bold=True, color="FFFFFF")
381
+ header_fill = PatternFill(start_color="366092", end_color="366092", fill_type="solid") # Dark steel blue
382
+ thin_border = Border(
383
+ left=Side(style='thin', color='D9D9D9'),
384
+ right=Side(style='thin', color='D9D9D9'),
385
+ top=Side(style='thin', color='D9D9D9'),
386
+ bottom=Side(style='thin', color='D9D9D9')
387
+ )
388
+
389
+ for sheet_name, table_dict in sheets_data.items():
390
+ df = table_dict["df"]
391
+ grid_spanning_map = table_dict.get("grid_spanning_map", {})
392
+
393
+ ws = wb.create_sheet(title=sheet_name)
394
+ ws.views.sheetView[0].showGridLines = True
395
+
396
+ # Write values
397
+ for r_idx in range(len(df)):
398
+ for c_idx in range(len(df.columns)):
399
+ val = df.iloc[r_idx, c_idx]
400
+ ws.cell(row=r_idx + 1, column=c_idx + 1, value=str(val))
401
+
402
+ # Apply premium styling
403
+ num_rows, num_cols = df.shape
404
+ for r in range(1, num_rows + 1):
405
+ for c in range(1, num_cols + 1):
406
+ cell = ws.cell(row=r, column=c)
407
+ cell.border = thin_border
408
+
409
+ # Check header styling (row 1)
410
+ if r == 1:
411
+ cell.font = header_font
412
+ cell.fill = header_fill
413
+ cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
414
+ else:
415
+ cell.font = Font(name="Calibri", size=11)
416
+ cell.alignment = Alignment(horizontal="left", vertical="center", wrap_text=True)
417
+
418
+ # Apply merged ranges
419
+ merged_ranges = set()
420
+ for (r, c), span in grid_spanning_map.items():
421
+ r_min, r_max = span["r_min"], span["r_max"]
422
+ c_min, c_max = span["c_min"], span["c_max"]
423
+ if r_max > r_min or c_max > c_min:
424
+ merged_ranges.add((r_min + 1, c_min + 1, r_max + 1, c_max + 1))
425
+
426
+ for merge_range in merged_ranges:
427
+ ws.merge_cells(
428
+ start_row=merge_range[0],
429
+ start_column=merge_range[1],
430
+ end_row=merge_range[2],
431
+ end_column=merge_range[3]
432
+ )
433
+ # Center alignment on merged cells
434
+ top_left_cell = ws.cell(row=merge_range[0], column=merge_range[1])
435
+ top_left_cell.alignment = Alignment(horizontal="center", vertical="center", wrap_text=True)
436
+
437
+ # Auto-adjust column widths
438
+ for col in ws.columns:
439
+ max_len = 0
440
+ col_letter = openpyxl.utils.get_column_letter(col[0].column)
441
+ for cell in col:
442
+ if cell.value:
443
+ max_len = max(max_len, len(str(cell.value)))
444
+ ws.column_dimensions[col_letter].width = min(max(max_len + 3, 10), 55)
445
+
446
+ wb.save(output_path)
447
+
448
+ # -------------------------------------------------------------
449
+ # Visual Drawing Helpers
450
+ # -------------------------------------------------------------
451
+
452
+ def draw_table_boxes(image, tables):
453
+ """Draw bounding boxes of detected tables on the page image."""
454
+ img_copy = image.copy()
455
+ draw = ImageDraw.Draw(img_copy)
456
+ for idx, t in enumerate(tables):
457
+ box = t["box"]
458
+ draw.rectangle(box, outline="#FF5722", width=5) # Orange box
459
+ # Draw table label index
460
+ draw.rectangle([box[0], box[1] - 30, box[0] + 120, box[1]], fill="#FF5722")
461
+ draw.text((box[0] + 10, box[1] - 25), f"Table {idx+1}", fill="white")
462
+ return img_copy
463
+
464
+ def draw_structure_grid(table_img, rows, columns):
465
+ """Draw rows and columns grid overlay."""
466
+ img_copy = table_img.copy()
467
+ draw = ImageDraw.Draw(img_copy)
468
+
469
+ # Draw horizontal red lines for rows
470
+ for idx, r in enumerate(rows):
471
+ draw.line((0, r[1], table_img.width, r[1]), fill="#E91E63", width=3) # Pink
472
+ draw.line((0, r[3], table_img.width, r[3]), fill="#E91E63", width=3)
473
+ draw.text((10, r[1] + 5), f"R{idx+1}", fill="#E91E63")
474
+
475
+ # Draw vertical blue lines for columns
476
+ for idx, c in enumerate(columns):
477
+ draw.line((c[0], 0, c[0], table_img.height), fill="#2196F3", width=3) # Blue
478
+ draw.line((c[2], 0, c[2], table_img.height), fill="#2196F3", width=3)
479
+ draw.text((c[0] + 5, 10), f"C{idx+1}", fill="#2196F3")
480
+
481
+ return img_copy
482
+
483
+ # -------------------------------------------------------------
484
+ # Gradio Orchestrator
485
+ # -------------------------------------------------------------
486
+
487
+ def process_pdf(pdf_file, mode, doc_layout_threshold, tatr_threshold, progress=gr.Progress()):
488
+ if not pdf_file:
489
+ return None, [], []
490
+
491
+ progress(0.05, "Converting PDF to 300 DPI Images...")
492
+ pages = convert_pdf_to_images(pdf_file.name)
493
+
494
+ progress(0.15, "Loading Layout Detection Model...")
495
+ load_layout_model()
496
+
497
+ sheets_data = {}
498
+ visual_pages = []
499
+ visual_grids = []
500
+
501
+ table_global_counter = 1
502
+
503
+ for page_idx, page_img in enumerate(pages):
504
+ progress(0.20 + (page_idx / len(pages)) * 0.10, f"Analyzing page {page_idx+1} layout...")
505
+ tables = detect_tables(page_img, confidence_threshold=doc_layout_threshold)
506
+
507
+ if not tables:
508
+ visual_pages.append(page_img)
509
+ continue
510
+
511
+ page_with_boxes = draw_table_boxes(page_img, tables)
512
+ visual_pages.append(page_with_boxes)
513
+
514
+ for t_idx, table in enumerate(tables):
515
+ box = table["box"]
516
+ # Crop table image
517
+ xmin, ymin, xmax, ymax = box
518
+ # Clip bounds
519
+ xmin = max(0, xmin)
520
+ ymin = max(0, ymin)
521
+ xmax = min(page_img.width, xmax)
522
+ ymax = min(page_img.height, ymax)
523
+
524
+ table_crop = page_img.crop((xmin, ymin, xmax, ymax))
525
+ sheet_name = f"Page{page_idx+1}_Table{t_idx+1}"
526
+
527
+ if mode == "Hybrid Grid Mode (PP-DocLayoutV3 + TATR + GLM-OCR)":
528
+ progress(0.35, f"Detecting structural grid for {sheet_name}...")
529
+ rows, columns, spanning = extract_table_structure(table_crop, confidence_threshold=tatr_threshold)
530
+
531
+ grid_img = draw_structure_grid(table_crop, rows, columns)
532
+ visual_grids.append((grid_img, f"{sheet_name} Structure Grid"))
533
+
534
+ progress(0.40, f"Loading GLM-OCR model...")
535
+ load_glm_model()
536
+
537
+ # Setup custom progress callback for cell OCRs
538
+ def progress_cb(frac, desc):
539
+ progress(0.45 + frac * 0.50, desc)
540
+
541
+ df, grid_spanning_map = build_grid_and_ocr(
542
+ table_crop,
543
+ rows,
544
+ columns,
545
+ spanning,
546
+ progress_callback=progress_cb
547
+ )
548
+
549
+ sheets_data[sheet_name] = {
550
+ "df": df,
551
+ "grid_spanning_map": grid_spanning_map
552
+ }
553
+ else:
554
+ # End-to-End Table Mode
555
+ progress(0.40, f"Loading GLM-OCR model...")
556
+ load_glm_model()
557
+
558
+ progress(0.50, f"Running GLM-OCR Table Recognition on {sheet_name}...")
559
+ md_text = run_glm_ocr(table_crop, prompt="Table Recognition:")
560
+ df = parse_markdown_table(md_text)
561
+
562
+ # Crop visualization
563
+ visual_grids.append((table_crop, f"{sheet_name} Crop"))
564
+
565
+ sheets_data[sheet_name] = {
566
+ "df": df,
567
+ "grid_spanning_map": {}
568
+ }
569
+
570
+ table_global_counter += 1
571
+
572
+ if not sheets_data:
573
+ # If no tables found, create a placeholder sheet
574
+ sheets_data["No Tables Found"] = {
575
+ "df": pd.DataFrame([["No tables were detected in the uploaded PDF."]]),
576
+ "grid_spanning_map": {}
577
+ }
578
+
579
+ progress(0.95, "Generating styled Excel workbook...")
580
+ temp_dir = tempfile.gettempdir()
581
+ excel_path = os.path.join(temp_dir, f"extracted_tables_{os.getpid()}.xlsx")
582
+ generate_excel_workbook(sheets_data, excel_path)
583
+
584
+ progress(1.0, "Complete!")
585
+
586
+ # Format dataframes list for preview tab
587
+ df_previews = []
588
+ for sheet_name, table_dict in sheets_data.items():
589
+ df_previews.append(gr.DataFrame(value=table_dict["df"], label=sheet_name, interactive=False))
590
+
591
+ return excel_path, visual_pages, visual_grids
592
+
593
+ # -------------------------------------------------------------
594
+ # Gradio Interface Custom CSS & Styling
595
+ # -------------------------------------------------------------
596
+
597
+ custom_css = """
598
+ body {
599
+ background-color: #0b0f19;
600
+ }
601
+ .gradio-container {
602
+ font-family: 'Outfit', sans-serif !important;
603
+ }
604
+ .header-box {
605
+ text-align: center;
606
+ background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
607
+ padding: 2.5rem;
608
+ border-radius: 12px;
609
+ margin-bottom: 2rem;
610
+ box-shadow: 0 4px 20px rgba(0,0,0,0.4);
611
+ color: white;
612
+ }
613
+ .header-box h1 {
614
+ font-size: 2.5rem;
615
+ font-weight: 800;
616
+ margin-bottom: 0.5rem;
617
+ letter-spacing: -1px;
618
+ }
619
+ .header-box p {
620
+ font-size: 1.1rem;
621
+ opacity: 0.9;
622
+ }
623
+ .primary-btn {
624
+ background: linear-gradient(135deg, #FF5722 0%, #E91E63 100%) !important;
625
+ border: none !important;
626
+ color: white !important;
627
+ font-weight: bold !important;
628
+ }
629
+ .secondary-btn {
630
+ background-color: #1e293b !important;
631
+ border: 1px solid #334155 !important;
632
+ color: white !important;
633
+ }
634
+ """
635
+
636
+ with gr.Blocks() as demo:
637
+ with gr.Column(elem_classes="header-box"):
638
+ gr.Markdown("# 📊 Multi-Model PDF to Excel Table Parser")
639
+ gr.Markdown(
640
+ "An enterprise-grade document extraction pipeline leveraging layout segmenters (PP-DocLayoutV3), "
641
+ "grid recognizers (Table Transformer), and state-of-the-art vision-language transcriptions (GLM-OCR)."
642
+ )
643
+
644
+ with gr.Row():
645
+ with gr.Column(scale=2):
646
+ pdf_input = gr.File(label="Upload PDF Document", file_types=[".pdf"])
647
+
648
+ with gr.Accordion("⚙️ Pipeline Configurations", open=True):
649
+ mode = gr.Dropdown(
650
+ choices=[
651
+ "Hybrid Grid Mode (PP-DocLayoutV3 + TATR + GLM-OCR)",
652
+ "End-to-End Table Mode (PP-DocLayoutV3 + GLM-OCR Table Recognition)"
653
+ ],
654
+ value="Hybrid Grid Mode (PP-DocLayoutV3 + TATR + GLM-OCR)",
655
+ label="Pipeline Processing Mode",
656
+ info="Hybrid uses coordinates and merges; End-to-End transcribes directly."
657
+ )
658
+
659
+ doc_layout_threshold = gr.Slider(
660
+ minimum=0.1,
661
+ maximum=0.9,
662
+ value=0.4,
663
+ step=0.05,
664
+ label="PP-DocLayoutV3 Confidence (Table Detection)",
665
+ info="Lower values detect more tables; higher values reduce false positives."
666
+ )
667
+
668
+ tatr_threshold = gr.Slider(
669
+ minimum=0.5,
670
+ maximum=0.95,
671
+ value=0.8,
672
+ step=0.05,
673
+ label="Table Transformer Confidence (Grid Structure)",
674
+ info="Confidence threshold for rows/columns/spanning cell components."
675
+ )
676
+
677
+ submit_btn = gr.Button("Convert and Extract Tables", variant="primary", elem_classes="primary-btn")
678
+
679
+ with gr.Column(scale=3):
680
+ excel_output = gr.File(label="📥 Download Extracted Excel Workbook")
681
+
682
+ with gr.Tabs():
683
+ with gr.TabItem("🖼️ Layout Detection Bboxes"):
684
+ page_gallery = gr.Gallery(label="Page layout overview", columns=2, height=600)
685
+
686
+ with gr.TabItem("🕸️ Table Grids / Crops"):
687
+ grid_gallery = gr.Gallery(label="Isolated tables and grids", columns=2, height=600)
688
+
689
+ # Trigger processing
690
+ submit_btn.click(
691
+ fn=process_pdf,
692
+ inputs=[pdf_input, mode, doc_layout_threshold, tatr_threshold],
693
+ outputs=[excel_output, page_gallery, grid_gallery]
694
+ )
695
+
696
+ if __name__ == "__main__":
697
+ demo.launch(
698
+ server_name="0.0.0.0",
699
+ server_port=7860,
700
+ theme=gr.themes.Soft(primary_hue="orange", secondary_hue="slate"),
701
+ css=custom_css
702
+ )