Spaces:
Running
Running
Download app.py from Baiyinyou/PDF2TEX_Formula: direct link, hf CLI and curl.
- Browser
- Download file 34.4 kB
-
https://huggingface.co/spaces/Baiyinyou/PDF2TEX_Formula/resolve/main/app.py
- Command line
-
hf download hf://spaces/Baiyinyou/PDF2TEX_Formula/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Baiyinyou/PDF2TEX_Formula/resolve/main/app.py
34.4 kB
| import gradio as gr | |
| import tempfile | |
| from pathlib import Path | |
| import json | |
| import torch | |
| import time | |
| import re | |
| import threading | |
| from queue import Queue | |
| import zipfile | |
| from datetime import datetime | |
| from detection_ocr import DetectionOCR | |
| from repair_head import RepairHead | |
| from semantic_evaluation import SemanticEvaluationPipeline | |
| # ============================================================ | |
| # Device Configuration | |
| # ============================================================ | |
| DEVICE = "cuda" if torch.cuda.is_available() else "cpu" | |
| print(f"[System] Using device: {DEVICE}") | |
| # ============================================================ | |
| # Lazy Singletons | |
| # ============================================================ | |
| _ocr = None | |
| _repair = None | |
| _semantic = None | |
| def get_ocr(): | |
| global _ocr | |
| if _ocr is None: | |
| print("[INIT] Loading DetectionOCR") | |
| _ocr = DetectionOCR(device=DEVICE) | |
| return _ocr | |
| def get_repair(): | |
| global _repair | |
| if _repair is None: | |
| print("[INIT] Loading RepairHead (CPU)") | |
| _repair = RepairHead(device="cpu") | |
| return _repair | |
| def get_semantic(): | |
| global _semantic | |
| if _semantic is None: | |
| print("[INIT] Loading Semantic Evaluation Pipeline") | |
| _semantic = SemanticEvaluationPipeline( | |
| base_model="Qwen/Qwen2.5-Math-1.5B-Instruct", | |
| lora_dir=None, | |
| device="cpu", | |
| ) | |
| return _semantic | |
| # ============================================================ | |
| # Helpers | |
| # ============================================================ | |
| KNOWN_ENV = ["equation", "align", "gather", "multline", "array", "cases"] | |
| def wrap_equation(expr: str) -> str: | |
| expr = expr.strip() | |
| if any(expr.startswith(f"\\begin{{{e}}}") for e in KNOWN_ENV): | |
| return expr | |
| if expr.startswith("$$") and expr.endswith("$$"): | |
| return expr | |
| if expr.startswith("\\[") and expr.endswith("\\]"): | |
| return expr | |
| return f"\\begin{{equation}}\n{expr}\n\\end{{equation}}" | |
| def is_reasonable(expr: str) -> bool: | |
| if not expr or not isinstance(expr, str): | |
| return False | |
| if expr.lower() in ["noise", "error"]: | |
| return False | |
| if len(expr) < 3 or len(expr) > 2000: | |
| return False | |
| return True | |
| def save_progress_log(outdir: Path, stage: str, content: str): | |
| """Save progress logs to files""" | |
| log_file = outdir / f"{stage}_log.txt" | |
| with open(log_file, "a", encoding="utf-8") as f: | |
| timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| f.write(f"[{timestamp}] {content}\n") | |
| def create_visualization_pdf(outdir: Path, repaired_data: dict, semantic_results: list): | |
| """创建可视化PDF,按页面顺序显示公式""" | |
| try: | |
| from fpdf import FPDF | |
| pdf = FPDF() | |
| pdf.set_auto_page_break(auto=True, margin=15) | |
| # 添加标题页 | |
| pdf.add_page() | |
| pdf.set_font('Arial', 'B', 16) | |
| pdf.cell(0, 10, 'PDF Formula Processing Results', 0, 1, 'C') | |
| pdf.ln(10) | |
| pdf.set_font('Arial', '', 12) | |
| pdf.cell(0, 10, f'Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}', 0, 1, 'C') | |
| pdf.ln(20) | |
| # 统计信息 | |
| total_pages = len(repaired_data) | |
| total_formulas = sum(len(page_data) for page_data in repaired_data.values()) | |
| valid_formulas = sum(len([f for f in page_data if f['repaired'] not in ['noise', 'error']]) | |
| for page_data in repaired_data.values()) | |
| pdf.set_font('Arial', 'B', 14) | |
| pdf.cell(0, 10, 'Processing Statistics', 0, 1, 'L') | |
| pdf.ln(5) | |
| pdf.set_font('Arial', '', 12) | |
| pdf.cell(0, 8, f'• Total Pages: {total_pages}', 0, 1, 'L') | |
| pdf.cell(0, 8, f'• Total Formulas Detected: {total_formulas}', 0, 1, 'L') | |
| pdf.cell(0, 8, f'• Valid Formulas Repaired: {valid_formulas}', 0, 1, 'L') | |
| pdf.cell(0, 8, f'• Semantic Evaluations: {len(semantic_results)}', 0, 1, 'L') | |
| pdf.ln(10) | |
| # 按页面处理 | |
| 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) | |
| for page_key in page_keys: | |
| formulas = repaired_data[page_key] | |
| valid_formulas = [f for f in formulas if f['repaired'] not in ['noise', 'error']] | |
| if not valid_formulas: | |
| continue | |
| pdf.add_page() | |
| pdf.set_font('Arial', 'B', 14) | |
| pdf.cell(0, 10, f'{page_key.upper()}', 0, 1, 'L') | |
| pdf.set_font('Arial', '', 10) | |
| pdf.cell(0, 6, f'Valid Formulas: {len(valid_formulas)} / {len(formulas)}', 0, 1, 'L') | |
| pdf.ln(5) | |
| for idx, formula_data in enumerate(valid_formulas): | |
| formula = formula_data['repaired'] | |
| original = formula_data['raw_latex'][:150] + "..." if len(formula_data['raw_latex']) > 150 else formula_data['raw_latex'] | |
| # 公式标题 | |
| pdf.set_font('Arial', 'B', 10) | |
| pdf.cell(0, 6, f'Formula {idx+1}:', 0, 1, 'L') | |
| # 原始公式 | |
| pdf.set_font('Arial', 'I', 8) | |
| pdf.cell(20, 4, 'Original:', 0, 0, 'L') | |
| pdf.set_font('Courier', '', 8) | |
| pdf.multi_cell(0, 4, f' {original}', 0, 'L') | |
| # 修复后公式 | |
| pdf.set_font('Arial', 'I', 8) | |
| pdf.cell(20, 4, 'Repaired:', 0, 0, 'L') | |
| pdf.set_font('Courier', '', 8) | |
| formula_display = formula[:150] + "..." if len(formula) > 150 else formula | |
| pdf.multi_cell(0, 4, f' {formula_display}', 0, 'L') | |
| # 查找对应的语义结果 | |
| sem_result = None | |
| for sem in semantic_results: | |
| if sem.get('original_formula') == formula_data['raw_latex'] or sem.get('fixed_formula') == formula: | |
| sem_result = sem | |
| break | |
| if sem_result: | |
| verdict = sem_result.get('semantic_verdict', 'unknown') | |
| confidence = sem_result.get('confidence', 0) | |
| pdf.set_font('Arial', '', 9) | |
| verdict_color = { | |
| 'equivalent': (0, 100, 0), # 绿色 | |
| 'contradiction': (220, 0, 0), # 红色 | |
| 'non_verifiable': (255, 140, 0), # 橙色 | |
| 'garbled': (128, 128, 128), # 灰色 | |
| }.get(verdict, (0, 0, 0)) # 黑色默认 | |
| pdf.set_text_color(*verdict_color) | |
| pdf.cell(0, 5, f'Verdict: {verdict} (Confidence: {confidence:.2f})', 0, 1, 'L') | |
| pdf.set_text_color(0, 0, 0) # 重置为黑色 | |
| if sem_result.get('explanation'): | |
| pdf.set_font('Arial', 'I', 8) | |
| explanation = sem_result['explanation'][:200] + "..." if len(sem_result['explanation']) > 200 else sem_result['explanation'] | |
| pdf.multi_cell(0, 4, f'Explanation: {explanation}', 0, 'L') | |
| pdf.ln(3) | |
| # 每5个公式后添加分隔线 | |
| if (idx + 1) % 5 == 0 and idx + 1 < len(valid_formulas): | |
| pdf.line(10, pdf.get_y(), 200, pdf.get_y()) | |
| pdf.ln(5) | |
| # 保存PDF | |
| pdf_path = outdir / "visualization_report.pdf" | |
| pdf.output(str(pdf_path)) | |
| return pdf_path | |
| except Exception as e: | |
| print(f"Error creating visualization PDF: {e}") | |
| # 如果fpdf不可用,创建简单的文本报告 | |
| try: | |
| report_path = outdir / "visualization_report.txt" | |
| with open(report_path, 'w', encoding='utf-8') as f: | |
| f.write("=" * 60 + "\n") | |
| f.write("PDF FORMULA PROCESSING RESULTS\n") | |
| f.write("=" * 60 + "\n") | |
| f.write(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n") | |
| # 统计信息 | |
| total_pages = len(repaired_data) | |
| total_formulas = sum(len(page_data) for page_data in repaired_data.values()) | |
| valid_formulas = sum(len([f for f in page_data if f['repaired'] not in ['noise', 'error']]) | |
| for page_data in repaired_data.values()) | |
| f.write("PROCESSING STATISTICS:\n") | |
| f.write(f"• Total Pages: {total_pages}\n") | |
| f.write(f"• Total Formulas Detected: {total_formulas}\n") | |
| f.write(f"• Valid Formulas Repaired: {valid_formulas}\n") | |
| f.write(f"• Semantic Evaluations: {len(semantic_results)}\n\n") | |
| f.write("=" * 60 + "\n\n") | |
| # 按页面显示公式 | |
| 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) | |
| for page_key in page_keys: | |
| formulas = repaired_data[page_key] | |
| valid_formulas_list = [f for f in formulas if f['repaired'] not in ['noise', 'error']] | |
| if not valid_formulas_list: | |
| continue | |
| f.write(f"\n{page_key.upper()}\n") | |
| f.write("-" * 40 + "\n") | |
| f.write(f"Valid Formulas: {len(valid_formulas_list)} / {len(formulas)}\n\n") | |
| for idx, formula_data in enumerate(valid_formulas_list): | |
| formula = formula_data['repaired'] | |
| original = formula_data['raw_latex'] | |
| f.write(f"Formula {idx+1}:\n") | |
| f.write(f" Original: {original}\n") | |
| f.write(f" Repaired: {formula}\n") | |
| # 查找语义结果 | |
| sem_result = None | |
| for sem in semantic_results: | |
| if sem.get('original_formula') == original or sem.get('fixed_formula') == formula: | |
| sem_result = sem | |
| break | |
| if sem_result: | |
| verdict = sem_result.get('semantic_verdict', 'unknown') | |
| confidence = sem_result.get('confidence', 0) | |
| f.write(f" Verdict: {verdict} (Confidence: {confidence:.2f})\n") | |
| if sem_result.get('explanation'): | |
| explanation = sem_result['explanation'] | |
| f.write(f" Explanation: {explanation}\n") | |
| f.write("\n") | |
| return report_path | |
| except Exception as e2: | |
| print(f"Fallback text report also failed: {e2}") | |
| return None | |
| # ============================================================ | |
| # Stream Manager for Real-time Updates | |
| # ============================================================ | |
| class StreamManager: | |
| """Manage real-time streaming to multiple outputs""" | |
| def __init__(self): | |
| self.ocr_queue = Queue() | |
| self.repair_queue = Queue() | |
| self.semantic_queue = Queue() | |
| self.summary_queue = Queue() | |
| def ocr_stream(self, message: str): | |
| self.ocr_queue.put(message) | |
| if hasattr(self, 'current_outdir'): | |
| save_progress_log(self.current_outdir, "ocr", message) | |
| def repair_stream(self, message: str): | |
| self.repair_queue.put(message) | |
| if hasattr(self, 'current_outdir'): | |
| save_progress_log(self.current_outdir, "repair", message) | |
| def semantic_stream(self, message: str): | |
| self.semantic_queue.put(message) | |
| if hasattr(self, 'current_outdir'): | |
| save_progress_log(self.current_outdir, "semantic", message) | |
| def summary_stream(self, message: str): | |
| self.summary_queue.put(message) | |
| if hasattr(self, 'current_outdir'): | |
| save_progress_log(self.current_outdir, "summary", message) | |
| def set_output_dir(self, outdir: Path): | |
| self.current_outdir = outdir | |
| # ============================================================ | |
| # Main Processing Pipeline | |
| # ============================================================ | |
| def process_pipeline(pdf_file, stream_mgr: StreamManager): | |
| """Main processing pipeline with streaming to different outputs""" | |
| if pdf_file is None: | |
| stream_mgr.summary_stream("❌ No PDF uploaded.") | |
| return None, None, None, None, None, [] | |
| try: | |
| # Create output directory | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| tmpdir = Path(tempfile.mkdtemp(prefix=f"pdf_pipeline_{timestamp}_")) | |
| outdir = tmpdir / "output" | |
| outdir.mkdir(exist_ok=True) | |
| stream_mgr.set_output_dir(outdir) | |
| stream_mgr.summary_stream(f"📁 Output directory: {outdir}") | |
| stream_mgr.summary_stream("=" * 60) | |
| # ============ STAGE 1: OCR ============ | |
| stream_mgr.ocr_stream("🚀 STAGE 1: OCR Detection Started") | |
| stream_mgr.ocr_stream("Loading OCR model...") | |
| ocr = get_ocr() | |
| stream_mgr.ocr_stream("Processing PDF pages...") | |
| ocr.process_pdf(pdf_file, output_dir=outdir) | |
| stream_mgr.ocr_stream("✅ OCR Processing Completed") | |
| stream_mgr.summary_stream("✓ Stage 1: OCR completed") | |
| # ============ STAGE 2: Merge Results ============ | |
| stream_mgr.ocr_stream("\n🔄 Merging page results...") | |
| merged = {} | |
| page_files = sorted(outdir.glob("page_*_results.json")) | |
| if not page_files: | |
| stream_mgr.ocr_stream("❌ No page results found!") | |
| stream_mgr.summary_stream("❌ Error: No page results found") | |
| return None, None, None, None, None, [] | |
| for p in page_files: | |
| m = re.search(r"(page_\d+)_results\.json", p.name) | |
| if not m: | |
| continue | |
| page = m.group(1) | |
| try: | |
| data = json.loads(p.read_text("utf-8")) | |
| formulas = data.get("formulas", []) | |
| merged[page] = { | |
| "formulas": formulas, | |
| "total": len(formulas) | |
| } | |
| stream_mgr.ocr_stream(f" {page}: {len(formulas)} formulas") | |
| except Exception as e: | |
| stream_mgr.ocr_stream(f" ⚠️ Error loading {page}: {str(e)}") | |
| merged[page] = {"formulas": [], "total": 0} | |
| # Save overall OCR results | |
| overall_json = outdir / "overall_ocr_results.json" | |
| overall_json.write_text(json.dumps(merged, indent=2), "utf-8") | |
| stream_mgr.ocr_stream(f"✅ Saved: {overall_json.name}") | |
| stream_mgr.summary_stream(f"✓ Stage 2: Merged {len(merged)} pages") | |
| # ============ STAGE 3: Repair Formulas ============ | |
| stream_mgr.repair_stream("🔧 STAGE 3: LaTeX Repair Started") | |
| stream_mgr.repair_stream("Loading Repair model...") | |
| repair = get_repair() | |
| repaired_all = {} | |
| repaired_eqs = [] | |
| total_formulas = sum(v["total"] for v in merged.values()) | |
| stream_mgr.repair_stream(f"Total formulas to repair: {total_formulas}") | |
| stream_mgr.repair_stream("=" * 40) | |
| processed = 0 | |
| for page, content in merged.items(): | |
| stream_mgr.repair_stream(f"\n📄 Processing {page}:") | |
| repaired_all[page] = [] | |
| for i, fm in enumerate(content["formulas"]): | |
| processed += 1 | |
| raw = fm.get("raw_latex", "") | |
| # Show progress every 5 formulas or first/last | |
| if processed <= 5 or processed >= total_formulas - 5 or processed % 10 == 0: | |
| raw_preview = raw[:100] + "..." if len(raw) > 100 else raw | |
| stream_mgr.repair_stream(f" [{processed}/{total_formulas}] {raw_preview}") | |
| try: | |
| repaired = repair.repair(raw) | |
| if is_reasonable(repaired): | |
| repaired_all[page].append({ | |
| "raw_latex": raw, | |
| "repaired": repaired | |
| }) | |
| repaired_eqs.append(repaired) | |
| if processed <= 3: # Show first few repairs | |
| stream_mgr.repair_stream(f" → {repaired[:80]}...") | |
| else: | |
| repaired_all[page].append({ | |
| "raw_latex": raw, | |
| "repaired": "noise" | |
| }) | |
| except Exception as e: | |
| repaired_all[page].append({ | |
| "raw_latex": raw, | |
| "repaired": f"error: {str(e)}" | |
| }) | |
| stream_mgr.repair_stream(f" ⚠️ Error: {str(e)}") | |
| # Save repaired results | |
| repaired_json = outdir / "overall_ocr_repaired.json" | |
| repaired_json.write_text(json.dumps(repaired_all, indent=2), "utf-8") | |
| stream_mgr.repair_stream(f"\n✅ Repair completed: {len(repaired_eqs)} valid formulas") | |
| stream_mgr.summary_stream(f"✓ Stage 3: Repaired {len(repaired_eqs)}/{total_formulas} formulas") | |
| # ============ STAGE 4: Semantic Evaluation ============ | |
| stream_mgr.semantic_stream("🧠 STAGE 4: Semantic Evaluation Started") | |
| stream_mgr.semantic_stream("Loading Semantic model...") | |
| semantic = get_semantic() | |
| semantic_results = [] | |
| stream_mgr.semantic_stream(f"Evaluating {len(repaired_eqs)} formulas...") | |
| stream_mgr.semantic_stream("=" * 40) | |
| for i, eq in enumerate(repaired_eqs): | |
| try: | |
| # Show progress | |
| if i < 3 or i >= len(repaired_eqs) - 3 or i % 5 == 0: | |
| eq_preview = eq[:80] + "..." if len(eq) > 80 else eq | |
| stream_mgr.semantic_stream(f"[{i+1}/{len(repaired_eqs)}] {eq_preview}") | |
| res = semantic.evaluate_formula(eq) | |
| res['original_formula'] = eq # 添加原始公式 | |
| semantic_results.append(res) | |
| # Show verdict for first few | |
| if i < 5: | |
| stream_mgr.semantic_stream(f" Verdict: {res.get('semantic_verdict', 'unknown')}") | |
| stream_mgr.semantic_stream(f" Confidence: {res.get('confidence', 0):.2f}") | |
| except Exception as e: | |
| stream_mgr.semantic_stream(f"⚠️ Error evaluating formula {i+1}: {str(e)}") | |
| semantic_results.append({ | |
| 'original_formula': eq, | |
| 'error': str(e), | |
| 'semantic_verdict': 'evaluation_error', | |
| 'confidence': 0.0 | |
| }) | |
| # Save semantic results | |
| semantic_json = outdir / "semantic_results.json" | |
| semantic_json.write_text(json.dumps(semantic_results, indent=2), "utf-8") | |
| stream_mgr.semantic_stream(f"\n✅ Semantic evaluation completed: {len(semantic_results)} formulas") | |
| stream_mgr.summary_stream(f"✓ Stage 4: Semantically evaluated {len(semantic_results)} formulas") | |
| # ============ STAGE 5: Generate Visualization PDF ============ | |
| stream_mgr.summary_stream("\n📊 Generating visualization PDF...") | |
| # Generate LaTeX file | |
| merge_tex = outdir / "merge.tex" | |
| with merge_tex.open("w", encoding="utf-8") as f: | |
| f.write("\\documentclass{article}\n") | |
| f.write("\\usepackage{amsmath, amssymb, bm}\n") | |
| f.write("\\begin{document}\n\n") | |
| f.write("% Auto-generated from PDF formula extraction pipeline\n") | |
| f.write("% Generated on: " + datetime.now().strftime("%Y-%m-%d %H:%M:%S") + "\n\n") | |
| for eq in repaired_eqs: | |
| f.write(wrap_equation(eq) + "\n\n") | |
| f.write("\\end{document}\n") | |
| stream_mgr.summary_stream("✓ Generated LaTeX file") | |
| # Create visualization PDF | |
| visualization_pdf = create_visualization_pdf(outdir, repaired_all, semantic_results) | |
| if visualization_pdf and visualization_pdf.exists(): | |
| stream_mgr.summary_stream(f"✓ Created visualization report: {visualization_pdf.name}") | |
| else: | |
| stream_mgr.summary_stream("⚠️ Could not create visualization PDF") | |
| visualization_pdf = None | |
| # ============ STAGE 6: Generate Statistics ============ | |
| stats = { | |
| "total_pages": len(merged), | |
| "total_formulas": total_formulas, | |
| "repaired_formulas": len(repaired_eqs), | |
| "semantic_evaluated": len(semantic_results), | |
| "output_directory": str(outdir), | |
| "processing_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), | |
| } | |
| stats_json = outdir / "processing_stats.json" | |
| stats_json.write_text(json.dumps(stats, indent=2), "utf-8") | |
| # Final summary | |
| stream_mgr.summary_stream("=" * 60) | |
| stream_mgr.summary_stream("🎉 PROCESSING COMPLETED SUCCESSFULLY!") | |
| stream_mgr.summary_stream("=" * 60) | |
| stream_mgr.summary_stream(f"📊 SUMMARY STATISTICS:") | |
| stream_mgr.summary_stream(f" • Pages processed: {len(merged)}") | |
| stream_mgr.summary_stream(f" • Formulas detected: {total_formulas}") | |
| stream_mgr.summary_stream(f" • Formulas repaired: {len(repaired_eqs)}") | |
| stream_mgr.summary_stream(f" • Formulas evaluated: {len(semantic_results)}") | |
| stream_mgr.summary_stream(f" • Output directory: {outdir}") | |
| stream_mgr.summary_stream("=" * 60) | |
| stream_mgr.summary_stream("📁 Generated files:") | |
| stream_mgr.summary_stream(f" 1. {overall_json.name} - OCR results") | |
| stream_mgr.summary_stream(f" 2. {repaired_json.name} - Repaired formulas") | |
| stream_mgr.summary_stream(f" 3. {semantic_json.name} - Semantic evaluation") | |
| stream_mgr.summary_stream(f" 4. {merge_tex.name} - Combined LaTeX") | |
| if visualization_pdf: | |
| stream_mgr.summary_stream(f" 5. {visualization_pdf.name} - Visualization report") | |
| stream_mgr.summary_stream(f" 6. {stats_json.name} - Processing statistics") | |
| stream_mgr.summary_stream("=" * 60) | |
| return overall_json, repaired_json, semantic_json, merge_tex, visualization_pdf, semantic_results | |
| except Exception as e: | |
| import traceback | |
| error_msg = f"❌ CRITICAL ERROR:\n{str(e)}\n\nTraceback:\n{traceback.format_exc()}" | |
| stream_mgr.summary_stream(error_msg) | |
| return None, None, None, None, None, [] | |
| # ============================================================ | |
| # Simplified Streaming Handler | |
| # ============================================================ | |
| def process_with_streaming(pdf_file): | |
| """Simplified streaming handler without threading issues""" | |
| if not pdf_file: | |
| yield "", "", "", "❌ No PDF uploaded", "Error", None, None, None, None, None, [] | |
| return | |
| # Initialize stream manager | |
| stream_mgr = StreamManager() | |
| # Initialize result variables | |
| result_container = { | |
| 'overall_json': None, | |
| 'repaired_json': None, | |
| 'semantic_json': None, | |
| 'merge_tex': None, | |
| 'visualization_pdf': None, | |
| 'sem_results': [] | |
| } | |
| # Process function | |
| def run_processing(): | |
| """Run the pipeline and store results""" | |
| results = process_pipeline(pdf_file, stream_mgr) | |
| if results: | |
| result_container['overall_json'] = results[0] | |
| result_container['repaired_json'] = results[1] | |
| result_container['semantic_json'] = results[2] | |
| result_container['merge_tex'] = results[3] | |
| result_container['visualization_pdf'] = results[4] | |
| result_container['sem_results'] = results[5] | |
| # Start processing in a thread | |
| process_thread = threading.Thread(target=run_processing) | |
| process_thread.start() | |
| # Initialize log contents | |
| ocr_log_content = "" | |
| repair_log_content = "" | |
| semantic_log_content = "" | |
| summary_log_content = "" | |
| # Stream updates while processing | |
| last_update = time.time() | |
| while process_thread.is_alive(): | |
| # Collect new messages | |
| while not stream_mgr.ocr_queue.empty(): | |
| ocr_log_content += stream_mgr.ocr_queue.get() + "\n" | |
| while not stream_mgr.repair_queue.empty(): | |
| repair_log_content += stream_mgr.repair_queue.get() + "\n" | |
| while not stream_mgr.semantic_queue.empty(): | |
| semantic_log_content += stream_mgr.semantic_queue.get() + "\n" | |
| while not stream_mgr.summary_queue.empty(): | |
| summary_log_content += stream_mgr.summary_queue.get() + "\n" | |
| # Update UI periodically | |
| current_time = time.time() | |
| if current_time - last_update > 0.1: | |
| yield ( | |
| ocr_log_content[-5000:], # Limit size | |
| repair_log_content[-5000:], | |
| semantic_log_content[-5000:], | |
| summary_log_content[-5000:], | |
| "Processing...", | |
| None, None, None, None, None, [] | |
| ) | |
| last_update = current_time | |
| time.sleep(0.05) | |
| # Wait for thread to complete | |
| process_thread.join() | |
| # Collect any remaining messages | |
| while not stream_mgr.ocr_queue.empty(): | |
| ocr_log_content += stream_mgr.ocr_queue.get() + "\n" | |
| while not stream_mgr.repair_queue.empty(): | |
| repair_log_content += stream_mgr.repair_queue.get() + "\n" | |
| while not stream_mgr.semantic_queue.empty(): | |
| semantic_log_content += stream_mgr.semantic_queue.get() + "\n" | |
| while not stream_mgr.summary_queue.empty(): | |
| summary_log_content += stream_mgr.summary_queue.get() + "\n" | |
| # Prepare download files | |
| ocr_file = str(result_container['overall_json']) if result_container['overall_json'] else None | |
| repair_file = str(result_container['repaired_json']) if result_container['repaired_json'] else None | |
| semantic_file = str(result_container['semantic_json']) if result_container['semantic_json'] else None | |
| tex_file = str(result_container['merge_tex']) if result_container['merge_tex'] else None | |
| pdf_file_dl = str(result_container['visualization_pdf']) if result_container['visualization_pdf'] else None | |
| # Final update | |
| yield ( | |
| ocr_log_content[-10000:], | |
| repair_log_content[-10000:], | |
| semantic_log_content[-10000:], | |
| summary_log_content[-10000:], | |
| "Completed", | |
| ocr_file, | |
| repair_file, | |
| semantic_file, | |
| tex_file, | |
| pdf_file_dl, | |
| result_container['sem_results'] | |
| ) | |
| # ============================================================ | |
| # Gradio UI with Multi-Stream Display | |
| # ============================================================ | |
| with gr.Blocks(title="PDF Formula Pipeline", theme=gr.themes.Soft()) as demo: | |
| gr.Markdown(""" | |
| # 📘 PDF Formula Processing Pipeline | |
| **Real-time streaming display with visualization report** | |
| """) | |
| # Input Section | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Upload PDF") | |
| pdf_input = gr.File( | |
| label="Upload PDF File", | |
| file_types=[".pdf"], | |
| height=100 | |
| ) | |
| with gr.Row(): | |
| run_btn = gr.Button("Start Processing", variant="primary", size="lg") | |
| clear_btn = gr.Button("Clear All", variant="secondary") | |
| gr.Markdown("### Processing Status") | |
| status_text = gr.Textbox( | |
| label="Current Status", | |
| value="Ready", | |
| interactive=False | |
| ) | |
| # Multi-Stream Display Section | |
| gr.Markdown("### 🎯 Real-time Processing Logs") | |
| with gr.Tabs(): | |
| with gr.TabItem("📄 OCR Process"): | |
| ocr_log = gr.Textbox( | |
| label="OCR Stage Log", | |
| lines=15, | |
| interactive=False, | |
| autoscroll=True | |
| ) | |
| with gr.TabItem("🔧 Repair Process"): | |
| repair_log = gr.Textbox( | |
| label="Repair Stage Log", | |
| lines=15, | |
| interactive=False, | |
| autoscroll=True | |
| ) | |
| with gr.TabItem("🧠 Semantic Process"): | |
| semantic_log = gr.Textbox( | |
| label="Semantic Stage Log", | |
| lines=15, | |
| interactive=False, | |
| autoscroll=True | |
| ) | |
| with gr.TabItem("📊 Summary"): | |
| summary_log = gr.Textbox( | |
| label="Processing Summary", | |
| lines=20, | |
| interactive=False, | |
| autoscroll=True | |
| ) | |
| # Results Download Section | |
| gr.Markdown("### 📥 Download Results") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| ocr_json_dl = gr.File(label="OCR Results (JSON)", visible=True) | |
| repaired_json_dl = gr.File(label="Repaired Formulas (JSON)", visible=True) | |
| with gr.Column(scale=1): | |
| semantic_json_dl = gr.File(label="Semantic Results (JSON)", visible=True) | |
| merge_tex_dl = gr.File(label="Combined LaTeX (.tex)", visible=True) | |
| with gr.Row(): | |
| visualization_pdf_dl = gr.File(label="Visualization Report", visible=True) | |
| # Formula Details Section | |
| gr.Markdown("### 🔍 Formula Details") | |
| with gr.Row(): | |
| formula_idx = gr.Number( | |
| label="Formula Index (0 to preview)", | |
| minimum=0, | |
| maximum=1000, | |
| step=1, | |
| precision=0, | |
| value=0 | |
| ) | |
| preview_btn = gr.Button("Show Details", variant="secondary") | |
| formula_details = gr.JSON( | |
| label="Formula Details", | |
| visible=True | |
| ) | |
| # Hidden state for semantic results | |
| semantic_state = gr.State([]) | |
| # Preview function | |
| def show_formula_details(idx, sem_results): | |
| if not sem_results or idx < 0 or idx >= len(sem_results): | |
| return {} | |
| result = sem_results[int(idx)] | |
| # 格式化输出 | |
| formatted_result = { | |
| "Formula": result.get('original_formula', 'N/A'), | |
| "Semantic Verdict": result.get('semantic_verdict', 'unknown'), | |
| "Confidence": f"{result.get('confidence', 0):.2f}" if isinstance(result.get('confidence'), (int, float)) else "N/A", | |
| "Source": result.get('source', 'N/A'), | |
| "Syntactically Correct": result.get('syntactically_correct', 'N/A'), | |
| "Mathematically Correct": result.get('mathematically_correct', 'N/A'), | |
| } | |
| if result.get('explanation'): | |
| formatted_result["Explanation"] = result['explanation'] | |
| if result.get('suggested_fix'): | |
| formatted_result["Suggested Fix"] = result['suggested_fix'] | |
| if result.get('error'): | |
| formatted_result["Error"] = result['error'] | |
| return formatted_result | |
| # Clear function | |
| def clear_all(): | |
| return [ | |
| None, # pdf_input | |
| "", # ocr_log | |
| "", # repair_log | |
| "", # semantic_log | |
| "", # summary_log | |
| "Ready", # status_text | |
| None, # ocr_json_dl | |
| None, # repaired_json_dl | |
| None, # semantic_json_dl | |
| None, # merge_tex_dl | |
| None, # visualization_pdf_dl | |
| [], # semantic_state | |
| 0, # formula_idx | |
| {} # formula_details | |
| ] | |
| # Connect events | |
| outputs = [ | |
| ocr_log, | |
| repair_log, | |
| semantic_log, | |
| summary_log, | |
| status_text, | |
| ocr_json_dl, | |
| repaired_json_dl, | |
| semantic_json_dl, | |
| merge_tex_dl, | |
| visualization_pdf_dl, | |
| semantic_state | |
| ] | |
| run_btn.click( | |
| fn=process_with_streaming, | |
| inputs=[pdf_input], | |
| outputs=outputs | |
| ) | |
| preview_btn.click( | |
| fn=show_formula_details, | |
| inputs=[formula_idx, semantic_state], | |
| outputs=[formula_details] | |
| ) | |
| formula_idx.change( | |
| fn=show_formula_details, | |
| inputs=[formula_idx, semantic_state], | |
| outputs=[formula_details] | |
| ) | |
| clear_btn.click( | |
| fn=clear_all, | |
| outputs=outputs + [formula_idx, formula_details] | |
| ) | |
| # ============================================================ | |
| # Main Execution | |
| # ============================================================ | |
| if __name__ == "__main__": | |
| print("=" * 60) | |
| print("PDF Formula Processing Pipeline - Visualization Version") | |
| print(f"Device: {DEVICE}") | |
| print("=" * 60) | |
| # 尝试导入fpdf,如果失败则使用文本报告 | |
| try: | |
| from fpdf import FPDF | |
| print("✓ fpdf is available for PDF generation") | |
| except ImportError: | |
| print("⚠️ fpdf not available, will use text report instead") | |
| print(" To install: pip install fpdf") | |
| demo.queue().launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=False, | |
| debug=False, | |
| show_error=True | |
| ) |