Commit graph

481 commits

Author SHA1 Message Date
LLLin000
29fc619d4d feat(ocr): add vnext same-page figure pass 2026-07-03 12:35:33 +08:00
LLLin000
a0cad9cacd fix(vnext-corpus): pass formal_legends only to candidate group builder
FigureCandidateIndex.from_corpus was passing corpus.raw_legends to
_build_candidate_figure_groups_from_assets, which let rejected captions
inflate candidate-group legend semantics (page_legend_count, band-group
assist scoring). Pass formal_legends (accepted legends only) instead.

Adds a regression test: page with one formal legend, one rejected legend,
and one asset. After from_corpus, the candidate group reports
page_legend_count=1 even though corpus.raw_legends has length 2.
2026-07-03 12:23:34 +08:00
LLLin000
37ab2b8bd1 feat(ocr): add vnext figure corpus and candidate index 2026-07-03 12:16:27 +08:00
LLLin000
4f27c30e6b fix: exclude figure_no and origin from ResourceRef equality/hash
ResourceRef equality/hash must be driven only by canonical identity
fields (kind, page, block_id/group_id) so that ledger dict lookups
succeed regardless of optional metadata differences.

Change: mark figure_no and origin with compare=False in the frozen
dataclass. The __post_init__ validation/normalization is unchanged.

Test: parametrized test_resource_ref_equality_ignores_metadata covers
asset/legend/group variants with figure_no, origin, and both together.
2026-07-03 12:10:25 +08:00
LLLin000
3c01b4da99 feat(ocr): add vnext figure ownership contracts 2026-07-03 12:05:25 +08:00
LLLin000
f95925b226 refactor(ocr): split legacy and vnext figure entrypoints 2026-07-03 12:01:11 +08:00
LLLin000
3a18160249 chore: gitignore audit per-paper dirs, delete stale annotated_pages, update AGENTS.md
- .gitignore: audit/*/ (keep root summaries), .git-rewrite, .tmp, temp reports
- Delete tests/fixtures/ocr_real_papers/*/annotated_pages/ (stale)
- rm --cached 87 per-paper audit dirs (834 files), temp reports
- AGENTS.md: add role pipeline notes for this session's changes
2026-07-03 02:03:12 +08:00
LLLin000
8dba963394 fix: complete reference detection fixes — 5 problem families 2026-07-03 01:46:27 +08:00
LLLin000
9aa228d60c feat: layout robustness layer — robust banding, role-aware gating, two-column continuation
Task 1: Add robust band estimator core module (ocr_banding.py)
  - LayoutBandEstimate, UsableContentDecision dataclasses
  - collect_layout_band_candidates, estimate_layout_bands
  - choose_runtime_bands, decide_usable_content

Task 2: Expose dry-run robust band diagnostics in normalize_document_structure
  - layout_band_estimate field on DocumentStructure
  - Serialized diagnostics with legacy vs robust comparison

Task 3: Enable robust runtime band selection
  - _order_tail_blocks uses robust estimate + choose_runtime_bands
  - Falls back to legacy-safe bands when robust holds

Task 4: Make layout band gating role-aware
  - decide_usable_content replaces _is_in_usable_content
  - Strong roles (reference_heading/item/body, backmatter_heading/boundary)
    bypass band gating entirely
  - backmatter_body gated at Phase 4b after heading ownership check

Task 5: Column-aware two-column continuation reference attachment
  - _block_column(), _should_attach_reference_item_to_ref_section()
  - rejected_ref_items list replaces body_pool fallback

Verification: 202/202 tests pass, 14/14 paper rebuilds OK,
95FDVE4W reference ordering confirmed
2026-07-02 19:15:44 +08:00
LLLin000
78b9973123 fix: reference-zone ordering fixes (P0-P4)
P0: disambiguate table field keys from block_page_by_id in ocr_render.py
P1: add _is_tail_backmatter_continuation() guard, order backmatter correctly
P2: phase 4b tail nonref guard with tail_backmatter_blocks list
P3: page-continuation-marker false reference gate in ocr_families.py
P4: force reference heading before same-page refs, ref_item bypass usable gate
2026-07-02 19:06:06 +08:00
LLLin000
5af3a1f8ca feat: asset-internal figure number recovery for rotated vector figures
adds a metadata-only recovery pass that scans matched figure assets for
internal PDF line labels ("Figure N.", "Fig. N:") when the figure lacks
a figure_number.

