diff --git a/paperforge/worker/ocr.py b/paperforge/worker/ocr.py index cddb361b..ab1e3835 100644 --- a/paperforge/worker/ocr.py +++ b/paperforge/worker/ocr.py @@ -1754,6 +1754,9 @@ def postprocess_ocr_result(vault: Path, key: str, all_results: list[dict]) -> tu write_raw_blocks_jsonl(artifacts.blocks_raw, all_raw_blocks) structured = build_structured_blocks(all_raw_blocks) write_structured_blocks_jsonl(artifacts.blocks_structured, structured) + # Write role-level span profiles + from paperforge.worker.ocr_profiles import write_role_span_profiles + write_role_span_profiles(structured, artifacts.blocks_structured.parent) # --- Phase 2: metadata resolution --- metadata_dir = ocr_root / "metadata" diff --git a/paperforge/worker/ocr_profiles.py b/paperforge/worker/ocr_profiles.py index 1456ee69..e22aff6f 100644 --- a/paperforge/worker/ocr_profiles.py +++ b/paperforge/worker/ocr_profiles.py @@ -6,6 +6,9 @@ profile-quality scoring, span cross-validation, role-family comparison. from __future__ import annotations +import json +from pathlib import Path + def extract_block_span_profile(block: dict) -> dict | None: """Extract a normalized style profile dict from a block's span_metadata. @@ -231,3 +234,18 @@ def cross_validate_with_span( "suggested_roles": suggested_roles, "match_details": {tentative_role: current_match}, } + + +def write_role_span_profiles( + blocks: list[dict], + output_dir: str | Path, +) -> Path: + """Build and write role_span_profiles.json. + + Returns the path to the written file. + """ + profiles = build_role_span_profiles(blocks) + output_path = Path(output_dir) / "role_span_profiles.json" + with open(output_path, "w", encoding="utf-8") as f: + json.dump(profiles, f, indent=2, ensure_ascii=False) + return output_path diff --git a/paperforge/worker/ocr_rebuild.py b/paperforge/worker/ocr_rebuild.py index 6da8e10c..86d0b5c4 100644 --- a/paperforge/worker/ocr_rebuild.py +++ b/paperforge/worker/ocr_rebuild.py @@ -60,6 +60,9 @@ def run_derived_rebuild_for_keys(vault: Path, keys: list[str]) -> dict: structured = build_structured_blocks(all_raw_blocks) write_structured_blocks_jsonl(artifacts.blocks_structured, structured) + # Write role-level span profiles + from paperforge.worker.ocr_profiles import write_role_span_profiles + write_role_span_profiles(structured, artifacts.blocks_structured.parent) # Read source metadata source_meta = read_json(artifacts.source_metadata) if artifacts.source_metadata.exists() else {} diff --git a/tests/test_ocr_blocks.py b/tests/test_ocr_blocks.py index c1e2f5c5..9fec0652 100644 --- a/tests/test_ocr_blocks.py +++ b/tests/test_ocr_blocks.py @@ -103,3 +103,19 @@ def test_build_structured_blocks_carries_span_metadata() -> None: ] rows = build_structured_blocks(raw_blocks) assert rows[0].get("span_metadata") == span_data + + +def test_role_span_profiles_written_to_output() -> None: + """Verify that role_span_profiles.json is written during rebuild.""" + import json + from paperforge.worker.ocr_profiles import build_role_span_profiles + + blocks = [ + {"role": "section_heading", "span_metadata": {"size": 16.0, "flags": "bold"}}, + {"role": "body_paragraph", "span_metadata": {"size": 10.0, "flags": 0}}, + ] + profiles = build_role_span_profiles(blocks) + # Must be JSON-serializable + dumped = json.dumps(profiles) + assert "section_heading" in dumped + assert "body_paragraph" in dumped