Files
allenai--olmocr/olmocr/bench/review_app_latex.py
T
wehub-resource-sync 917eedffcf
Main / Python 3.11 - Docs (push) Waiting to run
Main / Python 3.11 - Build (push) Waiting to run
Main / Python 3.11 - Lint (push) Waiting to run
Main / Python 3.11 - Style (push) Waiting to run
Main / Python 3.11 - Test (push) Waiting to run
Main / GPU CI (push) Blocked by required conditions
Main / Release (push) Blocked by required conditions
Main / Build and Push Docker Images (push) Blocked by required conditions
chore: import upstream snapshot with attribution
2026-07-13 13:27:09 +08:00

717 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
import argparse
import json
import os
import shutil
import sys
import tempfile
from collections import defaultdict
from typing import Dict, List, Optional, Tuple
from flask import Flask, jsonify, redirect, render_template, request, send_file, url_for
app = Flask(__name__)
# Global state
DATASET_DIR = ""
DATASET_FILE = None
CURRENT_PDF = None
PDF_TESTS = {}
ALL_PDFS = []
FORCE = False
def find_next_unchecked_pdf() -> Optional[str]:
"""Find the next PDF with at least one unchecked test."""
global PDF_TESTS, ALL_PDFS
for pdf_name in ALL_PDFS:
pdf_tests = PDF_TESTS[pdf_name]
for test in pdf_tests:
if test.get("checked") is None:
return pdf_name
return None
def calculate_stats() -> dict:
"""Calculate statistics for all tests in the dataset."""
global PDF_TESTS
total_tests = 0
null_status = 0
verified_status = 0
rejected_status = 0
for pdf_tests in PDF_TESTS.values():
total_tests += len(pdf_tests)
for test in pdf_tests:
status = test.get("checked")
if status is None:
null_status += 1
elif status == "verified":
verified_status += 1
elif status == "rejected":
rejected_status += 1
completion = 0
if total_tests > 0:
completion = (verified_status + rejected_status) / total_tests * 100
return {"total": total_tests, "null": null_status, "verified": verified_status, "rejected": rejected_status, "completion": completion}
def save_dataset(jsonl_file: str) -> None:
"""Save the tests to a JSONL file, using temp file for atomic write."""
global PDF_TESTS
# Flatten all tests
all_tests = []
for pdf_tests in PDF_TESTS.values():
all_tests.extend(pdf_tests)
# Create temp file and write updated content
with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp_file:
for test in all_tests:
temp_file.write(json.dumps(test) + "\n")
# Atomic replace
shutil.move(temp_file.name, jsonl_file)
@app.route("/pdf/<path:pdf_name>")
def serve_pdf(pdf_name):
"""Serve the PDF file directly."""
pdf_path = os.path.join(DATASET_DIR, "pdfs", pdf_name)
return send_file(pdf_path, mimetype="application/pdf")
@app.route("/")
def index():
"""Main page displaying the current PDF and its tests."""
global CURRENT_PDF, PDF_TESTS, DATASET_DIR, ALL_PDFS, FORCE
# If no current PDF is set, find the next one with unchecked tests
if CURRENT_PDF is None:
CURRENT_PDF = find_next_unchecked_pdf()
# If still no PDF, either show the "All done" page or force display the first PDF
if CURRENT_PDF is None:
if FORCE and ALL_PDFS:
CURRENT_PDF = ALL_PDFS[0]
else:
return render_template("all_done_latex.html")
# Get the tests for the current PDF
current_tests = PDF_TESTS.get(CURRENT_PDF, [])
# Create PDF URL for pdf.js to load
pdf_url = url_for("serve_pdf", pdf_name=CURRENT_PDF)
# Calculate statistics
stats = calculate_stats()
return render_template(
"review_latex.html",
pdf_name=CURRENT_PDF,
tests=current_tests,
pdf_path=pdf_url,
pdf_index=ALL_PDFS.index(CURRENT_PDF) if CURRENT_PDF in ALL_PDFS else 0,
total_pdfs=len(ALL_PDFS),
stats=stats,
)
@app.route("/update_test", methods=["POST"])
def update_test():
"""API endpoint to update a test."""
global PDF_TESTS, DATASET_DIR, DATASET_FILE
data = request.json
pdf_name = data.get("pdf")
test_id = data.get("id")
field = data.get("field")
value = data.get("value")
# Find and update the test
for test in PDF_TESTS.get(pdf_name, []):
if test.get("id") == test_id:
test[field] = value
break
# Save the updated tests
save_dataset(DATASET_FILE)
return jsonify({"status": "success"})
@app.route("/reject_all", methods=["POST"])
def reject_all():
"""API endpoint to reject all tests for a PDF."""
global PDF_TESTS, DATASET_DIR, DATASET_FILE
data = request.json
pdf_name = data.get("pdf")
if pdf_name and pdf_name in PDF_TESTS:
# Update all tests for this PDF to rejected
for test in PDF_TESTS[pdf_name]:
test["checked"] = "rejected"
# Save the updated tests
save_dataset(DATASET_FILE)
return jsonify({"status": "success", "count": len(PDF_TESTS[pdf_name])})
return jsonify({"status": "error", "message": "PDF not found"})
@app.route("/next_pdf", methods=["POST"])
def next_pdf():
"""Move to the next PDF in the list."""
global CURRENT_PDF, ALL_PDFS, FORCE
if CURRENT_PDF in ALL_PDFS:
current_index = ALL_PDFS.index(CURRENT_PDF)
if current_index < len(ALL_PDFS) - 1:
CURRENT_PDF = ALL_PDFS[current_index + 1]
else:
# If in force mode, cycle back to the beginning instead of checking for an unchecked PDF
if FORCE and ALL_PDFS:
CURRENT_PDF = ALL_PDFS[0]
else:
CURRENT_PDF = find_next_unchecked_pdf()
else:
if FORCE and ALL_PDFS:
CURRENT_PDF = ALL_PDFS[0]
else:
CURRENT_PDF = find_next_unchecked_pdf()
return redirect(url_for("index"))
@app.route("/prev_pdf", methods=["POST"])
def prev_pdf():
"""Move to the previous PDF in the list."""
global CURRENT_PDF, ALL_PDFS
if CURRENT_PDF in ALL_PDFS:
current_index = ALL_PDFS.index(CURRENT_PDF)
if current_index > 0:
CURRENT_PDF = ALL_PDFS[current_index - 1]
return redirect(url_for("index"))
@app.route("/goto_pdf/<int:index>", methods=["POST"])
def goto_pdf(index):
"""Go to a specific PDF by index."""
global CURRENT_PDF, ALL_PDFS
if 0 <= index < len(ALL_PDFS):
CURRENT_PDF = ALL_PDFS[index]
return redirect(url_for("index"))
def load_dataset(dataset_file: str) -> Tuple[Dict[str, List[Dict]], List[str]]:
"""Load tests from the dataset file and organize them by PDF."""
if not os.path.exists(dataset_file):
raise FileNotFoundError(f"Dataset file not found: {dataset_file}")
pdf_tests = defaultdict(list)
with open(dataset_file, "r") as f:
for line in f:
line = line.strip()
if not line:
continue
try:
test = json.loads(line)
pdf_name = test.get("pdf")
if pdf_name:
pdf_tests[pdf_name].append(test)
except json.JSONDecodeError:
print(f"Warning: Could not parse line as JSON: {line}")
all_pdfs = list(pdf_tests.keys())
return pdf_tests, all_pdfs
def create_templates_directory():
"""Create templates directory for Flask if it doesn't exist."""
templates_dir = os.path.join(os.path.dirname(__file__), "templates")
os.makedirs(templates_dir, exist_ok=True)
# Create the review_latex.html template with MathJax support
review_html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- You can adjust the viewport settings as needed -->
<meta name="viewport" content="width=1200, initial-scale=1.0">
<title>Equation Verification</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.2/es5/tex-mml-chtml.js"></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
height: 100vh;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background-color: #f0f0f0;
border-bottom: 1px solid #ddd;
}
.content {
display: flex;
flex: 1;
overflow: hidden;
}
.pdf-viewer {
flex: 2; /* Increased from 1 to 2 to make PDF larger */
border-right: 1px solid #ddd;
overflow: hidden;
position: relative;
}
/* Updated PDF container size */
#pdf-container {
width: 200%; /* New fixed width */
height: 200%; /* New fixed height */
overflow: auto;
}
#zoom-controls {
position: absolute;
top: 10px;
right: 10px;
z-index: 100;
background-color: white;
padding: 5px;
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
#zoom-controls button {
margin: 0 5px;
padding: 5px 10px;
cursor: pointer;
}
.tests-panel {
width: 1000px;
overflow-y: auto;
padding: 10px;
}
.test-item {
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
margin-bottom: 10px;
transition: background-color 0.2s;
}
.test-item.verified {
background-color: #d4edda;
}
.test-item.rejected {
background-color: #f8d7da;
}
/* The equation-display now stores the raw LaTeX in a data attribute */
.equation-display {
padding: 10px;
margin: 5px 0;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
overflow-x: auto;
font-size: 1.2em; /* Larger font for equations */
}
.button-group {
display: flex;
justify-content: space-between;
margin-top: 10px;
}
.button-group button {
padding: 5px 10px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.verify-button {
background-color: #28a745;
color: white;
}
.reject-button {
background-color: #dc3545;
color: white;
}
.edit-button {
background-color: #007bff;
color: white;
}
.navigation {
display: flex;
align-items: center;
}
.status {
margin-left: 20px;
}
.progress-bar {
height: 10px;
background-color: #e9ecef;
border-radius: 5px;
margin-top: 5px;
overflow: hidden;
}
.progress {
height: 100%;
background-color: #007bff;
width: 0%;
}
/* Make MathJax equations more visible */
.MathJax {
font-size: 120% !important;
}
</style>
</head>
<body>
<div class="header">
<div>
<h2>PDF: {{ pdf_name }}</h2>
<form method="post" action="/reject_all" id="reject-all-form" style="display:inline;">
<button type="button" onclick="rejectAll('{{ pdf_name }}')">Reject All Equations</button>
</form>
</div>
<div class="navigation">
<form method="post" action="/prev_pdf" style="display:inline;">
<button type="submit" {% if pdf_index == 0 %}disabled{% endif %}>Previous</button>
</form>
<span style="margin: 0 10px;">{{ pdf_index + 1 }} / {{ total_pdfs }}</span>
<form method="post" action="/next_pdf" style="display:inline;">
<button type="submit">Next</button>
</form>
<div class="status">
<div>Completion: {{ "%.1f"|format(stats.completion) }}%</div>
<div class="progress-bar">
<div class="progress" style="width: {{ stats.completion }}%;"></div>
</div>
</div>
</div>
</div>
<div class="content">
<div class="pdf-viewer">
<div id="zoom-controls">
<button onclick="changeZoom(0.2)">+</button>
<button onclick="changeZoom(-0.2)">-</button>
<button onclick="resetZoom()">Reset</button>
</div>
<div id="pdf-container"></div>
</div>
<div class="tests-panel">
<h3>Equations ({{ tests|length }})</h3>
{% for test in tests %}
<!-- Added data-latex attribute to store raw LaTeX -->
<div class="test-item {% if test.checked == 'verified' %}verified{% elif test.checked == 'rejected' %}rejected{% endif %}" id="test-{{ test.id }}">
<div class="equation-display" data-latex="{{ test.text|e }}">
{{ test.text|safe }}
</div>
<div class="button-group">
<button class="verify-button" onclick="updateTest('{{ test.id }}', '{{ test.pdf }}', 'checked', 'verified')">Verify</button>
<button class="reject-button" onclick="updateTest('{{ test.id }}', '{{ test.pdf }}', 'checked', 'rejected')">Reject</button>
<!-- New Edit button -->
<button class="edit-button" onclick="enableEdit('{{ test.id }}', '{{ test.pdf }}')">Edit</button>
</div>
</div>
{% endfor %}
</div>
</div>
<script>
// Set up PDF.js
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.worker.min.js';
// Track current zoom level
let currentScale = 2.0; // Initial larger scale
let pdfDoc = null;
let pageNum = 1;
let canvas = null;
// Load the PDF
const loadingTask = pdfjsLib.getDocument('{{ pdf_path }}');
loadingTask.promise.then(function(pdf) {
pdfDoc = pdf;
renderPage(pageNum);
});
// Function to render a page with the current scale
function renderPage(num) {
pdfDoc.getPage(num).then(function(page) {
const viewport = page.getViewport({ scale: currentScale });
if (!canvas) {
canvas = document.createElement('canvas');
document.getElementById('pdf-container').appendChild(canvas);
}
const context = canvas.getContext('2d');
canvas.height = viewport.height;
canvas.width = viewport.width;
const renderContext = {
canvasContext: context,
viewport: viewport
};
page.render(renderContext);
});
}
// Function to change zoom level
function changeZoom(delta) {
currentScale += delta;
if (currentScale < 0.5) currentScale = 0.5;
if (currentScale > 5) currentScale = 5;
renderPage(pageNum);
}
// Function to reset zoom
function resetZoom() {
currentScale = 2.0;
renderPage(pageNum);
}
// Function to update a test used by both verify/reject and edit
function updateTest(testId, pdfName, field, value) {
fetch('/update_test', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: testId,
pdf: pdfName,
field: field,
value: value
})
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
const testElement = document.getElementById(`test-${testId}`);
testElement.classList.remove('verified', 'rejected');
// Only update the class if the field updated is "checked".
if (field === 'checked') {
testElement.classList.add(value);
}
}
});
}
// New function to enable editing the LaTeX equation
function enableEdit(testId, pdfName) {
let testElement = document.getElementById("test-" + testId);
let equationDisplay = testElement.querySelector(".equation-display");
// Retrieve the raw LaTeX from the data attribute
let rawLatex = equationDisplay.getAttribute('data-latex');
// Save the current rendered HTML in case of cancellation
let originalHTML = equationDisplay.innerHTML;
// Create a textarea for editing the LaTeX
let textarea = document.createElement("textarea");
textarea.id = "edit-input-" + testId;
// Use the stored raw LaTeX if available; otherwise, fallback to textContent
textarea.value = rawLatex ? rawLatex : equationDisplay.textContent.trim();
textarea.style.width = "100%";
textarea.rows = 3;
// Create Save button to commit changes
let saveButton = document.createElement("button");
saveButton.innerText = "Save";
saveButton.onclick = function() {
let newText = textarea.value;
// Update the test via AJAX updating the 'text' field
updateTest(testId, pdfName, "text", newText);
// Update the data-latex attribute to hold the new raw LaTeX code
equationDisplay.setAttribute('data-latex', newText);
// Replace the display content with the wrapped LaTeX for MathJax to process
equationDisplay.innerHTML = '$$' + newText + '$$';
if (typeof MathJax !== 'undefined') {
MathJax.typeset();
}
// Clean up the temporary editing elements
textarea.remove();
saveButton.remove();
cancelButton.remove();
};
// Create Cancel button to revert changes
let cancelButton = document.createElement("button");
cancelButton.innerText = "Cancel";
cancelButton.onclick = function() {
equationDisplay.innerHTML = originalHTML;
textarea.remove();
saveButton.remove();
cancelButton.remove();
};
// Show the editing interface: clear the display and insert the textarea
equationDisplay.innerHTML = "";
equationDisplay.appendChild(textarea);
// Append buttons to the test element (or you can choose to append them elsewhere)
testElement.appendChild(saveButton);
testElement.appendChild(cancelButton);
}
// Function to reject all tests for a PDF
function rejectAll(pdfName) {
if (confirm('Are you sure you want to reject all equations for this PDF?')) {
fetch('/reject_all', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
pdf: pdfName
})
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
const testElements = document.querySelectorAll('.test-item');
testElements.forEach(element => {
element.classList.remove('verified');
element.classList.add('rejected');
});
}
});
}
}
// Process LaTeX equations on page load: wrap plain text with $$ and trigger MathJax typesetting
document.addEventListener('DOMContentLoaded', function() {
const equationDisplays = document.querySelectorAll('.equation-display');
equationDisplays.forEach(display => {
let equation = display.textContent.trim();
if (!equation.startsWith('$$')) {
display.innerHTML = '$$' + equation + '$$';
}
});
if (typeof MathJax !== 'undefined') {
MathJax.typeset();
}
});
</script>
</body>
</html>
"""
# Create the all_done_latex.html template
all_done_html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>All Done!</title>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f5f5f5;
}
.container {
text-align: center;
padding: 30px;
background-color: white;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
h1 {
color: #28a745;
margin-bottom: 20px;
}
p {
font-size: 18px;
margin-bottom: 20px;
}
button {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
</style>
</head>
<body>
<div class="container">
<h1>All Done! 🎉</h1>
<p>You have reviewed all equations in the dataset.</p>
<form method="post" action="/next_pdf">
<button type="submit">Start Over</button>
</form>
</div>
</body>
</html>
"""
with open(os.path.join(templates_dir, "review_latex.html"), "w") as f:
f.write(review_html)
with open(os.path.join(templates_dir, "all_done_latex.html"), "w") as f:
f.write(all_done_html)
def main():
"""Main entry point with command-line arguments."""
global DATASET_DIR, DATASET_FILE, PDF_TESTS, ALL_PDFS, CURRENT_PDF, FORCE
parser = argparse.ArgumentParser(description="Interactive Test Review App")
parser.add_argument("dataset_file", help="Path to the dataset jsonl file")
parser.add_argument("--port", type=int, default=5000, help="Port for the Flask app")
parser.add_argument("--host", default="127.0.0.1", help="Host for the Flask app")
parser.add_argument("--debug", action="store_true", help="Run Flask in debug mode")
parser.add_argument("--force", action="store_true", help="Force show each file one by one and never do the 'All done' page")
args = parser.parse_args()
FORCE = args.force
if not os.path.exists(args.dataset_file):
print(f"Error: Dataset not found: {args.dataset_file}")
return 1
DATASET_DIR = os.path.dirname(os.path.abspath(args.dataset_file))
DATASET_FILE = args.dataset_file
pdf_dir = os.path.join(DATASET_DIR, "pdfs")
if not os.path.isdir(pdf_dir):
print(f"Error: PDF directory not found: {pdf_dir}")
return 1
try:
PDF_TESTS, ALL_PDFS = load_dataset(args.dataset_file)
except Exception as e:
print(f"Error loading dataset: {str(e)}")
return 1
create_templates_directory()
CURRENT_PDF = find_next_unchecked_pdf()
print(f"Starting server at http://{args.host}:{args.port}")
app.run(host=args.host, port=args.port, debug=args.debug)
return 0
if __name__ == "__main__":
sys.exit(main())