new components:
- extract_pdf_lines_normalized() in ocr_pdf_spans.py — reads PDF rawdict
  lines page-by-page, normalizes coordinates to OCR space
- _recover_missing_figure_numbers_from_assets() — inventory pass between
  synthetic fallback and dedup
- _needs_asset_internal_figure_number_recovery() — gate supporting both
  synthetic_figure and figure_unknown entries
- _looks_like_internal_figure_label() — regex-based label detection
- _asset_edge_band_score() — geometric rejection for center-positioned
  lines or lines covering >15% asset area
- page_pdf_lines_by_page parameter added to build_figure_inventory()

verification: U746UJ7G figure_unknown_000 -> figure_002 with recovered
label "Plot of Criteria Time". 428 regression tests pass (6 new).
2026-07-02 03:00:33 +08:00
LLLin000
d41712d7bc fix: normalize rotated figure captions before normal matching
- preserve PyMuPDF dir/wmode in span_metadata and bump span backfill version
- promote rotated vision_footnote figure descriptions into normal legend prematch
- normalize rotated caption/asset regions before score_figure_match
- carry rotation_correction_deg into matched figures and cropped renders
- use LANCZOS resampling on rotated figure crops to avoid aliasing
- keep synthetic fallback as last resort, reusing the same rotation metadata
2026-07-02 02:28:56 +08:00
LLLin000
8678de48d0 feat: add bbox-only synthetic vector figure fallback
- Add _apply_bbox_only_synthetic_vector_fallback helper
- Score by page/x-overlap/vertical-gap with hard gates at 300px and x_ratio<0.25
- Tie-breaking: skip when top-two scores differ by <0.15
- Duplicate figure number prevention against existing matched_figures
- Inserted after _infer_missing_main_figure_numbers in pipeline
- No PNG rendering — bbox-only, render layer shows placeholder
- Minimum score threshold 0.65 to prevent false positives
2026-07-02 00:51:42 +08:00
LLLin000
a0d0be9048 fix: rescue figure-description text in vision_footnote from footnote role
- Add _FIGURE_DESCRIPTION_OPENING_PATTERN for 'This figure...' / 'Figure N' etc.
- Insert rescue before generic footnote fallback
- Route to figure_caption (near media) or figure_caption_candidate (far)
2026-07-02 00:48:43 +08:00
LLLin000
01ba102605 fix: support Roman numeral and S-prefix table captions across roles/signatures/tables
- _TABLE_PREFIX_PATTERN in ocr_roles.py, ocr_signatures.py, ocr_tables.py
  now matches Table I/II/III and Table S1/S2
- _extract_table_number and _extract_marker_signature parse Roman numbers
- figure_title generic fallback checks table prefix before figure caption
- ocr_families.py adds early table-prefix guard before reference family
2026-07-02 00:47:18 +08:00
LLLin000
89f3f6e05f fix: 3 pre-existing test failures
- Add missing continue in ocr.py exception path (UnboundLocalError page_num)
- Accept selected_keys param in test stub_run_ocr
- Update active queue content assertion for new status text
2026-07-01 22:21:34 +08:00
LLLin000
8c0fbeaae7 fix: add cross-column gate + figure-table asset arbitration
- Reject cross-column safe page-assets groups (Issue 5)
- Add conservative post-hoc figure/table arbitration (Issue 6)
- Wire arbitration before ownership_conflicts in both OCR pipelines
2026-07-01 21:51:03 +08:00
LLLin000
916eadb5ff fix: add validated container regions for figure contained text
Add _container_area_ok, _container_has_media_asset, _validated_container_regions
helpers and wire validated _container_bbox regions into tag_figure_contained_text.
Containment-only fix: no ownership mutation, no new matched_figure entries.
2026-07-01 21:50:37 +08:00
LLLin000
05e75e1b29 fix: PR1 deterministic OCR residual fixes
- Clamp backfill words to original bbox (Issue 3)
- Allow validation-first bare tables to fall through (Issue 1A)
- Materialize split table caption continuations (Issue 1B)
- Add short-form OCR health profile (Issue 4)
2026-07-01 21:49:45 +08:00
LLLin000
b32ddd3b29 test: add regression tests for previous-page legend locator bridge
test_previous_page_locator_bridge_cross_page_full_legend:
  p15 full legend (in rejected_legends) + p16 locator + 3 assets.
  Verifies: settlement_type, legend_page, page, text (full not locator),
  bridge_block_ids, all 3 asset_block_ids, cluster_bbox present,
  full legend removed from unmatched_legends, assets from unmatched_assets.

