freepdfeditor/docs/spike-results/0002-spike-b-reconstruction.md

107 lines
5.3 KiB
Markdown
Raw Permalink 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.

<!--
SPDX-License-Identifier: GPL-3.0-or-later
SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
-->
# Spike B — glyph→Unicode + line/paragraph reconstruction: M0 result
* **Spike**: B — glyph→Unicode + line/paragraph reconstruction
(engineering plan §14 step 5)
* **Date**: 2025-07-25
* **Status**: Complete. **Gate MET** on the synthetic corpus.
* **Gate**: paragraph-boundary F1 ≥ 0.85 (M0 exit criterion); ≥ 0.93 is the
release gate (§8.2) and requires the real labelled corpus.
## What was built
`spike/B_reconstruction/` implements the reconstruction pipeline (§4.1
steps 25) on a synthetic in-memory glyph model:
- `GlyphRun.h` — the input model (`Glyph`, `GlyphRun`, `Line`, `Paragraph`,
`GroundTruthParagraph`), mirroring §3.2/§3.3.
- `Reconstruct.cpp` — step 2 (flatten runs to placed glyphs), step 3 (line
detection by baseline clustering, tolerance 0.25 × font size per §4.1
step 3), step 4 (reading order: descending-y sort for the single-column LTR
case — the recursive XY-cut reduces to this), step 5 (paragraph grouping),
plus a boundary-F1 scorer.
- `Corpus.cpp` — a deterministic synthetic corpus generator (seeded PRNG)
producing 38 paragraphs of 15 lines each, with realistic leading,
paragraph gaps, short last lines, and occasional list items.
- `main.cpp` — runs the pipeline over the corpus and emits the contract JSON.
Step 1 (glyph→Unicode via font cmap / ToUnicode) is not exercised by this
spike: the synthetic generator produces glyphs whose `unicode` is known by
construction. Step 1 is exercised separately once the production L3
interpreter (M2) wires FreeType + the ToUnicode CMap path.
## Result
| Metric | Value | Target |
|---|---|---|
| paragraph-boundary F1 | **0.963** | ≥ 0.85 |
| precision | 0.929 | — |
| recall | **1.000** | — |
| perfect documents | 334 / 500 | — |
Tested on 500 synthetic documents, single-column LTR, 12pt font, 14.4pt
leading, 23pt paragraph gap. Recall is perfect (no missed boundaries); the
remaining imperfection is over-segmentation (precision 0.93) on the ~33% of
documents where the synthetic generator creates edge cases — single-line
paragraphs adjacent to multi-line ones, and list items.
## Findings
1. **The leading-gap signal is primary; the short-last-line signal is
confirming, not primary.** The first implementation treated "previous line
ended short of the right margin" as a sufficient paragraph-break condition
and over-segmented badly (F1 ≈ 0.37): a short *first* line of a paragraph
is common, and the heuristic split paragraphs at every short line. The
correct hierarchy (per §4.1 step 5) is: leading consistency is the merge
condition; a short last line only forces a break when *combined* with
above-normal leading. This recovered F1 to 0.963.
2. **Coordinate-system orientation matters and is easy to get backwards.**
The synthetic corpus uses PDF coordinates (y increases upward), so reading
order is *descending* y. An ascending-y sort inverted the reading order and
broke both the leading sign and the "previous line" semantics. This is a
concrete instance of the §11 "real PDFs are pathological" risk and the
reason the plan keeps the corpus in PDF coordinates throughout.
3. **Column-width inference must be robust to outliers.** Inferring the
column right edge as the max `x_end` across lines let a single overshooting
line push the edge out and make every other line look "not wrapped". Using
the 90th percentile instead fixed the wrap-tolerance. This is the kind of
detail the real §4.1 step 4 XY-cut handles by computing column geometry
rather than inferring it from line ends.
4. **The 80/20 boundary is real and visible in the failure cases.** The 166
imperfect documents are exactly the §11 "last 20%" cases: single-line
paragraphs, list items, paragraphs whose first line is short. The synthetic
corpus makes these diagnosable; the real labelled corpus (§8.1) is needed
to score against ground truth on actual layout pathology and to validate
the 0.93 release gate.
## What this means for the project
- **The reconstruction pipeline shape works and scores above the M0 bar on a
controlled corpus.** The M0 exit criterion (F1 ≥ 0.85) is met; the project
is not blocked on reconstruction feasibility.
- **The release gate (F1 ≥ 0.93, §8.2) must be scored on the real labelled
corpus**, not the synthetic one. The synthetic corpus validates the
pipeline; it does not validate that the pipeline handles real PDF
pathology. Acquiring/constructing the labelled corpus (§8.1) is the next
prerequisite for the M4 text-editing milestone.
- **Step 1 (glyph→Unicode) remains the highest-risk step** and is not covered
by this spike. It is where the §4.1 priority ladder (ToUnicode → Encoding →
built-in → cmap → OCR) lives, and where low-confidence mappings drive the
UI's confidence-underlining. It should be the next reconstruction work.
## Reproducing
```bash
cmake -S . -B build/manual -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build/manual --target spike_b_reconstruction
build/manual/bin/spike_b_reconstruction 500 0.85 # <n_docs> <target_F1>
```
The single-line JSON report on stdout is the CI contract. Exit 0 if the gate
is met, 1 otherwise. Also verified clean under ASan+UBSan.