rehan953 commited on
Commit
177ec96
·
verified ·
1 Parent(s): 1729896

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +99 -286
app.py CHANGED
@@ -87,285 +87,111 @@ def _resize_for_inference(img):
87
  return img.resize(new_size, Image.LANCZOS)
88
 
89
 
90
- def _build_table_from_entries(entries: List[dict]) -> str:
91
- if not entries:
92
- return ""
93
- table = [
94
- "| Date | Description | Amount |",
95
- "|------|------------|--------|",
96
- ]
97
- for row in entries:
98
- table.append(f"| {row['date']} | {row['description']} | {row['amount']} |")
99
- return "\n".join(table)
100
 
101
 
102
- def _is_transaction_start(line: str) -> bool:
103
- return bool(re.match(r"\d{2}/\d{2}", line.strip()))
104
-
105
-
106
- def _ends_with_amount(text: str) -> bool:
107
- return bool(re.search(r"\d{1,3}(?:,\d{3})*\.\d{2}$", text.strip()))
108
-
109
-
110
- def _is_noise_line(line: str) -> bool:
111
- s = line.strip().lower()
112
- if not s:
113
- return True
114
- if s == "---page-separator---":
115
- return True
116
- if "call 1-800-937-2000" in s:
117
- return True
118
- if s in ("<table border=\"1\"><tr><td></td></tr></table>", "<table><tr><td></td></tr></table>"):
119
- return True
120
- return False
121
-
122
-
123
- def _merge_transaction_lines(rows: List[str]) -> List[str]:
124
- """
125
- Step 1/2: Merge multiline OCR rows into single transaction rows.
126
- A new row begins when a line starts with MM/DD.
127
- """
128
- merged: List[str] = []
129
- current = ""
130
-
131
- for raw in rows:
132
- line = " ".join(raw.strip().split())
133
- if not line or _is_noise_line(line):
134
- continue
135
-
136
- if _is_transaction_start(line):
137
- if current:
138
- merged.append(current.strip())
139
- current = line
140
- else:
141
- if current:
142
- current += " " + line
143
- else:
144
- current = line
145
-
146
- if current:
147
- merged.append(current.strip())
148
-
149
- return merged
150
-
151
-
152
- def _repair_orphan_amounts(rows: List[str]) -> List[str]:
153
  """
154
- Step 3: Attach lines without ending amount to prior transaction line when possible.
 
 
155
  """
156
- fixed: List[str] = []
157
- for line in rows:
158
- if _ends_with_amount(line):
159
- fixed.append(line)
160
- elif fixed:
161
- fixed[-1] = f"{fixed[-1]} {line}".strip()
162
- else:
163
- fixed.append(line)
164
- return fixed
165
-
 
166
 
167
- def _normalize_transactions(text: str) -> str:
168
- """
169
- Convert loose transaction lines into structured markdown tables using:
170
- MM/DD ... amount
171
- """
172
- lines = text.split("\n")
173
- result: List[str] = []
174
- table_buffer: List[str] = []
175
- in_section = False
176
- pattern = re.compile(r"(\d{2}/\d{2})\s+(.*?)\s+([\d,]+\.\d{2})$")
177
-
178
- for line in lines:
179
- if re.search(r"POSTING DATE.*DESCRIPTION.*AMOUNT", line, re.IGNORECASE):
180
- in_section = True
181
- table_buffer = []
182
- continue
183
 
184
- if in_section and (line.strip() == "" or line.strip().startswith("Subtotal")):
185
- if table_buffer:
186
- clean_rows = []
187
- merged_rows = _merge_transaction_lines(table_buffer)
188
- repaired_rows = _repair_orphan_amounts(merged_rows)
189
- for normalized in repaired_rows:
190
- match = pattern.search(normalized)
191
- if match:
192
- date, desc, amount = match.groups()
193
- clean_rows.append(
194
- {"date": date, "description": desc, "amount": amount}
195
- )
196
- if clean_rows:
197
- result.append(_build_table_from_entries(clean_rows))
198
- else:
199
- result.extend(table_buffer)
200
- table_buffer = []
201
- in_section = False
202
  result.append(line)
 
203
  continue
204
 