test_previous_page_locator_bridge_does_not_swallow_other_group:
  p8 locator + Fig.8 3-asset group + unrelated 'other' asset.
  Verifies: only the 3-asset group consumed; 'other' stays unmatched.
2026-06-29 19:28:38 +08:00
LLLin000
55927b49f3 fix: tighten year-period regex in _is_reference_item_candidate to prevent false ref match on acknowledgment text
4AG67PBH P21: acknowledgment text absorbed as reference_item because:
  'doctoral scholarships 2023.02051.BD' matched \b(?:19|20)\d{2}\.
Fixed to \b(?:19|20)\d{2}\.(?:\s|$) — year+period must be followed by whitespace or end of string.

Left column now correct: body (Acknowledgments, Conflict, Keywords, dates) in body_zone,
then [1]-[3] in reference_zone. Right column all 33 refs unaffected.
2026-06-29 12:39:03 +08:00
LLLin000
0e04bcd5f7 fix: post-P1 bio detection fixes
- Raise word limit 80→200 (real bio text is 90-100 words)
- Add structured_insert_candidate to Pass C role list (fixes Barbara bio miss)
- Fix page-agnostic block_id filter in ocr_figures.py (lines 4479, 4529)
  - Was filtering by bare block_id, colliding same id across pages
  - Changed to (page, block_id) tuples
- Add bio passes (Pass B+C) to ocr_rebuild.py pipeline
2026-06-29 12:32:03 +08:00
Research Assistant
e61703d3fb style: fix ruff lint issues in bio detection files
- SIM103: return conditions directly instead of if/else pattern
- F541: remove extraneous f-string prefix
- I001: organize imports
2026-06-29 02:02:53 +08:00
Research Assistant
bf5b9e756b feat: add P1 author bio detection passes
- residual_author_bio_pass (Pass B): detects portrait unmatched_assets
  and unresolved_clusters with nearby bio text, reclassifies as
  author_bio_asset
- Extend post_ref_bio_cleanup (Pass C) to handle figure_caption role
- Wire Pass B before Pass C in ocr.py pipeline
- Add tag_figure_contained_text protection: skip author_bio blocks
  and author_bio_asset role
- 7 new P1 tests, 1018 total OCR tests pass, 0 regressions

Part of P1 bio detection (spec v3 §7-8, Tasks 4-5).
2026-06-29 01:29:08 +08:00
Research Assistant
30c1ef5c3a feat: add bio detection utilities and post_ref_bio_cleanup
- _bio_text_score: category-weighted 0-5 scoring with career/education/
  research/institution/publication categories
- _is_portrait_like / _any_portrait_like: image profile card detection
- _has_formal_figure_number: guard for Fig./Table/Scheme prefixes
- _looks_like_reference: guard for [N] + et al/year patterns
- _nearby_blocks: spatial proximity search with cluster_bbox support
- _resolve_ref_start_page: reference zone page detection
- _add_block_keys / _any_bio_text helpers
- post_ref_bio_cleanup: detects author bio text in post-ref reference_zone
  blocks and overrides role to backmatter_body
- _is_strongly_figure_matched: guards against figure-matched blocks
- _is_protected_strong_figure / _is_reversible_weak_figure_match:
  strong vs weak figure settlement type classification
- prune_figure_inventory_after_bio: removes bio artifacts from
  ambiguous_figures and held_figures before reader

