Baiyinyou commited on
Commit
ef2d336
·
verified ·
1 Parent(s): 25ae816

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +199 -779
app.py CHANGED
@@ -9,11 +9,10 @@ import threading
9
  from queue import Queue
10
  import zipfile
11
  from datetime import datetime
12
- import matplotlib.pyplot as plt
13
  import matplotlib
14
- matplotlib.use('Agg') # 使用非交互式后端
15
- from io import BytesIO
16
- import base64
17
 
18
  from detection_ocr import DetectionOCR
19
  from repair_head import RepairHead
@@ -35,21 +34,18 @@ _semantic = None
35
  def get_ocr():
36
  global _ocr
37
  if _ocr is None:
38
- print("[INIT] Loading DetectionOCR")
39
  _ocr = DetectionOCR(device=DEVICE)
40
  return _ocr
41
 
42
  def get_repair():
43
  global _repair
44
  if _repair is None:
45
- print("[INIT] Loading RepairHead (CPU)")
46
  _repair = RepairHead(device="cpu")
47
  return _repair
48
 
49
  def get_semantic():
50
  global _semantic
51
  if _semantic is None:
52
- print("[INIT] Loading Semantic Evaluation Pipeline")
53
  _semantic = SemanticEvaluationPipeline(
54
  base_model="Qwen/Qwen2.5-Math-1.5B-Instruct",
55
  lora_dir=None,
@@ -66,10 +62,6 @@ def wrap_equation(expr: str) -> str:
66
  expr = expr.strip()
67
  if any(expr.startswith(f"\\begin{{{e}}}") for e in KNOWN_ENV):
68
  return expr
69
- if expr.startswith("$$") and expr.endswith("$$"):
70
- return expr
71
- if expr.startswith("\\[") and expr.endswith("\\]"):
72
- return expr
73
  return f"\\begin{{equation}}\n{expr}\n\\end{{equation}}"
74
 
75
  def is_reasonable(expr: str) -> bool:
@@ -77,820 +69,248 @@ def is_reasonable(expr: str) -> bool:
77
  return False
78
  if expr.lower() in ["noise", "error"]:
79
  return False
80
- if len(expr) < 3 or len(expr) > 2000:
81
- return False
82
- return True
83
 
84
  def save_progress_log(outdir: Path, stage: str, content: str):
85
- """Save progress logs to files"""
86
  log_file = outdir / f"{stage}_log.txt"
87
  with open(log_file, "a", encoding="utf-8") as f:
88
- timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
89
- f.write(f"[{timestamp}] {content}\n")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
 
 
 
 
91
  def create_visualization_pdf(outdir: Path, repaired_data: dict, semantic_results: list):
92
- """创建可视化PDF,按页面顺序显示公式"""
93
  try:
94
  from fpdf import FPDF
95
- import matplotlib.pyplot as plt
96
- import matplotlib
97
- matplotlib.use('Agg')
98
- from io import BytesIO
99
- import base64
100
-
101
  pdf = FPDF()
102
  pdf.set_auto_page_break(auto=True, margin=15)
103
-
104
- # 添加标题页
105
  pdf.add_page()
106
- pdf.set_font('Arial', 'B', 16)
107
- pdf.cell(0, 10, 'PDF Formula Processing Results', 0, 1, 'C')
108
- pdf.ln(10)
109
-
110
- pdf.set_font('Arial', '', 12)
111
- pdf.cell(0, 10, f'Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}', 0, 1, 'C')
112
- pdf.ln(20)
113
-
114
- # 按页面处理
115
- page_keys = sorted(repaired_data.keys(), key=lambda x: int(re.search(r'page_(\d+)', x).group(1)) if re.search(r'page_(\d+)', x) else 0)
116
-
117
- for page_key in page_keys:
118
- formulas = repaired_data[page_key]
119
- valid_formulas = [f for f in formulas if f['repaired'] not in ['noise', 'error']]
120
-
121
- if not valid_formulas:
122
  continue
123
-
124
  pdf.add_page()
125
- pdf.set_font('Arial', 'B', 14)
126
- pdf.cell(0, 10, f'{page_key.upper()}', 0, 1, 'L')
127
- pdf.ln(5)
128
-
129
- for idx, formula_data in enumerate(valid_formulas[:20]): # 每页最多20个公式
130
- formula = formula_data['repaired']
131
- original = formula_data['raw_latex'][:100] + "..." if len(formula_data['raw_latex']) > 100 else formula_data['raw_latex']
132
-
133
- # 添加公式信息
134
- pdf.set_font('Arial', 'I', 10)
135
- pdf.cell(0, 5, f'Formula {idx+1}:', 0, 1, 'L')
136
-
137
- pdf.set_font('Courier', '', 8)
138
- pdf.multi_cell(0, 4, f'Original: {original}', 0, 'L')
139
- pdf.multi_cell(0, 4, f'Repaired: {formula[:100]}{"..." if len(formula) > 100 else ""}', 0, 'L')
140
-
141
- # 查找对应的语义结果
142
- sem_result = None
143
- for sem in semantic_results:
144
- if sem.get('original_formula') == formula_data['raw_latex'] or sem.get('fixed_formula') == formula:
145
- sem_result = sem
146
- break
147
-
148
- if sem_result:
149
- verdict = sem_result.get('semantic_verdict', 'unknown')
150
- confidence = sem_result.get('confidence', 0)
151
- pdf.set_font('Arial', '', 9)
152
- pdf.cell(0, 5, f'Verdict: {verdict} (Confidence: {confidence:.2f})', 0, 1, 'L')
153
-
154
- pdf.ln(2)
155
-
156
- # 每5个公式后添加分隔线
157
- if (idx + 1) % 5 == 0 and idx + 1 < len(valid_formulas):
158
- pdf.line(10, pdf.get_y(), 200, pdf.get_y())
159
- pdf.ln(5)
160
-
161
- # 添加统计页
162
- pdf.add_page()
163
- pdf.set_font('Arial', 'B', 16)
164
- pdf.cell(0, 10, 'Processing Statistics', 0, 1, 'C')
165
- pdf.ln(10)
166
-
167
- total_pages = len(repaired_data)
168
- total_formulas = sum(len(page_data) for page_data in repaired_data.values())
169
- valid_formulas = sum(len([f for f in page_data if f['repaired'] not in ['noise', 'error']])
170
- for page_data in repaired_data.values())
171
-
172
- stats = [
173
- ("Total Pages Processed", str(total_pages)),
174
- ("Total Formulas Detected", str(total_formulas)),
175
- ("Valid Formulas Repaired", str(valid_formulas)),
176
- ("Semantic Evaluations", str(len(semantic_results))),
177
- ]
178
-
179
- pdf.set_font('Arial', '', 12)
180
- for label, value in stats:
181
- pdf.cell(90, 8, f'{label}:', 0, 0, 'R')
182
- pdf.set_font('Arial', 'B', 12)
183
- pdf.cell(0, 8, f' {value}', 0, 1, 'L')
184
- pdf.set_font('Arial', '', 12)
185
- pdf.ln(2)
186
-
187
- # 保存PDF
188
- pdf_path = outdir / "visualization_report.pdf"
189
- pdf.output(str(pdf_path))
190
-
191
- return pdf_path
192
-
193
- except Exception as e:
194
- print(f"Error creating visualization PDF: {e}")
195
- return None
196
 
197
- def render_formula_to_image(formula: str, output_path: Path):
198
- """将LaTeX公式渲染为图像"""
199
- try:
200
- import matplotlib.pyplot as plt
201
- import matplotlib
202
- matplotlib.use('Agg')
203
- from matplotlib import rcParams
204
-
205
- # 配置matplotlib使用LaTeX
206
- rcParams['text.usetex'] = True
207
- rcParams['font.family'] = 'serif'
208
- rcParams['font.size'] = 14
209
-
210
- # 创建图形
211
- fig, ax = plt.subplots(figsize=(8, 2))
212
- ax.text(0.5, 0.5, f'${formula}$',
213
- fontsize=16,
214
- ha='center',
215
- va='center',
216
- usetex=True)
217
-
218
- ax.axis('off')
219
- plt.tight_layout()
220
-
221
- # 保存图像
222
- plt.savefig(str(output_path), dpi=150, bbox_inches='tight', pad_inches=0.1)
223
- plt.close(fig)
224
-
225
- return True
226
  except Exception as e:
227
- print(f"Error rendering formula: {e}")
228
- # 如果LaTeX渲染失败,使用文本模式
229
- try:
230
- fig, ax = plt.subplots(figsize=(8, 2))
231
- ax.text(0.5, 0.5, formula,
232
- fontsize=14,
233
- ha='center',
234
- va='center',
235
- fontfamily='monospace',
236
- wrap=True)
237
-
238
- ax.axis('off')
239
- plt.tight_layout()
240
- plt.savefig(str(output_path), dpi=150, bbox_inches='tight', pad_inches=0.1)
241
- plt.close(fig)
242
- return True
243
- except Exception as e2:
244
- print(f"Fallback rendering also failed: {e2}")
245
- return False
246
-
247
- def create_formula_images(outdir: Path, repaired_data: dict):
248
- """为所有公式创建图像文件"""
249
- images_dir = outdir / "formula_images"
250
- images_dir.mkdir(exist_ok=True)
251
-
252
- image_paths = []
253
-
254
- for page_idx, (page, formulas) in enumerate(repaired_data.items()):
255
- page_images_dir = images_dir / page
256
- page_images_dir.mkdir(exist_ok=True)
257
-
258
- for idx, formula_data in enumerate(formulas):
259
- if formula_data['repaired'] in ['noise', 'error']:
260
- continue
261
-
262
- formula = formula_data['repaired']
263
- image_path = page_images_dir / f"formula_{idx+1}.png"
264
-
265
- if render_formula_to_image(formula, image_path):
266
- image_paths.append({
267
- 'page': page,
268
- 'formula_idx': idx + 1,
269
- 'formula': formula,
270
- 'image_path': str(image_path),
271
- 'relative_path': f"{page}/formula_{idx+1}.png"
272
- })
273
-
274
- # 创建索引文件
275
- if image_paths:
276
- index_path = images_dir / "index.json"
277
- with open(index_path, 'w', encoding='utf-8') as f:
278
- json.dump(image_paths, f, indent=2, ensure_ascii=False)
279
-
280
- return image_paths
281
 
282
  # ============================================================
283
- # Stream Manager for Real-time Updates
284
  # ============================================================
285
  class StreamManager:
286
- """Manage real-time streaming to multiple outputs"""
287
  def __init__(self):
288
  self.ocr_queue = Queue()
289
  self.repair_queue = Queue()
290
  self.semantic_queue = Queue()
291
  self.summary_queue = Queue()
292
-
293
- def ocr_stream(self, message: str):
294
- self.ocr_queue.put(message)
295
- if hasattr(self, 'current_outdir'):
296
- save_progress_log(self.current_outdir, "ocr", message)
297
-
298
- def repair_stream(self, message: str):
299
- self.repair_queue.put(message)
300
- if hasattr(self, 'current_outdir'):
301
- save_progress_log(self.current_outdir, "repair", message)
302
-
303
- def semantic_stream(self, message: str):
304
- self.semantic_queue.put(message)
305
- if hasattr(self, 'current_outdir'):
306
- save_progress_log(self.current_outdir, "semantic", message)
307
-
308
- def summary_stream(self, message: str):
309
- self.summary_queue.put(message)
310
- if hasattr(self, 'current_outdir'):
311
- save_progress_log(self.current_outdir, "summary", message)
312
-
313
  def set_output_dir(self, outdir: Path):
314
  self.current_outdir = outdir
315
 
 
 
 
 
 
 
 
 
 
 
316
  # ============================================================
317
- # Main Processing Pipeline
318
  # ============================================================
319
  def process_pipeline(pdf_file, stream_mgr: StreamManager):
320
- """Main processing pipeline with streaming to different outputs"""
321
-
322
- if pdf_file is None:
323
- stream_mgr.summary_stream("❌ No PDF uploaded.")
324
- return None, None, None, None, None, None, []
325
-
326
- try:
327
- # Create output directory
328
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
329
- tmpdir = Path(tempfile.mkdtemp(prefix=f"pdf_pipeline_{timestamp}_"))
330
- outdir = tmpdir / "output"
331
- outdir.mkdir(exist_ok=True)
332
-
333
- stream_mgr.set_output_dir(outdir)
334
-
335
- stream_mgr.summary_stream(f"📁 Output directory: {outdir}")
336
- stream_mgr.summary_stream("=" * 60)
337
-
338
- # ============ STAGE 1: OCR ============
339
- stream_mgr.ocr_stream("🚀 STAGE 1: OCR Detection Started")
340
- stream_mgr.ocr_stream("Loading OCR model...")
341
-
342
- ocr = get_ocr()
343
- stream_mgr.ocr_stream("Processing PDF pages...")
344
-
345
- ocr.process_pdf(pdf_file, output_dir=outdir)
346
-
347
- stream_mgr.ocr_stream("✅ OCR Processing Completed")
348
- stream_mgr.summary_stream("✓ Stage 1: OCR completed")
349
-
350
- # ============ STAGE 2: Merge Results ============
351
- stream_mgr.ocr_stream("\n🔄 Merging page results...")
352
-
353
- merged = {}
354
- page_files = sorted(outdir.glob("page_*_results.json"))
355
-
356
- if not page_files:
357
- stream_mgr.ocr_stream("❌ No page results found!")
358
- stream_mgr.summary_stream("❌ Error: No page results found")
359
- return None, None, None, None, None, None, []
360
-
361
- for p in page_files:
362
- m = re.search(r"(page_\d+)_results\.json", p.name)
363
- if not m:
364
- continue
365
- page = m.group(1)
366
  try:
367
- data = json.loads(p.read_text("utf-8"))
368
- formulas = data.get("formulas", [])
369
- merged[page] = {
370
- "formulas": formulas,
371
- "total": len(formulas)
372
- }
373
- stream_mgr.ocr_stream(f" {page}: {len(formulas)} formulas")
374
- except Exception as e:
375
- stream_mgr.ocr_stream(f" ⚠️ Error loading {page}: {str(e)}")
376
- merged[page] = {"formulas": [], "total": 0}
377
-
378
- # Save overall OCR results
379
- overall_json = outdir / "overall_ocr_results.json"
380
- overall_json.write_text(json.dumps(merged, indent=2), "utf-8")
381
-
382
- stream_mgr.ocr_stream(f"✅ Saved: {overall_json.name}")
383
- stream_mgr.summary_stream(f"✓ Stage 2: Merged {len(merged)} pages")
384
-
385
- # ============ STAGE 3: Repair Formulas ============
386
- stream_mgr.repair_stream("🔧 STAGE 3: LaTeX Repair Started")
387
- stream_mgr.repair_stream("Loading Repair model...")
388
-
389
- repair = get_repair()
390
-
391
- repaired_all = {}
392
- repaired_eqs = []
393
- total_formulas = sum(v["total"] for v in merged.values())
394
-
395
- stream_mgr.repair_stream(f"Total formulas to repair: {total_formulas}")
396
- stream_mgr.repair_stream("=" * 40)
397
-
398
- processed = 0
399
- for page, content in merged.items():
400
- stream_mgr.repair_stream(f"\n📄 Processing {page}:")
401
- repaired_all[page] = []
402
-
403
- for i, fm in enumerate(content["formulas"]):
404
- processed += 1
405
- raw = fm.get("raw_latex", "")
406
-
407
- # Show progress every 5 formulas or first/last
408
- if processed <= 5 or processed >= total_formulas - 5 or processed % 10 == 0:
409
- raw_preview = raw[:100] + "..." if len(raw) > 100 else raw
410
- stream_mgr.repair_stream(f" [{processed}/{total_formulas}] {raw_preview}")
411
-
412
- try:
413
- repaired = repair.repair(raw)
414
-
415
- if is_reasonable(repaired):
416
- repaired_all[page].append({
417
- "raw_latex": raw,
418
- "repaired": repaired
419
- })
420
- repaired_eqs.append(repaired)
421
- if processed <= 3: # Show first few repairs
422
- stream_mgr.repair_stream(f" → {repaired[:80]}...")
423
- else:
424
- repaired_all[page].append({
425
- "raw_latex": raw,
426
- "repaired": "noise"
427
- })
428
- except Exception as e:
429
  repaired_all[page].append({
430
  "raw_latex": raw,
431
- "repaired": f"error: {str(e)}"
432
  })
433
- stream_mgr.repair_stream(f" ⚠️ Error: {str(e)}")
434
-
435
- # Save repaired results
436
- repaired_json = outdir / "overall_ocr_repaired.json"
437
- repaired_json.write_text(json.dumps(repaired_all, indent=2), "utf-8")
438
-
439
- stream_mgr.repair_stream(f"\n✅ Repair completed: {len(repaired_eqs)} valid formulas")
440
- stream_mgr.summary_stream(f"✓ Stage 3: Repaired {len(repaired_eqs)}/{total_formulas} formulas")
441
-
442
- # ============ STAGE 4: Semantic Evaluation ============
443
- stream_mgr.semantic_stream("🧠 STAGE 4: Semantic Evaluation Started")
444
- stream_mgr.semantic_stream("Loading Semantic model...")
445
-
446
- semantic = get_semantic()
447
-
448
- semantic_results = []
449
- stream_mgr.semantic_stream(f"Evaluating {len(repaired_eqs)} formulas...")
450
- stream_mgr.semantic_stream("=" * 40)
451
-
452
- for i, eq in enumerate(repaired_eqs):
453
- try:
454
- # Show progress
455
- if i < 3 or i >= len(repaired_eqs) - 3 or i % 5 == 0:
456
- eq_preview = eq[:80] + "..." if len(eq) > 80 else eq
457
- stream_mgr.semantic_stream(f"[{i+1}/{len(repaired_eqs)}] {eq_preview}")
458
-
459
- res = semantic.evaluate_formula(eq)
460
- res['original_formula'] = eq # 添加原始公式
461
- semantic_results.append(res)
462
-
463
- # Show verdict for first few
464
- if i < 5:
465
- stream_mgr.semantic_stream(f" Verdict: {res.get('semantic_verdict', 'unknown')}")
466
- stream_mgr.semantic_stream(f" Confidence: {res.get('confidence', 0):.2f}")
467
-
468
  except Exception as e:
469
- stream_mgr.semantic_stream(f"⚠️ Error evaluating formula {i+1}: {str(e)}")
470
- semantic_results.append({
471
- 'original_formula': eq,
472
- 'error': str(e),
473
- 'semantic_verdict': 'evaluation_error',
474
- 'confidence': 0.0
475
- })
476
-
477
- # Save semantic results
478
- semantic_json = outdir / "semantic_results.json"
479
- semantic_json.write_text(json.dumps(semantic_results, indent=2), "utf-8")
480
-
481
- stream_mgr.semantic_stream(f"\n✅ Semantic evaluation completed: {len(semantic_results)} formulas")
482
- stream_mgr.summary_stream(f"✓ Stage 4: Semantically evaluated {len(semantic_results)} formulas")
483
-
484
- # ============ STAGE 5: Generate Visualization Files ============
485
- stream_mgr.summary_stream("\n📊 Generating visualization files...")
486
-
487
- # 1. Generate LaTeX file
488
- merge_tex = outdir / "merge.tex"
489
- with merge_tex.open("w", encoding="utf-8") as f:
490
- f.write("\\documentclass{article}\n")
491
- f.write("\\usepackage{amsmath, amssymb, bm}\n")
492
- f.write("\\begin{document}\n\n")
493
- f.write("% Auto-generated from PDF formula extraction pipeline\n")
494
- f.write("% Generated on: " + datetime.now().strftime("%Y-%m-%d %H:%M:%S") + "\n\n")
495
- for eq in repaired_eqs:
496
- f.write(wrap_equation(eq) + "\n\n")
497
- f.write("\\end{document}\n")
498
-
499
- stream_mgr.summary_stream("✓ Generated LaTeX file")
500
-
501
- # 2. Create visualization PDF
502
- stream_mgr.summary_stream("Creating visualization PDF...")
503
- visualization_pdf = create_visualization_pdf(outdir, repaired_all, semantic_results)
504
-
505
- if visualization_pdf and visualization_pdf.exists():
506
- stream_mgr.summary_stream(f"✓ Created visualization PDF: {visualization_pdf.name}")
507
- else:
508
- stream_mgr.summary_stream("⚠️ Could not create visualization PDF")
509
- visualization_pdf = None
510
-
511
- # 3. Create formula images
512
- stream_mgr.summary_stream("Rendering formula images...")
513
- formula_images = create_formula_images(outdir, repaired_all)
514
-
515
- if formula_images:
516
- # Create zip of formula images
517
- images_zip = outdir / "formula_images.zip"
518
- with zipfile.ZipFile(images_zip, 'w') as zf:
519
- for img_info in formula_images:
520
- img_path = Path(img_info['image_path'])
521
- if img_path.exists():
522
- zf.write(img_path, img_info['relative_path'])
523
-
524
- stream_mgr.summary_stream(f"✓ Created {len(formula_images)} formula images")
525
- else:
526
- images_zip = None
527
- stream_mgr.summary_stream("⚠️ No formula images created")
528
-
529
- # ============ STAGE 6: Generate Statistics ============
530
- stats = {
531
- "total_pages": len(merged),
532
- "total_formulas": total_formulas,
533
- "repaired_formulas": len(repaired_eqs),
534
- "semantic_evaluated": len(semantic_results),
535
- "output_directory": str(outdir),
536
- "processing_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
537
- "formula_images_count": len(formula_images) if formula_images else 0
538
- }
539
-
540
- stats_json = outdir / "processing_stats.json"
541
- stats_json.write_text(json.dumps(stats, indent=2), "utf-8")
542
-
543
- # Final summary
544
- stream_mgr.summary_stream("=" * 60)
545
- stream_mgr.summary_stream("🎉 PROCESSING COMPLETED SUCCESSFULLY!")
546
- stream_mgr.summary_stream("=" * 60)
547
- stream_mgr.summary_stream(f"📊 SUMMARY STATISTICS:")
548
- stream_mgr.summary_stream(f" • Pages processed: {len(merged)}")
549
- stream_mgr.summary_stream(f" • Formulas detected: {total_formulas}")
550
- stream_mgr.summary_stream(f" • Formulas repaired: {len(repaired_eqs)}")
551
- stream_mgr.summary_stream(f" • Formulas evaluated: {len(semantic_results)}")
552
- stream_mgr.summary_stream(f" • Formula images: {len(formula_images) if formula_images else 0}")
553
- stream_mgr.summary_stream(f" • Output directory: {outdir}")
554
- stream_mgr.summary_stream("=" * 60)
555
- stream_mgr.summary_stream("📁 Generated files:")
556
- stream_mgr.summary_stream(f" 1. {overall_json.name} - OCR results")
557
- stream_mgr.summary_stream(f" 2. {repaired_json.name} - Repaired formulas")
558
- stream_mgr.summary_stream(f" 3. {semantic_json.name} - Semantic evaluation")
559
- stream_mgr.summary_stream(f" 4. {merge_tex.name} - Combined LaTeX")
560
- if visualization_pdf:
561
- stream_mgr.summary_stream(f" 5. {visualization_pdf.name} - Visualization PDF")
562
- if images_zip:
563
- stream_mgr.summary_stream(f" 6. {images_zip.name} - Formula images")
564
- stream_mgr.summary_stream(f" 7. {stats_json.name} - Processing statistics")
565
- stream_mgr.summary_stream("=" * 60)
566
-
567
- return overall_json, repaired_json, semantic_json, merge_tex, visualization_pdf, images_zip, semantic_results
568
-
569
- except Exception as e:
570
- import traceback
571
- error_msg = f" CRITICAL ERROR:\n{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
572
- stream_mgr.summary_stream(error_msg)
573
- return None, None, None, None, None, None, []
574
 
575
  # ============================================================
576
- # Simplified Streaming Handler
577
  # ============================================================
578
  def process_with_streaming(pdf_file):
579
- """Simplified streaming handler without threading issues"""
580
- if not pdf_file:
581
- yield "", "", "", "❌ No PDF uploaded", "Error", None, None, None, None, None, None, []
582
- return
583
-
584
- # Initialize stream manager
585
  stream_mgr = StreamManager()
586
-
587
- # Initialize result variables
588
- result_container = {
589
- 'overall_json': None,
590
- 'repaired_json': None,
591
- 'semantic_json': None,
592
- 'merge_tex': None,
593
- 'visualization_pdf': None,
594
- 'images_zip': None,
595
- 'sem_results': []
596
- }
597
-
598
- # Process function
599
- def run_processing():
600
- """Run the pipeline and store results"""
601
- results = process_pipeline(pdf_file, stream_mgr)
602
- if results:
603
- result_container['overall_json'] = results[0]
604
- result_container['repaired_json'] = results[1]
605
- result_container['semantic_json'] = results[2]
606
- result_container['merge_tex'] = results[3]
607
- result_container['visualization_pdf'] = results[4]
608
- result_container['images_zip'] = results[5]
609
- result_container['sem_results'] = results[6]
610
-
611
- # Start processing in a thread
612
- process_thread = threading.Thread(target=run_processing)
613
- process_thread.start()
614
-
615
- # Initialize log contents
616
- ocr_log_content = ""
617
- repair_log_content = ""
618
- semantic_log_content = ""
619
- summary_log_content = ""
620
-
621
- # Stream updates while processing
622
- last_update = time.time()
623
- while process_thread.is_alive():
624
- # Collect new messages
625
- while not stream_mgr.ocr_queue.empty():
626
- ocr_log_content += stream_mgr.ocr_queue.get() + "\n"
627
-
628
- while not stream_mgr.repair_queue.empty():
629
- repair_log_content += stream_mgr.repair_queue.get() + "\n"
630
-
631
- while not stream_mgr.semantic_queue.empty():
632
- semantic_log_content += stream_mgr.semantic_queue.get() + "\n"
633
-
634
- while not stream_mgr.summary_queue.empty():
635
- summary_log_content += stream_mgr.summary_queue.get() + "\n"
636
-
637
- # Update UI periodically
638
- current_time = time.time()
639
- if current_time - last_update > 0.1:
640
- yield (
641
- ocr_log_content[-5000:], # Limit size
642
- repair_log_content[-5000:],
643
- semantic_log_content[-5000:],
644
- summary_log_content[-5000:],
645
- "Processing...",
646
- None, None, None, None, None, None, []
647
- )
648
- last_update = current_time
649
-
650
- time.sleep(0.05)
651
-
652
- # Wait for thread to complete
653
- process_thread.join()
654
-
655
- # Collect any remaining messages
656
- while not stream_mgr.ocr_queue.empty():
657
- ocr_log_content += stream_mgr.ocr_queue.get() + "\n"
658
-
659
- while not stream_mgr.repair_queue.empty():
660
- repair_log_content += stream_mgr.repair_queue.get() + "\n"
661
-
662
- while not stream_mgr.semantic_queue.empty():
663
- semantic_log_content += stream_mgr.semantic_queue.get() + "\n"
664
-
665
- while not stream_mgr.summary_queue.empty():
666
- summary_log_content += stream_mgr.summary_queue.get() + "\n"
667
-
668
- # Prepare download files
669
- ocr_file = str(result_container['overall_json']) if result_container['overall_json'] else None
670
- repair_file = str(result_container['repaired_json']) if result_container['repaired_json'] else None
671
- semantic_file = str(result_container['semantic_json']) if result_container['semantic_json'] else None
672
- tex_file = str(result_container['merge_tex']) if result_container['merge_tex'] else None
673
- pdf_file_dl = str(result_container['visualization_pdf']) if result_container['visualization_pdf'] else None
674
- images_file = str(result_container['images_zip']) if result_container['images_zip'] else None
675
-
676
- # Final update
677
  yield (
678
- ocr_log_content[-10000:],
679
- repair_log_content[-10000:],
680
- semantic_log_content[-10000:],
681
- summary_log_content[-10000:],
682
  "Completed",
683
- ocr_file,
684
- repair_file,
685
- semantic_file,
686
- tex_file,
687
- pdf_file_dl,
688
- images_file,
689
- result_container['sem_results']
690
  )
691
 
692
  # ============================================================
693
- # Gradio UI with Multi-Stream Display
694
  # ============================================================
695
- with gr.Blocks(title="PDF2TEX Formula", theme=gr.themes.Soft()) as demo:
696
- gr.Markdown("""
697
- # 📘 PDF2TEX Formula Pipeline
698
- **A Lightweight Pipeline for Extracting and Repairing Mathematical Formulas from Raw PDFs**
699
- """)
700
-
701
- # Input Section
702
- with gr.Row():
703
- with gr.Column(scale=1):
704
- gr.Markdown("### Upload PDF")
705
- pdf_input = gr.File(
706
- label="Upload PDF File",
707
- file_types=[".pdf"],
708
- height=100
709
- )
710
-
711
- with gr.Row():
712
- run_btn = gr.Button("Start Processing", variant="primary", size="lg")
713
- clear_btn = gr.Button("Clear All", variant="secondary")
714
-
715
- gr.Markdown("### Processing Status")
716
- status_text = gr.Textbox(
717
- label="Current Status",
718
- value="Ready",
719
- interactive=False
720
- )
721
-
722
- # Multi-Stream Display Section
723
- gr.Markdown("### 🎯 Real-time Processing Logs")
724
-
725
- with gr.Tabs():
726
- with gr.TabItem("📄 OCR Process"):
727
- ocr_log = gr.Textbox(
728
- label="OCR Stage Log",
729
- lines=15,
730
- interactive=False,
731
- autoscroll=True
732
- )
733
-
734
- with gr.TabItem("🔧 Repair Process"):
735
- repair_log = gr.Textbox(
736
- label="Repair Stage Log",
737
- lines=15,
738
- interactive=False,
739
- autoscroll=True
740
- )
741
-
742
- with gr.TabItem("🧠 Semantic Process"):
743
- semantic_log = gr.Textbox(
744
- label="Semantic Stage Log",
745
- lines=15,
746
- interactive=False,
747
- autoscroll=True
748
- )
749
-
750
- with gr.TabItem("📊 Summary"):
751
- summary_log = gr.Textbox(
752
- label="Processing Summary",
753
- lines=20,
754
- interactive=False,
755
- autoscroll=True
756
- )
757
-
758
- # Results Download Section
759
- gr.Markdown("### 📥 Download Results")
760
-
761
- with gr.Row():
762
- with gr.Column(scale=1):
763
- ocr_json_dl = gr.File(label="OCR Results (JSON)", visible=True)
764
- repaired_json_dl = gr.File(label="Repaired Formulas (JSON)", visible=True)
765
-
766
- with gr.Column(scale=1):
767
- semantic_json_dl = gr.File(label="Semantic Results (JSON)", visible=True)
768
- merge_tex_dl = gr.File(label="Combined LaTeX (.tex)", visible=True)
769
-
770
- with gr.Row():
771
- with gr.Column(scale=1):
772
- visualization_pdf_dl = gr.File(label="Visualization PDF", visible=True)
773
-
774
- with gr.Column(scale=1):
775
- formula_images_dl = gr.File(label="Formula Images (.zip)", visible=True)
776
-
777
- # Hidden state for semantic results
778
- semantic_state = gr.State([])
779
-
780
- # Formula Preview Section
781
- gr.Markdown("### 🔍 Formula Preview")
782
-
783
- with gr.Row():
784
- formula_idx = gr.Number(
785
- label="Formula Index",
786
- minimum=0,
787
- maximum=1000,
788
- step=1,
789
- precision=0,
790
- value=0
791
- )
792
- preview_btn = gr.Button("Preview Formula", variant="secondary")
793
-
794
- formula_preview = gr.Image(label="Formula Visualization", height=200)
795
- formula_info = gr.JSON(label="Formula Details")
796
-
797
- # Preview function
798
- def preview_formula(idx, sem_results):
799
- if not sem_results or idx < 0 or idx >= len(sem_results):
800
- return None, {}
801
-
802
- result = sem_results[int(idx)]
803
-
804
- # Try to find the formula image
805
- formula = result.get('original_formula', '')
806
- if formula:
807
- # Create temporary image
808
- temp_dir = Path(tempfile.mkdtemp())
809
- img_path = temp_dir / f"formula_{idx}.png"
810
-
811
- if render_formula_to_image(formula, img_path) and img_path.exists():
812
- return str(img_path), result
813
- else:
814
- # If rendering fails, return empty
815
- return None, result
816
-
817
- return None, result
818
-
819
- # Clear function
820
- def clear_all():
821
- return [
822
- None, # pdf_input
823
- "", # ocr_log
824
- "", # repair_log
825
- "", # semantic_log
826
- "", # summary_log
827
- "Ready", # status_text
828
- None, # ocr_json_dl
829
- None, # repaired_json_dl
830
- None, # semantic_json_dl
831
- None, # merge_tex_dl
832
- None, # visualization_pdf_dl
833
- None, # formula_images_dl
834
- [], # semantic_state
835
- 0, # formula_idx
836
- None, # formula_preview
837
- {} # formula_info
838
- ]
839
-
840
- # Connect events
841
- outputs = [
842
- ocr_log,
843
- repair_log,
844
- semantic_log,
845
- summary_log,
846
- status_text,
847
- ocr_json_dl,
848
- repaired_json_dl,
849
- semantic_json_dl,
850
- merge_tex_dl,
851
- visualization_pdf_dl,
852
- formula_images_dl,
853
- semantic_state
854
- ]
855
-
856
- run_btn.click(
857
- fn=process_with_streaming,
858
- inputs=[pdf_input],
859
- outputs=outputs
860
- )
861
-
862
- preview_btn.click(
863
- fn=preview_formula,
864
- inputs=[formula_idx, semantic_state],
865
- outputs=[formula_preview, formula_info]
866
- )
867
-
868
- clear_btn.click(
869
- fn=clear_all,
870
- outputs=outputs + [formula_idx, formula_preview, formula_info]
871
  )
872
 
873
- # ============================================================
874
- # Main Execution
875
- # ============================================================
876
  if __name__ == "__main__":
877
- print("=" * 60)
878
- print("PDF Formula Processing Pipeline - Visualization Version")
879
- print(f"Device: {DEVICE}")
880
- print("=" * 60)
881
-
882
- # Install required packages if missing
883
- try:
884
- import fpdf
885
- except ImportError:
886
- print("Installing fpdf for PDF generation...")
887
- import subprocess
888
- subprocess.run(["pip", "install", "fpdf"])
889
-
890
- demo.queue().launch(
891
- server_name="0.0.0.0",
892
- server_port=7860,
893
- share=False,
894
- debug=False,
895
- show_error=True
896
- )
 
9
  from queue import Queue
10
  import zipfile
11
  from datetime import datetime
12
+
13
  import matplotlib
14
+ matplotlib.use('Agg')
15
+ import matplotlib.pyplot as plt
 
16
 
17
  from detection_ocr import DetectionOCR
18
  from repair_head import RepairHead
 
34
  def get_ocr():
35
  global _ocr
36
  if _ocr is None:
 
37
  _ocr = DetectionOCR(device=DEVICE)
38
  return _ocr
39
 
40
  def get_repair():
41
  global _repair
42
  if _repair is None:
 
43
  _repair = RepairHead(device="cpu")
44
  return _repair
45
 
46
  def get_semantic():
47
  global _semantic
48
  if _semantic is None:
 
49
  _semantic = SemanticEvaluationPipeline(
50
  base_model="Qwen/Qwen2.5-Math-1.5B-Instruct",
51
  lora_dir=None,
 
62
  expr = expr.strip()
63
  if any(expr.startswith(f"\\begin{{{e}}}") for e in KNOWN_ENV):
64
  return expr
 
 
 
 
65
  return f"\\begin{{equation}}\n{expr}\n\\end{{equation}}"
66
 
67
  def is_reasonable(expr: str) -> bool:
 
69
  return False
70
  if expr.lower() in ["noise", "error"]:
71
  return False
72
+ return 3 <= len(expr) <= 2000
 
 
73
 
74
  def save_progress_log(outdir: Path, stage: str, content: str):
 
75
  log_file = outdir / f"{stage}_log.txt"
76
  with open(log_file, "a", encoding="utf-8") as f:
77
+ f.write(f"[{datetime.now()}] {content}\n")
78
+
79
+ # ============================================================
80
+ # Formula Rendering (FIXED: no usetex)
81
+ # ============================================================
82
+ def render_formula_to_image(formula: str, output_path: Path):
83
+ try:
84
+ fig, ax = plt.subplots(figsize=(8, 2))
85
+ ax.text(
86
+ 0.5, 0.5,
87
+ f"${formula}$", # mathtext
88
+ ha='center', va='center',
89
+ fontsize=16
90
+ )
91
+ ax.axis('off')
92
+ plt.tight_layout()
93
+ plt.savefig(output_path, dpi=150, bbox_inches='tight', pad_inches=0.1)
94
+ plt.close(fig)
95
+ return True
96
+ except Exception as e:
97
+ print("[Render Error]", e)
98
+ return False
99
 
100
+ # ============================================================
101
+ # Visualization PDF (ASCII SAFE)
102
+ # ============================================================
103
  def create_visualization_pdf(outdir: Path, repaired_data: dict, semantic_results: list):
 
104
  try:
105
  from fpdf import FPDF
106
+
 
 
 
 
 
107
  pdf = FPDF()
108
  pdf.set_auto_page_break(auto=True, margin=15)
 
 
109
  pdf.add_page()
110
+ pdf.set_font("Arial", size=14)
111
+ pdf.cell(0, 10, "PDF Formula Processing Results", ln=1, align="C")
112
+ pdf.ln(5)
113
+
114
+ def page_num(k):
115
+ m = re.search(r'page_(\d+)', k)
116
+ return int(m.group(1)) if m else 0
117
+
118
+ for page in sorted(repaired_data.keys(), key=page_num):
119
+ formulas = repaired_data[page]
120
+ valid = [f for f in formulas if f["repaired"] not in ["noise", "error"]]
121
+ if not valid:
 
 
 
 
122
  continue
123
+
124
  pdf.add_page()
125
+ pdf.set_font("Arial", "B", 12)
126
+ pdf.cell(0, 8, page, ln=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
 
128
+ pdf.set_font("Courier", size=8)
129
+ for i, f in enumerate(valid[:20]):
130
+ pdf.multi_cell(0, 4, f"[{i+1}] {f['repaired'][:200]}")
131
+ pdf.ln(1)
132
+
133
+ out = outdir / "visualization_report.pdf"
134
+ pdf.output(str(out))
135
+ return out
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  except Exception as e:
137
+ print("[PDF Error]", e)
138
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
  # ============================================================
141
+ # Stream Manager
142
  # ============================================================
143
  class StreamManager:
 
144
  def __init__(self):
145
  self.ocr_queue = Queue()
146
  self.repair_queue = Queue()
147
  self.semantic_queue = Queue()
148
  self.summary_queue = Queue()
149
+ self.current_outdir = None
150
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  def set_output_dir(self, outdir: Path):
152
  self.current_outdir = outdir
153
 
154
+ def _push(self, q: Queue, stage: str, msg: str):
155
+ q.put(msg)
156
+ if self.current_outdir:
157
+ save_progress_log(self.current_outdir, stage, msg)
158
+
159
+ def ocr_stream(self, msg): self._push(self.ocr_queue, "ocr", msg)
160
+ def repair_stream(self, msg): self._push(self.repair_queue, "repair", msg)
161
+ def semantic_stream(self, msg): self._push(self.semantic_queue, "semantic", msg)
162
+ def summary_stream(self, msg): self._push(self.summary_queue, "summary", msg)
163
+
164
  # ============================================================
165
+ # Main Pipeline
166
  # ============================================================
167
  def process_pipeline(pdf_file, stream_mgr: StreamManager):
168
+
169
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
170
+ outdir = Path(tempfile.mkdtemp(prefix=f"pdf_pipeline_{timestamp}_"))
171
+ stream_mgr.set_output_dir(outdir)
172
+
173
+ # ---------- OCR ----------
174
+ stream_mgr.ocr_stream("Starting OCR...")
175
+ ocr = get_ocr()
176
+ ocr.process_pdf(pdf_file, output_dir=outdir)
177
+
178
+ merged = {}
179
+ for p in sorted(outdir.glob("page_*_results.json")):
180
+ page = p.stem.replace("_results", "")
181
+ data = json.loads(p.read_text("utf-8"))
182
+ merged[page] = data.get("formulas", [])
183
+
184
+ # ---------- Repair ----------
185
+ stream_mgr.repair_stream("Starting Repair...")
186
+ repair = get_repair()
187
+
188
+ repaired_all = {}
189
+ repaired_eqs = []
190
+
191
+ for page, formulas in merged.items():
192
+ repaired_all[page] = []
193
+ for f in formulas:
194
+ raw = f.get("raw_latex", "")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
  try:
196
+ fixed = repair.repair(raw)
197
+ if is_reasonable(fixed):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  repaired_all[page].append({
199
  "raw_latex": raw,
200
+ "repaired": fixed
201
  })
202
+ repaired_eqs.append((raw, fixed))
203
+ else:
204
+ repaired_all[page].append({"raw_latex": raw, "repaired": "noise"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  except Exception as e:
206
+ repaired_all[page].append({"raw_latex": raw, "repaired": "error"})
207
+
208
+ # ---------- Semantic ----------
209
+ stream_mgr.semantic_stream("Starting Semantic Evaluation...")
210
+ semantic = get_semantic()
211
+
212
+ semantic_results = []
213
+ for raw, fixed in repaired_eqs:
214
+ try:
215
+ res = semantic.evaluate_formula(fixed)
216
+ res["raw_latex"] = raw # <<< FIX
217
+ res["repaired_latex"] = fixed # <<< FIX
218
+ semantic_results.append(res)
219
+ except Exception as e:
220
+ semantic_results.append({
221
+ "raw_latex": raw,
222
+ "repaired_latex": fixed,
223
+ "semantic_verdict": "error",
224
+ "confidence": 0.0
225
+ })
226
+
227
+ # ---------- Save ----------
228
+ (outdir / "repaired.json").write_text(json.dumps(repaired_all, indent=2), "utf-8")
229
+ (outdir / "semantic.json").write_text(json.dumps(semantic_results, indent=2), "utf-8")
230
+
231
+ tex = outdir / "merge.tex"
232
+ with tex.open("w", encoding="utf-8") as f:
233
+ f.write("\\documentclass{article}\n\\usepackage{amsmath}\n\\begin{document}\n")
234
+ for _, eq in repaired_eqs:
235
+ f.write(wrap_equation(eq) + "\n\n")
236
+ f.write("\\end{document}")
237
+
238
+ pdf = create_visualization_pdf(outdir, repaired_all, semantic_results)
239
+
240
+ return outdir, tex, pdf, semantic_results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
 
242
  # ============================================================
243
+ # Streaming Wrapper
244
  # ============================================================
245
  def process_with_streaming(pdf_file):
246
+
 
 
 
 
 
247
  stream_mgr = StreamManager()
248
+ result = {}
249
+
250
+ def runner():
251
+ out = process_pipeline(pdf_file, stream_mgr)
252
+ result["data"] = out
253
+
254
+ t = threading.Thread(target=runner)
255
+ t.start()
256
+
257
+ logs = {"ocr": "", "repair": "", "semantic": "", "summary": ""}
258
+
259
+ while t.is_alive():
260
+ for k, q in [
261
+ ("ocr", stream_mgr.ocr_queue),
262
+ ("repair", stream_mgr.repair_queue),
263
+ ("semantic", stream_mgr.semantic_queue),
264
+ ("summary", stream_mgr.summary_queue),
265
+ ]:
266
+ while not q.empty():
267
+ logs[k] += q.get() + "\n"
268
+
269
+ yield (
270
+ logs["ocr"][-5000:],
271
+ logs["repair"][-5000:],
272
+ logs["semantic"][-5000:],
273
+ logs["summary"][-5000:],
274
+ "Processing...",
275
+ None, None, None
276
+ )
277
+ time.sleep(0.1)
278
+
279
+ outdir, tex, pdf, sem = result["data"]
280
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  yield (
282
+ logs["ocr"],
283
+ logs["repair"],
284
+ logs["semantic"],
285
+ logs["summary"],
286
  "Completed",
287
+ str(tex),
288
+ str(pdf) if pdf else None,
289
+ sem
 
 
 
 
290
  )
291
 
292
  # ============================================================
293
+ # Gradio UI(基本不动)
294
  # ============================================================
295
+ with gr.Blocks(title="PDF2TEX Formula") as demo:
296
+ pdf_input = gr.File(file_types=[".pdf"])
297
+ run = gr.Button("Run")
298
+
299
+ ocr_log = gr.Textbox(lines=10)
300
+ repair_log = gr.Textbox(lines=10)
301
+ semantic_log = gr.Textbox(lines=10)
302
+ summary_log = gr.Textbox(lines=10)
303
+ status = gr.Textbox()
304
+
305
+ tex_dl = gr.File()
306
+ pdf_dl = gr.File()
307
+ sem_state = gr.State([])
308
+
309
+ run.click(
310
+ process_with_streaming,
311
+ pdf_input,
312
+ [ocr_log, repair_log, semantic_log, summary_log, status, tex_dl, pdf_dl, sem_state]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  )
314
 
 
 
 
315
  if __name__ == "__main__":
316
+ demo.queue().launch(server_name="0.0.0.0", server_port=7860)