From dcb7badea611c3f2a62eeb822a81f360e20140db Mon Sep 17 00:00:00 2001 From: Tom Main Date: Tue, 28 Jul 2026 19:44:32 -0400 Subject: [PATCH 1/3] Make OCR CI failures observable --- .github/workflows/ci.yml | 20 ++++++++++++++++---- .github/workflows/deploy.yml | 6 +++--- benchmark/eval/ocr_form_quality.py | 3 +-- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 949998b..af90d5f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: name: Native parser gates runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup Zig uses: mlugg/setup-zig@v2 @@ -39,7 +39,7 @@ jobs: name: Build WASM runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup Zig uses: mlugg/setup-zig@v2 @@ -53,10 +53,14 @@ jobs: name: Adaptive OCR quality gate runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Install OCR and rasterization tools - run: sudo apt-get update && sudo apt-get install -y poppler-utils tesseract-ocr + run: | + sudo apt-get update + sudo apt-get install -y poppler-utils tesseract-ocr + tesseract --version + pdftoppm -v - name: Setup Zig uses: mlugg/setup-zig@v2 @@ -68,3 +72,11 @@ jobs: - name: Enforce scanned-form text and numeric floors run: python3 benchmark/eval/ocr_form_quality.py --output /tmp/ocr-form-quality.json + + - name: Upload OCR quality report + if: always() + uses: actions/upload-artifact@v6 + with: + name: ocr-form-quality + path: /tmp/ocr-form-quality.json + if-no-files-found: ignore diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index ad01660..6b692a7 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -18,7 +18,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Setup Zig uses: mlugg/setup-zig@v2 @@ -35,7 +35,7 @@ jobs: cp zig-out/bin/pdf_parser.wasm deploy/ - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v5 with: path: deploy @@ -48,4 +48,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 diff --git a/benchmark/eval/ocr_form_quality.py b/benchmark/eval/ocr_form_quality.py index f68290d..02abf8e 100644 --- a/benchmark/eval/ocr_form_quality.py +++ b/benchmark/eval/ocr_form_quality.py @@ -171,8 +171,7 @@ def main() -> int: rendered = json.dumps(report, indent=2, sort_keys=True) + "\n" if args.output: Path(args.output).write_text(rendered, encoding="utf-8") - else: - print(rendered, end="") + print(rendered, end="") return 0 if report["status"] == "pass" else 1 From cfc1dc3eab0ec5c06766bcbf9f1ce4d58a1d92cd Mon Sep 17 00:00:00 2001 From: Tom Main Date: Tue, 28 Jul 2026 19:51:13 -0400 Subject: [PATCH 2/3] Include recognized OCR rows in quality reports --- benchmark/eval/ocr_form_quality.py | 1 + 1 file changed, 1 insertion(+) diff --git a/benchmark/eval/ocr_form_quality.py b/benchmark/eval/ocr_form_quality.py index 02abf8e..01d3fd7 100644 --- a/benchmark/eval/ocr_form_quality.py +++ b/benchmark/eval/ocr_form_quality.py @@ -156,6 +156,7 @@ def evaluate(artifacts: list[dict[str, Any]], truth: dict[str, Any]) -> dict[str "failures": failures, "expected_row_count": len(expected_rows), "actual_row_count": len(actual_rows), + "actual_rows": actual_rows, "attempts": attempts, } From be0f9ba5cbda7b4332143a445308f274bc159eec Mon Sep 17 00:00:00 2001 From: Tom Main Date: Tue, 28 Jul 2026 20:07:54 -0400 Subject: [PATCH 3/3] Normalize OCR date separator noise --- benchmark/eval/ocr_form_quality.py | 14 +++++++++++++- benchmark/eval/test_benchmark_hygiene.py | 5 +++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/benchmark/eval/ocr_form_quality.py b/benchmark/eval/ocr_form_quality.py index 01d3fd7..2e48443 100644 --- a/benchmark/eval/ocr_form_quality.py +++ b/benchmark/eval/ocr_form_quality.py @@ -77,6 +77,18 @@ def exact_recall(expected: list[str], actual: list[str]) -> float: return matched / len(expected) +def normalize_ocr_date(value: str) -> str: + """Normalize separator glyph loss while preserving all recognized date digits.""" + digits = re.sub(r"\D", "", value) + if len(digits) != 8: + return value.strip() + month = int(digits[:2]) + day = int(digits[2:4]) + if not (1 <= month <= 12 and 1 <= day <= 31): + return value.strip() + return f"{digits[:2]}/{digits[2:4]}/{digits[4:]}" + + def table_rows(artifacts: list[dict[str, Any]]) -> list[list[str]]: tables = [record for record in artifacts if record.get("record_type") == "table"] if not tables: @@ -106,7 +118,7 @@ def evaluate(artifacts: list[dict[str, Any]], truth: dict[str, Any]) -> dict[str expected_dates = [str(row["date"]) for row in expected_rows] expected_vendors = [str(row["vendor"]) for row in expected_rows] expected_amounts = [str(row["amount"]) for row in expected_rows] - actual_dates = [row[0] for row in actual_rows if len(row) >= 3] + actual_dates = [normalize_ocr_date(row[0]) for row in actual_rows if len(row) >= 3] actual_vendors = [row[1] for row in actual_rows if len(row) >= 3] actual_amounts = [row[2] for row in actual_rows if len(row) >= 3] diff --git a/benchmark/eval/test_benchmark_hygiene.py b/benchmark/eval/test_benchmark_hygiene.py index 2824474..130da03 100644 --- a/benchmark/eval/test_benchmark_hygiene.py +++ b/benchmark/eval/test_benchmark_hygiene.py @@ -99,6 +99,11 @@ def test_ocr_form_quality_enforces_absolute_numeric_and_row_floors(self) -> None self.assertEqual("fail", failing["status"]) self.assertIn("numeric_exact_match", [item["metric"] for item in failing["failures"]]) + def test_ocr_form_quality_normalizes_separator_glyph_loss_in_dates(self) -> None: + self.assertEqual("03/21/2026", ocr_form_quality.normalize_ocr_date("0321/2026")) + self.assertEqual("03/22/2026", ocr_form_quality.normalize_ocr_date("03°22/2026")) + self.assertEqual("not-a-date", ocr_form_quality.normalize_ocr_date("not-a-date")) + def test_compare_ensure_releasefast_rebuilds_even_when_binary_exists(self) -> None: calls: list[list[str]] = []