Part of P0 bio detection (spec v3 §4-5, §8, Tasks 1+2).
2026-06-29 01:24:33 +08:00
Research Assistant
d05623913b test: fix 25 stale test fixtures across 10 issues (616 OCR tests pass)
Pipeline fixes:
  - non_body_insert page-1 guard threshold: page_width * 0.2 -> 0.25
  - frontmatter_noise safe role preservation: removed explicit VERIFY_REQUIRED exclusion
  - span_visual_container_version: 2026-06-22.1 -> 2026-06-26.6

Test expectation updates:
  - non_body_insert: figure_caption test -> ponytail behavior
  - caffard abstract: accept Methods in main_ids as structured abstract subheading
  - legend_like role override: body_zone -> display_zone
  - backmatter heading render: **bold** -> ## heading
  - state machine: accept done_degraded as terminal status
  - body_zone anchor_family: relaxed assertion
  - truth surface docs: updated phrases for current phase
  - trace-vs-expectations: 10 assertions updated for post-P1 pipeline behavior
2026-06-28 23:19:47 +08:00
Research Assistant
c15227076a backmatter: remove pre-ref disclosure normalization (pre-ref = body flow)
Confirmed with user: pre-ref disclosure headings (CRediT, Ethics) must stay
as subsection_heading in body flow. Removed _normalize_pre_ref_disclosure_runs
and related constants/functions. Updated tests (20 -> 16 cases).

5 audit papers verified:
  25K5KZAQ: body_end=11 ref_start=10 (was None)
  YGH7VEX6: body_end=8 ref_start=8
  NC66N4Q3: body_end=56 ref_start=56 (was None)
  9TW98JH8: body_end=5 ref_start=5
  XD2BPCMG: body_end=26 ref_start=21

318/318 tests pass.
2026-06-28 01:28:53 +08:00
Research Assistant
1a9ad1d147 P1: backmatter boundary redesign — ref-anchored partition replaces second _reconcile_tail_spread
reference_zone is the only hard boundary. Pre-ref disclosure headings (CRediT,
Ethics, Declaration) are normalized as local same-column runs — they never set
global boundaries. Four contracts enforced (Contract A-D in spec).

- 13 new helpers in ocr_document.py: _has_verified_reference_zone, _partition_by_reference_zone,
  _classify_same_page_block, _normalize_reference_roles_from_partition,
  _normalize_pre_ref_disclosure_runs, _build_tail_boundary_from_ref_partition, etc.
- infer_zones() stores effective_end_page for trimmed reference extent
- normalize_document_structure(): ref-partitioned path replaces second _reconcile_tail_spread;
  _promote_tail_body_candidates/_assign_tail_spread_ownership skipped when active
- 20 tests in test_backmatter_boundary.py
- 322/322 tests pass
2026-06-28 01:13:54 +08:00
Research Assistant
b1fdbc30eb feat: P0 fixes (ref sort, caption insert) + P1 figure containment render hygiene
P0:
- Fix 2: _ref_number_sort_key handles [N] bracket format (ocr_render.py:481)
- Fix 4: remove figure_caption from _INSERT_CANDIDATE_ROLES (ocr_document.py:3846)
- Fix 5: filter demoted body_paragraph from figure legends (ocr_figures.py:2950)

P1:
- figure containment render-hygiene pass after table inventory writeback
- 6 helpers: _cluster_bboxes_by_proximity, _is_contained,
  _highly_overlaps_any_matched_region, _figure_region_bbox,
  _matched_asset_keys, tag_figure_contained_text
- Wire call site in postprocess_ocr_result() (ocr.py:1891)

