Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -3,22 +3,19 @@ import tempfile
|
|
| 3 |
from pathlib import Path
|
| 4 |
import json
|
| 5 |
import torch
|
| 6 |
-
import re
|
| 7 |
-
from PIL import Image
|
| 8 |
|
| 9 |
from detection_ocr import DetectionOCR
|
| 10 |
from repair_head import RepairHead
|
| 11 |
from semantic_head import ExplanationLLM, FormulaEvaluator
|
| 12 |
|
| 13 |
-
# =========================
|
| 14 |
# Device
|
| 15 |
-
# =========================
|
| 16 |
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
| 17 |
-
print(f"[System] Using device: {DEVICE}")
|
| 18 |
|
| 19 |
-
# =========================
|
| 20 |
# Lazy singletons
|
| 21 |
-
# =========================
|
| 22 |
_ocr = None
|
| 23 |
_repair = None
|
| 24 |
_semantic = None
|
|
@@ -40,15 +37,15 @@ def get_semantic():
|
|
| 40 |
if _semantic is None:
|
| 41 |
explainer = ExplanationLLM(
|
| 42 |
base_model="Qwen/Qwen2.5-Math-1.5B-Instruct",
|
| 43 |
-
lora_dir=
|
| 44 |
device="cpu",
|
| 45 |
)
|
| 46 |
_semantic = FormulaEvaluator(explainer)
|
| 47 |
return _semantic
|
| 48 |
|
| 49 |
-
# =========================
|
| 50 |
# Helpers
|
| 51 |
-
# =========================
|
| 52 |
def is_reasonable_math(expr: str) -> bool:
|
| 53 |
if not expr:
|
| 54 |
return False
|
|
@@ -58,31 +55,17 @@ def is_reasonable_math(expr: str) -> bool:
|
|
| 58 |
return False
|
| 59 |
return True
|
| 60 |
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
def merge_bbox_images_to_pdf(output_dir: Path) -> Path:
|
| 65 |
-
images = sorted(output_dir.glob("page_*_bbox.png"))
|
| 66 |
-
if not images:
|
| 67 |
-
return None
|
| 68 |
-
|
| 69 |
-
pil_images = [Image.open(p).convert("RGB") for p in images]
|
| 70 |
-
pdf_path = output_dir / "merged_bbox.pdf"
|
| 71 |
-
pil_images[0].save(
|
| 72 |
-
pdf_path,
|
| 73 |
-
save_all=True,
|
| 74 |
-
append_images=pil_images[1:],
|
| 75 |
-
)
|
| 76 |
-
return pdf_path
|
| 77 |
-
|
| 78 |
-
# ============================================================
|
| 79 |
-
# Core pipeline
|
| 80 |
-
# ============================================================
|
| 81 |
def run_pipeline(pdf_file):
|
| 82 |
-
|
|
|
|
| 83 |
|
|
|
|
| 84 |
def log(msg):
|
| 85 |
logs.append(msg)
|
|
|
|
| 86 |
|
| 87 |
tmpdir = Path(tempfile.mkdtemp(prefix="pdf_ocr_"))
|
| 88 |
outdir = tmpdir / "output"
|
|
@@ -90,129 +73,123 @@ def run_pipeline(pdf_file):
|
|
| 90 |
|
| 91 |
log(f"π Output dir: {outdir}")
|
| 92 |
|
| 93 |
-
# --------
|
| 94 |
log("\nπ Stage 1: OCR")
|
| 95 |
ocr = get_ocr()
|
| 96 |
ocr.process_pdf(pdf_file, output_dir=outdir)
|
|
|
|
| 97 |
|
| 98 |
-
|
| 99 |
-
log(
|
| 100 |
-
|
| 101 |
-
# ---------------- Merge raw OCR ----------------
|
| 102 |
merged = {}
|
| 103 |
-
|
|
|
|
|
|
|
| 104 |
page = p.stem.replace("_results", "")
|
| 105 |
data = json.loads(p.read_text(encoding="utf-8"))
|
| 106 |
-
merged[page] =
|
| 107 |
-
|
| 108 |
-
"total": len(data.get("formulas", [])),
|
| 109 |
-
}
|
| 110 |
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
| 114 |
|
| 115 |
-
# --------
|
| 116 |
-
log("\nπ§ Stage
|
| 117 |
repair = get_repair()
|
| 118 |
|
| 119 |
repaired_all = {}
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
for page, content in merged.items():
|
| 123 |
repaired_all[page] = []
|
| 124 |
-
|
|
|
|
| 125 |
raw = fm.get("raw_latex", "")
|
|
|
|
| 126 |
repaired = repair.repair(raw)
|
| 127 |
-
|
| 128 |
if is_reasonable_math(repaired):
|
|
|
|
| 129 |
repaired_all[page].append({
|
| 130 |
"raw_latex": raw,
|
| 131 |
"repaired": repaired
|
| 132 |
})
|
| 133 |
-
merged_equations.append(repaired)
|
| 134 |
else:
|
|
|
|
| 135 |
repaired_all[page].append({
|
| 136 |
"raw_latex": raw,
|
| 137 |
"repaired": "noise"
|
| 138 |
})
|
| 139 |
|
| 140 |
repaired_json = outdir / "overall_ocr_repaired.json"
|
| 141 |
-
repaired_json.write_text(
|
| 142 |
-
|
|
|
|
|
|
|
| 143 |
|
| 144 |
-
# --------
|
| 145 |
-
log("\nπ§ Stage
|
| 146 |
-
|
| 147 |
|
| 148 |
semantic_results = []
|
| 149 |
for page, items in repaired_all.items():
|
| 150 |
-
for
|
| 151 |
-
if
|
| 152 |
continue
|
| 153 |
-
res =
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
})
|
| 158 |
|
| 159 |
semantic_json = outdir / "semantic_results.json"
|
| 160 |
-
semantic_json.write_text(
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
merge_tex = outdir / "merge.tex"
|
| 165 |
-
with merge_tex.open("w", encoding="utf-8") as f:
|
| 166 |
-
f.write("\\documentclass{article}\n")
|
| 167 |
-
f.write("\\usepackage{amsmath,amssymb}\n")
|
| 168 |
-
f.write("\\begin{document}\n\n")
|
| 169 |
-
for eq in merged_equations:
|
| 170 |
-
f.write(wrap_equation(eq) + "\n\n")
|
| 171 |
-
f.write("\\end{document}\n")
|
| 172 |
-
log("πΎ Saved merge.tex")
|
| 173 |
-
|
| 174 |
-
# ---------------- bbox pdf ----------------
|
| 175 |
-
bbox_pdf = merge_bbox_images_to_pdf(outdir)
|
| 176 |
-
if bbox_pdf:
|
| 177 |
-
log("πΎ Saved merged_bbox.pdf")
|
| 178 |
|
| 179 |
log("\nπ DONE")
|
| 180 |
|
| 181 |
return (
|
| 182 |
"\n".join(logs),
|
|
|
|
| 183 |
str(repaired_json),
|
| 184 |
str(semantic_json),
|
| 185 |
-
str(merge_tex),
|
| 186 |
-
str(bbox_pdf) if bbox_pdf else None,
|
| 187 |
)
|
| 188 |
|
| 189 |
-
# =========================
|
| 190 |
-
# Gradio UI
|
| 191 |
-
# =========================
|
| 192 |
with gr.Blocks() as demo:
|
| 193 |
-
gr.Markdown("# π PDF
|
| 194 |
|
| 195 |
pdf = gr.File(label="Upload PDF", file_types=[".pdf"])
|
| 196 |
run = gr.Button("Run")
|
| 197 |
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
|
| 204 |
with gr.Row():
|
| 205 |
-
|
| 206 |
-
|
|
|
|
| 207 |
|
| 208 |
run.click(
|
| 209 |
fn=run_pipeline,
|
| 210 |
inputs=pdf,
|
| 211 |
-
outputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
)
|
| 213 |
|
| 214 |
-
# ============================================================
|
| 215 |
-
# Main
|
| 216 |
-
# ============================================================
|
| 217 |
if __name__ == "__main__":
|
| 218 |
-
demo.launch(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from pathlib import Path
|
| 4 |
import json
|
| 5 |
import torch
|
|
|
|
|
|
|
| 6 |
|
| 7 |
from detection_ocr import DetectionOCR
|
| 8 |
from repair_head import RepairHead
|
| 9 |
from semantic_head import ExplanationLLM, FormulaEvaluator
|
| 10 |
|
| 11 |
+
# =========================
|
| 12 |
# Device
|
| 13 |
+
# =========================
|
| 14 |
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
|
|
|
|
| 15 |
|
| 16 |
+
# =========================
|
| 17 |
# Lazy singletons
|
| 18 |
+
# =========================
|
| 19 |
_ocr = None
|
| 20 |
_repair = None
|
| 21 |
_semantic = None
|
|
|
|
| 37 |
if _semantic is None:
|
| 38 |
explainer = ExplanationLLM(
|
| 39 |
base_model="Qwen/Qwen2.5-Math-1.5B-Instruct",
|
| 40 |
+
lora_dir="src/repair/repair_training/Repair_improved_1.5B_v1",
|
| 41 |
device="cpu",
|
| 42 |
)
|
| 43 |
_semantic = FormulaEvaluator(explainer)
|
| 44 |
return _semantic
|
| 45 |
|
| 46 |
+
# =========================
|
| 47 |
# Helpers
|
| 48 |
+
# =========================
|
| 49 |
def is_reasonable_math(expr: str) -> bool:
|
| 50 |
if not expr:
|
| 51 |
return False
|
|
|
|
| 55 |
return False
|
| 56 |
return True
|
| 57 |
|
| 58 |
+
# =========================
|
| 59 |
+
# Main pipeline (simple & reliable)
|
| 60 |
+
# =========================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
def run_pipeline(pdf_file):
|
| 62 |
+
if pdf_file is None:
|
| 63 |
+
return "No PDF uploaded.", None, None, None
|
| 64 |
|
| 65 |
+
logs = []
|
| 66 |
def log(msg):
|
| 67 |
logs.append(msg)
|
| 68 |
+
return "\n".join(logs)
|
| 69 |
|
| 70 |
tmpdir = Path(tempfile.mkdtemp(prefix="pdf_ocr_"))
|
| 71 |
outdir = tmpdir / "output"
|
|
|
|
| 73 |
|
| 74 |
log(f"π Output dir: {outdir}")
|
| 75 |
|
| 76 |
+
# -------- OCR --------
|
| 77 |
log("\nπ Stage 1: OCR")
|
| 78 |
ocr = get_ocr()
|
| 79 |
ocr.process_pdf(pdf_file, output_dir=outdir)
|
| 80 |
+
log("β
OCR done")
|
| 81 |
|
| 82 |
+
# -------- Merge OCR results --------
|
| 83 |
+
log("\nπ Stage 2: Merge OCR results")
|
|
|
|
|
|
|
| 84 |
merged = {}
|
| 85 |
+
page_files = sorted(outdir.glob("page_*_results.json"))
|
| 86 |
+
|
| 87 |
+
for p in page_files:
|
| 88 |
page = p.stem.replace("_results", "")
|
| 89 |
data = json.loads(p.read_text(encoding="utf-8"))
|
| 90 |
+
merged[page] = data["formulas"]
|
| 91 |
+
log(f" {page}: {len(data['formulas'])} formulas")
|
|
|
|
|
|
|
| 92 |
|
| 93 |
+
overall_ocr = outdir / "overall_ocr_results.json"
|
| 94 |
+
overall_ocr.write_text(
|
| 95 |
+
json.dumps(merged, indent=2, ensure_ascii=False),
|
| 96 |
+
encoding="utf-8",
|
| 97 |
+
)
|
| 98 |
|
| 99 |
+
# -------- Repair --------
|
| 100 |
+
log("\nπ§ Stage 3: Repair formulas")
|
| 101 |
repair = get_repair()
|
| 102 |
|
| 103 |
repaired_all = {}
|
| 104 |
+
for page, formulas in merged.items():
|
|
|
|
|
|
|
| 105 |
repaired_all[page] = []
|
| 106 |
+
log(f"\n==== {page} ====")
|
| 107 |
+
for i, fm in enumerate(formulas, 1):
|
| 108 |
raw = fm.get("raw_latex", "")
|
| 109 |
+
log(f"[{i}/{len(formulas)}] RAW: {raw[:80]}")
|
| 110 |
repaired = repair.repair(raw)
|
|
|
|
| 111 |
if is_reasonable_math(repaired):
|
| 112 |
+
log(" β repaired")
|
| 113 |
repaired_all[page].append({
|
| 114 |
"raw_latex": raw,
|
| 115 |
"repaired": repaired
|
| 116 |
})
|
|
|
|
| 117 |
else:
|
| 118 |
+
log(" β noise")
|
| 119 |
repaired_all[page].append({
|
| 120 |
"raw_latex": raw,
|
| 121 |
"repaired": "noise"
|
| 122 |
})
|
| 123 |
|
| 124 |
repaired_json = outdir / "overall_ocr_repaired.json"
|
| 125 |
+
repaired_json.write_text(
|
| 126 |
+
json.dumps(repaired_all, indent=2, ensure_ascii=False),
|
| 127 |
+
encoding="utf-8",
|
| 128 |
+
)
|
| 129 |
|
| 130 |
+
# -------- Semantic --------
|
| 131 |
+
log("\nπ§ Stage 4: Semantic evaluation")
|
| 132 |
+
semantic = get_semantic()
|
| 133 |
|
| 134 |
semantic_results = []
|
| 135 |
for page, items in repaired_all.items():
|
| 136 |
+
for r in items:
|
| 137 |
+
if r["repaired"] == "noise":
|
| 138 |
continue
|
| 139 |
+
res = semantic.evaluate(r["repaired"])
|
| 140 |
+
res["page"] = page
|
| 141 |
+
semantic_results.append(res)
|
| 142 |
+
log(f" [{page}] {res['semantic_verdict']} (conf={res['confidence']})")
|
|
|
|
| 143 |
|
| 144 |
semantic_json = outdir / "semantic_results.json"
|
| 145 |
+
semantic_json.write_text(
|
| 146 |
+
json.dumps(semantic_results, indent=2, ensure_ascii=False),
|
| 147 |
+
encoding="utf-8",
|
| 148 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
|
| 150 |
log("\nπ DONE")
|
| 151 |
|
| 152 |
return (
|
| 153 |
"\n".join(logs),
|
| 154 |
+
str(overall_ocr),
|
| 155 |
str(repaired_json),
|
| 156 |
str(semantic_json),
|
|
|
|
|
|
|
| 157 |
)
|
| 158 |
|
| 159 |
+
# =========================
|
| 160 |
+
# Gradio UI (simple, stable)
|
| 161 |
+
# =========================
|
| 162 |
with gr.Blocks() as demo:
|
| 163 |
+
gr.Markdown("# π PDF β OCR β Repair β Semantic (Simple)")
|
| 164 |
|
| 165 |
pdf = gr.File(label="Upload PDF", file_types=[".pdf"])
|
| 166 |
run = gr.Button("Run")
|
| 167 |
|
| 168 |
+
live_log = gr.Textbox(
|
| 169 |
+
label="Live Log",
|
| 170 |
+
lines=22,
|
| 171 |
+
interactive=False
|
| 172 |
+
)
|
| 173 |
|
| 174 |
with gr.Row():
|
| 175 |
+
ocr_json = gr.File(label="overall_ocr_results.json")
|
| 176 |
+
repaired_json = gr.File(label="overall_ocr_repaired.json")
|
| 177 |
+
semantic_json = gr.File(label="semantic_results.json")
|
| 178 |
|
| 179 |
run.click(
|
| 180 |
fn=run_pipeline,
|
| 181 |
inputs=pdf,
|
| 182 |
+
outputs=[
|
| 183 |
+
live_log,
|
| 184 |
+
ocr_json,
|
| 185 |
+
repaired_json,
|
| 186 |
+
semantic_json,
|
| 187 |
+
],
|
| 188 |
)
|
| 189 |
|
|
|
|
|
|
|
|
|
|
| 190 |
if __name__ == "__main__":
|
| 191 |
+
demo.launch(
|
| 192 |
+
server_name="0.0.0.0",
|
| 193 |
+
server_port=7860,
|
| 194 |
+
share=False,
|
| 195 |
+
)
|