205
- if in_section:
206
- table_buffer.append(line)
207
- else:
208
- result.append(line)
209
-
210
- if table_buffer:
211
- clean_rows = []
212
- merged_rows = _merge_transaction_lines(table_buffer)
213
- repaired_rows = _repair_orphan_amounts(merged_rows)
214
- for normalized in repaired_rows:
215
- match = pattern.search(normalized)
216
- if match:
217
- date, desc, amount = match.groups()
218
- clean_rows.append(
219
- {"date": date, "description": desc, "amount": amount}
220
- )
221
- if clean_rows:
222
- result.append(_build_table_from_entries(clean_rows))
223
- else:
224
- result.extend(table_buffer)
225
-
226
- return "\n".join(result)
227
-
228
-
229
- def _fix_missing_amounts(text: str) -> str:
230
- lines = text.split("\n")
231
- fixed: List[str] = []
232
-
233
- for line in lines:
234
- if re.search(r"\d{2}/\d{2}", line) and not re.search(r"\d+\.\d{2}", line):
235
- match = re.search(r"(\d{1,3}(?:,\d{3})*\.\d{2})$", line)
236
- if match:
237
- line += f" {match.group(1)}"
238
- fixed.append(line)
239
-
240
- return "\n".join(fixed)
241
-
242
-
243
- def _html_to_markdown_tables(text: str) -> str:
244
- try:
245
- from bs4 import BeautifulSoup
246
- except Exception:
247
- # Fallback when bs4 is unavailable: regex-based conversion for simple tables.
248
- table_re = re.compile(r"<table[\s\S]*?</table>", re.IGNORECASE)
249
- row_re = re.compile(r"<tr[\s\S]*?</tr>", re.IGNORECASE)
250
- cell_re = re.compile(r"<t[dh][^>]*>([\s\S]*?)</t[dh]>", re.IGNORECASE)
251
- tag_re = re.compile(r"<[^>]+>")
252
-
253
- def _clean_cell(cell: str) -> str:
254
- cell = re.sub(r"<br\s*/?>", " ", cell, flags=re.IGNORECASE)
255
- cell = tag_re.sub(" ", cell)
256
- cell = cell.replace("&nbsp;", " ").replace("&amp;", "&")
257
- cell = re.sub(r"\s+", " ", cell).strip()
258
- return cell
259
-
260
- def _convert(table_html: str) -> str:
261
- rows = []
262
- for tr in row_re.findall(table_html):
263
- cols = [_clean_cell(c) for c in cell_re.findall(tr)]
264
- cols = [c for c in cols if c != ""]
265
- if cols:
266
- rows.append(cols)
267
- if not rows:
268
- return table_html
269
- width = max(len(r) for r in rows)
270
- norm = [r + [""] * (width - len(r)) for r in rows]
271
- md = []
272
- md.append("| " + " | ".join(norm[0]) + " |")
273
- md.append("| " + " | ".join(["---"] * width) + " |")
274
- for r in norm[1:]:
275
- md.append("| " + " | ".join(r) + " |")
276
- return "\n".join(md)
277
-
278
- return table_re.sub(lambda m: _convert(m.group(0)), text)
279
-
280
- soup = BeautifulSoup(text, "html.parser")
281
-
282
- for table in soup.find_all("table"):
283
- rows = []
284
- for tr in table.find_all("tr"):
285
- cols = [td.get_text(strip=True) for td in tr.find_all(["td", "th"])]
286
- if cols:
287
- rows.append(cols)
288
-
289
- if rows:
290
- md = []
291
- header = rows[0]
292
- md.append("| " + " | ".join(header) + " |")
293
- md.append("| " + " | ".join(["---"] * len(header)) + " |")
294
-
295
- for row in rows[1:]:
296
- padded = row + [""] * (len(header) - len(row))
297
- md.append("| " + " | ".join(padded[: len(header)]) + " |")
298
-
299
- table.replace_with("\n".join(md))
300
-
301
- return str(soup)
302
-
303
-
304
- def _remove_prompt_echo_artifacts(text: str) -> str:
305
- """
306
- Remove model prompt-echo garbage blocks such as repeated:
307
- 'N) Keep left-most amount values.' / 'N) Keep right-most amount values.'
308
- """
309
- lines = text.split("\n")
310
- cleaned: List[str] = []
311
- keep_line_re = re.compile(
312
- r"^\s*\d+\)\s*Keep\s+(left-most|right-most)\s+amount\s+values\.?\s*$",
313
- re.IGNORECASE,
314
- )
315
- repeat_run = 0
316
-
317
- for line in lines:
318
- if keep_line_re.match(line):
319
- repeat_run += 1
320
  continue
321
- # Also drop explicit prompt/rules echoes if they appear as plain output.
322
- if re.match(r"^\s*Document Parsing to markdown\.?\s*$", line, re.IGNORECASE):
323
- continue
324
- if re.match(r"^\s*Rules:\s*$", line, re.IGNORECASE):
325
- continue
326
- cleaned.append(line)
327
-
328
- # If there was a long repeated run, ensure dangling separator spacing stays clean.
329
- text = "\n".join(cleaned)
330
- text = re.sub(r"\n{4,}", "\n\n\n", text)
331
- return text
332
 
 
 