Infra:
- Sync workspace fulltext after rebuild (asset_index.py)
- Fix UnboundLocalError on index-refresh (sync_service.py)
- Add missing frontmatter fields to field registry (field_registry.yaml)
- 283 + 19 = 302 tests passing
2026-06-27 22:56:22 +08:00
Research Assistant
71477d8b69 feat: evidence-driven container admission rewrite
Replace size-first gates with evidence-driven admission:
- Feature extraction per drawing (fill/border/gray/line-like flags)
- page-sized/crop-like: completely excluded (no grouping_pool)
- line-like: grouping_pool only, not standalone
- Vertical component merge (x_overlap>=0.8, gap -2..5pt)
- Merged rects replace child candidates; area-descending sort
- Text evidence required (PDF text blocks + OCR raw blocks via _map_ocr_bbox_to_pdf_rect)
- Per-page three-phase loop: clear flags → compute containers → mark blocks
- 11 new tests (color helpers, admission, veto, merge, PDF-coordinate mapping)
2026-06-26 22:50:15 +08:00
Research Assistant
5cc6011ff3 feat: figure-number inference for leading Figure 1 gaps
Add _infer_missing_main_figure_numbers() that fills figure_number=None
for matched main-sequence figures when a single leading gap (Figure 1)
can be inferred with high confidence.

- _FRONTMATTER_VISUAL_VETO + _has_frontmatter_visual_veto()
- _FIGURE_MARKER_PATTERN regex + _extract_figure_marker()
- _coerce_int_figure_number(), _resolve_legend_bbox()
- _infer_missing_main_figure_numbers() wired into build_figure_inventory()
- 9 test cases covering acceptance, veto, isolation, skip reasons
- Fix golden vault test key drift (aliases, ocr_time, etc.)
2026-06-26 21:13:24 +08:00
Research Assistant
c9ca8e2657 docs+fix: evidence-driven container admission + figure-number-inference spec/plan
_extract_visual_container_rects:
- Replace flat width<100/height<50 gate with evidence-driven admission
- Add line-like veto (<=3pt, pure decorative separators)
- Add page-sized veto (>=60% page + near full edge = background/clip)
- Add vertical component merge (same x-range, gap <=5pt)
- Keep existing fill/border/thin-gray filter logic

spec: 2026-06-26-figure-number-fallback-by-sequence-design
- Fix integration point (after _promote_sequence_matches)
- Add 3-tier legend_bbox lookup
- Add _FRONTMATTER_VISUAL_VETO keyword list
- Add structured _extract_figure_marker() return
- Add skip-reason contract to inventory

plan: 2026-06-26-figure-number-inference-implementation
- 6 tasks, ~95min, single-commit PR
2026-06-26 20:22:43 +08:00
Research Assistant
190ce2440a fix: refine thin-border rect filter to only skip dark-gray decorations
Skip only thin-border unfilled rects with near-gray color and
low brightness — these are PyMuPDF get_drawings() false positives
from clipping paths. Keep colored thin borders (legitimate boxes)
and pure black thin borders (legitimate black frames).

Added 2 tests (colored border kept, black border kept).
Verified via 50-paper audit: all 14 dark-gray rects filtered,
all colored/black rects preserved.
2026-06-26 19:16:37 +08:00
Research Assistant
5d9d1bf973 fix: filter thin-border page decoration rects from _extract_visual_container_rects
N6XCZD25's body paragraphs on pages 3+ were misclassified as
structured_insert because a 0.5pt unfilled page-decoration border
rectangle (covering 80% of the page) was detected as a container.

Filter: skip bordered+unfilled rectangles with stroke_width <= 1pt
that cover >50% of page area — these are text-area frames, not
callout boxes.
2026-06-26 18:52:51 +08:00
Research Assistant
faa70545eb fix: resolve figure_id collisions via name mutation
When two figures share the same figure_id (e.g. supplementary
"Figure S.1" and main "Figure 1" both mapped to "figure_001"),
the second occurrence is renamed with an "s" prefix:
  figure_001 -> figure_s001, figure_ss001, etc.

This avoids file overwrite in ocr_objects.py and ensures both
body and supplementary figures appear at their correct positions
in the rendered fulltext.

Changes:
- _resolve_figure_id_collisions() in ocr_figures.py
- figure_id passthrough in ocr_figure_reader.py (reader + normalize)
- _reader_figure_embed_target prefers explicit figure_id
- 11 unit tests covering single/triple/multi collisions, mixed buckets,
  empty IDs, realistic body+supplementary layout
2026-06-26 18:36:04 +08:00
Research Assistant
93c90f988a fix: page_numbered_legend_count uses ordered_legends (post-suppression) not deduped_legends 2026-06-26 00:24:00 +08:00
Research Assistant
5a494c0dce test: page_assets safety gate regressions (6 unit + 1 integration) 2026-06-26 00:16:33 +08:00
Research Assistant
943e5995cb fix: ownership skip also matches str(block_id) for int/string alias 2026-06-25 23:08:07 +08:00
Research Assistant
206669b0da test: render contract for table ownership and note skip 2026-06-25 22:31:01 +08:00
Research Assistant
4b89209229 test: inventory contract for table note ownership 2026-06-25 22:29:30 +08:00
Research Assistant
87d3e8cae2 test: add regression tests for footnote skip, caption fallback, bare table N matching 2026-06-25 17:23:49 +08:00
Research Assistant
06d36ff501 test: update heading prefix expectation for font-size logic 2026-06-25 16:50:38 +08:00
Research Assistant
d018a68fd6 fix: harden early OCR frontmatter-side zone inference 2026-06-25 15:57:53 +08:00
Research Assistant
1f7bd743ce fix: close out OCR merge-gate figure blockers 2026-06-24 01:09:38 +08:00
Research Assistant
fb19d5ba20 fix: sidecar outlier-tolerant narrow column + raw band for far-column panels
- _same_page_narrow_caption_column: cluster by x-center when global
  spread check fails, keep largest aligned column with >=2 captions.
  Singleton right-column outliers (misclassified body mentions) dropped.
- Sidecar promotion uses raw_band_assets directly (band partition is
  the ownership unit for sidecar pages, not row-coupled geometry).
- Add asset_block_ids to sidecar entry schema.
- Fixes 6FGDBFQN Fig 3: 2 assets [11,13] via sidecar (was assetless_shell).
  Fig 2: 2 assets [3,4] via sidecar (was 4 via same_page).
- 221 tests, 0 failures.
2026-06-23 19:29:38 +08:00
Research Assistant
c03b95964e fix: same-number distinct legend dedup guard
- _strip_caption_number_prefix + _normalized_caption_body helpers
- When same figure number but different caption body (and not bundle-source),
  retain both as distinct legends instead of deduping
- Fixes 2UIPV93M page 49 appendix figures silently removed by dedup
- Bundle-source dedup (DWQQK2YB pattern) unaffected
- same_number_distinct_legends audit surface added to inventory
- 217 tests passed, 0 failures
2026-06-23 16:56:52 +08:00
Research Assistant
1076cffc82 fix: converge figure ownership accounting and phantom cluster cleanup
- Add _score_dense_parent_candidate_against_local_ownership (Task 5)
- Dense parent built before legend loop to participate in arbitration (Task 7)
- Relax _dense_parent_single_group_ok to fragment_count >= 4
- Dense parents excluded from pages with competing captions
- Dense page unmatched assets -> unresolved_clusters with cluster_source
- Dedup unresolved_clusters against matched_figures after all matching
- Composite parent supports representative cases (2UIPV93M, RKSLQRIM);
  VFS8CBW2 resolved via same_page/cross_page after phantom fix
- 217 tests passed, 0 failures
2026-06-23 15:22:34 +08:00
Research Assistant
d7e7821c9c fix: resolve VFS8CBW2 page-32 Fig. 3 via page-aware reservation keys
- Changed _reserve_cross_page_objects to return set[tuple[int, str]]
  instead of set[str] so reused block_id across pages does not
  overwrite reserved legends (VFS8CBW2 page 33 vs 38 both used block_id=1)
- Updated _settle_cross_page_reserved_objects to key legends_by_id
  by (page, block_id)
- Fix: reserved legends no longer filtered by caption-band assist on
  competing-caption pages, so deferred hypotheses still emit
- Fix: reservation test assertion updated for tuple format
- Fix: competing-caption veto test updated for band-scoped parent
  acceptance
2026-06-23 14:29:05 +08:00
Research Assistant
b3ccea2de6 Dense composite parent candidate hardening — visual-only construction from atomic groups + unresolved clusters for dense fragmented pages 2026-06-23 11:50:22 +08:00
Research Assistant
1ef14b0030 Ticket B: Panel-title structural suppression — demote short unnumbered text inside visual envelopes on dense pages 2026-06-23 11:14:06 +08:00