333
 
334
- def _clean_markdown(text: str) -> str:
335
- """Post-process markdown to fix table formatting and section structure."""
336
- text = _remove_prompt_echo_artifacts(text)
337
- text = _html_to_markdown_tables(text)
338
- text = _fix_missing_amounts(text)
339
- text = _normalize_transactions(text)
340
-
341
- lines = text.split('\n')
342
- cleaned = []
343
- in_table = False
344
-
345
- for line in lines:
346
- if '|' in line and line.strip().startswith('|'):
347
- if not in_table:
348
- if cleaned and cleaned[-1].strip():
349
- cleaned.append('')
350
- in_table = True
351
-
352
- line = re.sub(r'\s*\|\s*', ' | ', line)
353
- line = re.sub(r'\s+', ' ', line)
354
- cleaned.append(line.strip())
355
- elif in_table and line.strip() == '':
356
- in_table = False
357
- cleaned.append('')
358
- else:
359
- in_table = False
360
- if line.strip() or (cleaned and cleaned[-1].strip()):
361
- cleaned.append(line)
362
-
363
- text = '\n'.join(cleaned)
364
- text = re.sub(r'(#{1,6})\s*([^\n]+)', r'\1 \2', text)
365
- text = re.sub(r'\n([•\-\*])\s+', r'\n\1 ', text)
366
- text = '\n'.join(line.rstrip() for line in text.split('\n'))
367
- text = re.sub(r'\n{4,}', '\n\n\n', text)
368
- return text.strip()
369
 
370
 
371
  def _infer_image(image_path: str) -> str:
@@ -387,14 +213,7 @@ def _infer_image(image_path: str) -> str:
387
  "role": "user",
388
  "content": [
389
  {"type": "image", "url": resized_path},
390
- {
391
- "type": "text",
392
- "text": (
393
- "Extract document text as markdown only. "
394
- "Do not include instructions, examples, or template rows. "
395
- "Preserve transaction lines exactly."
396
- ),
397
- },
398
  ],
399
  }]
400
 
@@ -407,8 +226,7 @@ def _infer_image(image_path: str) -> str:
407
  ).to(model.device)
408
  inputs.pop("token_type_ids", None)
409
 
410
- if torch.cuda.is_available():
411
- torch.cuda.empty_cache()
412
 
413
  with torch.no_grad():
414
  ids = model.generate(
@@ -422,9 +240,9 @@ def _infer_image(image_path: str) -> str:
422
  ids[0][inputs["input_ids"].shape[1]:],
423
  skip_special_tokens=True,
424
  )
425
-
426
- # Clean up the markdown output
427
- return _clean_markdown(result.strip())
428
 
429
  finally:
430
  try:
@@ -528,18 +346,13 @@ def _create_gradio_demo():
528
  with gr.Blocks(title="GLM-OCR Fine-tuned") as demo:
529
  gr.Markdown("# GLM-OCR (Fine-tuned)")
530
  gr.Markdown("Upload a PDF or image to extract structured markdown content.")
531
-
532
  file_in = gr.File(
533
  label="Upload PDF or image",
534
  file_types=[".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".bmp"],
535
  )
536
  run_btn = gr.Button("Run OCR", variant="primary")
537
- out = gr.Textbox(
538
- lines=40,
539
- label="Output (markdown)"
540
- )
541
  run_btn.click(fn=run_ocr, inputs=file_in, outputs=out)
542
-
543
  return demo
544
 
545
 
 
87
  return img.resize(new_size, Image.LANCZOS)
88
 
89
 
90
+ def _detect_headers(header_line: str) -> List[str]:
91
+ """Extract column headers from a header line by splitting on 2+ spaces."""
92
+ parts = re.split(r'\s{2,}', header_line.strip())
93
+ headers = [p.strip() for p in parts if p.strip()]
94
+ return headers if headers else ["POSTING DATE", "DESCRIPTION", "AMOUNT"]
 
 
 
 
 
95
 
96
 
97
+ def _plaintext_rows_to_table(text: str) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  """
99
+ Convert plain-text transaction rows (no pipe boundaries) into proper
100
+ markdown tables. Detects headers dynamically from the document.
101
+ Works for any column layout, not just Date/Description/Amount.
102
  """
103
+ lines = text.split('\n')
104
+ result = []
105
+ i = 0
106
+ # Pattern to detect a header line (2+ words separated by 2+ spaces)
107
+ header_re = re.compile(
108
+ r'^[A-Z][A-Z\s]{3,}$',
109
+ )
110
+ # Pattern for a transaction row starting with MM/DD
111
+ txn_re = re.compile(r'^\d{2}/\d{2}\s+\S')
112
+ # Pattern to extract amount at end of line
113
+ amt_re = re.compile(r'^(.*?)\s+([\d,]+\.\d{2})$')
114
 
115
+ while i < len(lines):
116
+ line = lines[i]
117
+ stripped = line.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
+ # Already a proper markdown table row — pass through unchanged
120
+ if stripped.startswith('|'):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  result.append(line)
122
+ i += 1
123
  continue
124
 
125
+ # Detect start of a plain-text transaction block
126
+ if txn_re.match(stripped):
127
+ # Look back to find the most recent header line before this block
128
+ detected_headers = None
129
+ for j in range(len(result) - 1, max(len(result) - 5, -1), -1):
130
+ prev = result[j].strip()
131
+ if prev and not prev.startswith('|') and not prev.startswith('#'):
132
+ candidate_headers = _detect_headers(prev)
133
+ if len(candidate_headers) >= 2:
134
+ detected_headers = candidate_headers
135
+ # Remove the header line from result since we'll put it in table
136
+ result.pop(j)
137
+ break
138
+
139
+ if not detected_headers:
140
+ detected_headers = ["POSTING DATE", "DESCRIPTION", "AMOUNT"]
141
+
142
+ # Collect all transaction rows in this block
143
+ block = []
144
+ while i < len(lines):
145
+ l = lines[i].strip()
146
+ if not l:
147
+ break
148
+ if l.startswith('|') or l.startswith('#'):
149
+ break
150
+ if re.match(r'^Subtotal', l, re.IGNORECASE):
151
+ break
152
+ if txn_re.match(l):
153
+ block.append(l)
154
+ elif block:
155
+ # Continuation line — merge into previous row
156
+ block[-1] += ' ' + l
157
+ i += 1
158
+
159
+ if block:
160
+ # Build markdown table with detected headers
161
+ num_cols = len(detected_headers)
162
+ result.append('| ' + ' | '.join(detected_headers) + ' |')
163
+ result.append('| ' + ' | '.join(['---'] * num_cols) + ' |')
164
+
165
+ for row in block:
166
+ if num_cols == 3:
167
+ # Try to split into date / description / amount
168
+ m = amt_re.match(row)
169
+ if m:
170
+ rest = m.group(1)
171
+ amount = m.group(2)
172
+ date_m = re.match(r'^(\d{2}/\d{2})\s+(.*)', rest)
173
+ if date_m:
174
+ result.append(
175
+ f'| {date_m.group(1)} | {date_m.group(2)} | {amount} |'
176
+ )
177
+ continue
178
+ # No amount found — put full row in description, leave amount blank
179
+ date_m = re.match(r'^(\d{2}/\d{2})\s+(.*)', row)
180
+ if date_m:
181
+ result.append(
182
+ f'| {date_m.group(1)} | {date_m.group(2)} | |'
183
+ )
184
+ else:
185
+ result.append(f'| | {row} | |')
186
+ else:
187
+ # For non-3-column tables just put full row as single cell
188
+ result.append(f'| {row} |')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  continue
 
 
 
 
 
 
 
 
 
 
 
190
 
191
+ result.append(line)
192
+ i += 1
193
 
194
+ return '\n'.join(result)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
 
196
 
197
  def _infer_image(image_path: str) -> str:
 
213
  "role": "user",
214
  "content": [
215
  {"type": "image", "url": resized_path},
216
+ {"type": "text", "text": "Document Parsing:"},
 
 
 
 
 
 
 
217
  ],
218
  }]
219
 
 
226
  ).to(model.device)
227
  inputs.pop("token_type_ids", None)
228
 
229
+ torch.cuda.empty_cache()
 
230
 
231
  with torch.no_grad():
232
  ids = model.generate(
 
240
  ids[0][inputs["input_ids"].shape[1]:],
241
  skip_special_tokens=True,
242
  )
243
+
244
+ result = _plaintext_rows_to_table(result)
245
+ return result.strip()
246
 
247
  finally:
248
  try:
 
346
  with gr.Blocks(title="GLM-OCR Fine-tuned") as demo:
347
  gr.Markdown("# GLM-OCR (Fine-tuned)")
348
  gr.Markdown("Upload a PDF or image to extract structured markdown content.")
 
349
  file_in = gr.File(
350
  label="Upload PDF or image",
351
  file_types=[".pdf", ".png", ".jpg", ".jpeg", ".tiff", ".bmp"],
352
  )
353
  run_btn = gr.Button("Run OCR", variant="primary")
354
+ out = gr.Textbox(lines=40, label="Output (markdown)")
 
 
 
355
  run_btn.click(fn=run_ocr, inputs=file_in, outputs=out)
 
356
  return demo
357
 
358