mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
feat: surface OCR structured health in doctor and status
This commit is contained in:
parent
5cfb58668d
commit
22d189e69e
7 changed files with 161 additions and 4 deletions
|
|
@ -15,6 +15,7 @@
|
|||
"done": 0,
|
||||
"failed": 0
|
||||
},
|
||||
"structured_ocr_health": [],
|
||||
"path_errors": 0,
|
||||
"env_configured": false,
|
||||
"lifecycle_level_counts": {},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
"""OCR command — unifies OCR run and diagnose."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -51,6 +52,35 @@ def _collect_ocr_queue_data(vault: Path) -> dict:
|
|||
}
|
||||
|
||||
|
||||
def _collect_ocr_health_summary(vault: Path) -> list[dict]:
|
||||
"""Scan OCR directories for health/ocr_health.json files and return summaries."""
|
||||
from paperforge.worker._utils import pipeline_paths
|
||||
|
||||
paths = pipeline_paths(vault)
|
||||
ocr_root = paths.get("ocr")
|
||||
if not ocr_root or not ocr_root.exists():
|
||||
return []
|
||||
summaries = []
|
||||
for dir_path in sorted(ocr_root.iterdir()):
|
||||
if not dir_path.is_dir():
|
||||
continue
|
||||
health_path = dir_path / "health" / "ocr_health.json"
|
||||
if health_path.exists():
|
||||
try:
|
||||
health = json.loads(health_path.read_text(encoding="utf-8"))
|
||||
summaries.append({
|
||||
"key": dir_path.name,
|
||||
"overall": health.get("overall", "unknown"),
|
||||
"page_count": health.get("page_count", 0),
|
||||
"blocks_count": health.get("blocks_count", 0),
|
||||
"figure_count": health.get("figure_caption_count", 0),
|
||||
"table_count": health.get("table_caption_count", 0),
|
||||
})
|
||||
except Exception:
|
||||
continue
|
||||
return summaries
|
||||
|
||||
|
||||
def _diagnose(vault: Path, live: bool = False, json_output: bool = False) -> int:
|
||||
"""Run OCR diagnostics and print results."""
|
||||
from paperforge.ocr_diagnostics import ocr_doctor
|
||||
|
|
@ -61,6 +91,7 @@ def _diagnose(vault: Path, live: bool = False, json_output: bool = False) -> int
|
|||
|
||||
if json_output:
|
||||
queue_data = _collect_ocr_queue_data(vault)
|
||||
structured_health = _collect_ocr_health_summary(vault)
|
||||
pf_error = None
|
||||
if not passed:
|
||||
if level == 1:
|
||||
|
|
@ -86,6 +117,7 @@ def _diagnose(vault: Path, live: bool = False, json_output: bool = False) -> int
|
|||
"passed": passed,
|
||||
"message": result.get("message", result.get("error", "")),
|
||||
},
|
||||
"structured_health": structured_health,
|
||||
**queue_data,
|
||||
},
|
||||
error=pf_error,
|
||||
|
|
@ -97,13 +129,25 @@ def _diagnose(vault: Path, live: bool = False, json_output: bool = False) -> int
|
|||
print("-" * 40)
|
||||
if passed:
|
||||
print(f"[PASS] {result.get('message', 'All checks passed')}")
|
||||
return 0
|
||||
else:
|
||||
print(f"[FAIL] Level {level}: {result.get('error', 'Unknown failure')}")
|
||||
print(f"[FIX] {result.get('fix', 'No fix suggestion available')}")
|
||||
if result.get("raw_response"):
|
||||
print(f"[RAW] {result['raw_response'][:200]}...")
|
||||
return 1
|
||||
|
||||
structured_health = _collect_ocr_health_summary(vault)
|
||||
if structured_health:
|
||||
print()
|
||||
print("--- Structured OCR Health ---")
|
||||
print(f"Papers with health data: {len(structured_health)}")
|
||||
for entry in structured_health:
|
||||
print(
|
||||
f"- {entry['key']}: overall={entry['overall']}, "
|
||||
f"{entry['page_count']} pages, {entry['blocks_count']} blocks, "
|
||||
f"{entry['figure_count']} figures, {entry['table_count']} tables"
|
||||
)
|
||||
|
||||
return 0 if passed else 1
|
||||
|
||||
|
||||
def _get_run_ocr():
|
||||
|
|
|
|||
|
|
@ -1110,6 +1110,10 @@ def run_status(vault: Path, verbose: bool = False, json_output: bool = False) ->
|
|||
except Exception:
|
||||
pass
|
||||
|
||||
# Structured OCR health
|
||||
from paperforge.commands.ocr import _collect_ocr_health_summary as _collect_health
|
||||
_structured_health = _collect_health(vault)
|
||||
|
||||
if json_output:
|
||||
payload = {
|
||||
"version": __import__("paperforge").__version__,
|
||||
|
|
@ -1128,6 +1132,7 @@ def run_status(vault: Path, verbose: bool = False, json_output: bool = False) ->
|
|||
"done": ocr_done,
|
||||
"failed": ocr_failed,
|
||||
},
|
||||
"structured_ocr_health": _structured_health,
|
||||
"path_errors": path_error_count,
|
||||
"env_configured": len(env_found) > 0,
|
||||
# Phase 25: lifecycle/health/maturity from canonical index ({} when unavailable)
|
||||
|
|
@ -1171,8 +1176,17 @@ def run_status(vault: Path, verbose: bool = False, json_output: bool = False) ->
|
|||
f"asset={ha['asset_health']['healthy']}/{ha['asset_health']['unhealthy']}"
|
||||
)
|
||||
print(
|
||||
f"- ocr: {ocr_done}/{ocr_total} done (pending: {ocr_pending}, processing: {ocr_processing}, failed: {ocr_failed})"
|
||||
f"- ocr: {ocr_done}/{ocr_total} done "
|
||||
f"(pending: {ocr_pending}, processing: {ocr_processing}, failed: {ocr_failed})"
|
||||
)
|
||||
if _structured_health:
|
||||
print(f"- structured_ocr_health: {len(_structured_health)} paper(s)")
|
||||
for entry in _structured_health:
|
||||
print(
|
||||
f" - {entry['key']}: overall={entry['overall']}, "
|
||||
f"{entry['page_count']} pages, {entry['blocks_count']} blocks, "
|
||||
f"{entry['figure_count']} figures, {entry['table_count']} tables"
|
||||
)
|
||||
print(f"- path_errors: {path_error_count}")
|
||||
if path_error_count > 0:
|
||||
print(" Tip: Run `paperforge repair --fix-paths` to attempt resolution")
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
"done": 0,
|
||||
"failed": 0
|
||||
},
|
||||
"structured_ocr_health": [],
|
||||
"path_errors": 0,
|
||||
"env_configured": true,
|
||||
"lifecycle_level_counts": {},
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ class TestStatusJson:
|
|||
OPTIONAL_KEYS = {
|
||||
"version", # duplicated from envelope (old format preserved in data)
|
||||
"formal_notes", "exports", "domains", "bases", "path_errors",
|
||||
"env_configured", "ocr",
|
||||
"env_configured", "ocr", "structured_ocr_health",
|
||||
"lifecycle_level_counts", "health_aggregate", "maturity_distribution",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from paperforge.ocr_diagnostics import ocr_doctor
|
||||
|
|
@ -182,3 +183,44 @@ def test_l4_live_failure():
|
|||
assert result["level"] == 4
|
||||
assert result["passed"] is False
|
||||
assert "bad pdf" in result["error"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Structured OCR Health in _diagnose()
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_doctor_reads_structured_ocr_health(tmp_path: Path, capsys) -> None:
|
||||
"""_diagnose() reads ocr_health.json and prints structured summary."""
|
||||
vault = tmp_path / "vault"
|
||||
vault.mkdir()
|
||||
(vault / "paperforge.json").write_text(
|
||||
json.dumps({"vault_config": {"system_dir": "System", "resources_dir": "Resources"}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
ocr_dir = vault / "System" / "PaperForge" / "ocr" / "HLTH001"
|
||||
ocr_dir.mkdir(parents=True)
|
||||
health_dir = ocr_dir / "health"
|
||||
health_dir.mkdir(parents=True)
|
||||
(health_dir / "ocr_health.json").write_text(
|
||||
json.dumps({
|
||||
"overall": "yellow",
|
||||
"page_count": 5,
|
||||
"blocks_count": 100,
|
||||
"figure_caption_count": 3,
|
||||
"table_caption_count": 2,
|
||||
}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
from paperforge.commands.ocr import _diagnose
|
||||
|
||||
with patch("paperforge.ocr_diagnostics.ocr_doctor",
|
||||
return_value={"level": 3, "passed": True, "message": "All good"}):
|
||||
exit_code = _diagnose(vault, live=False)
|
||||
|
||||
captured = capsys.readouterr()
|
||||
assert exit_code == 0
|
||||
assert "Structured OCR Health" in captured.out
|
||||
assert "yellow" in captured.out
|
||||
assert "HLTH001" in captured.out
|
||||
assert "3 figures" in captured.out
|
||||
assert "2 tables" in captured.out
|
||||
|
|
|
|||
|
|
@ -150,6 +150,33 @@ class TestStatusJsonIndexSource:
|
|||
assert data["health_aggregate"] == {}
|
||||
assert data["maturity_distribution"] == {}
|
||||
|
||||
def test_status_json_includes_structured_ocr_health(self, tmp_path: Path, capsys) -> None:
|
||||
"""JSON output includes structured_ocr_health key."""
|
||||
from paperforge.worker.status import run_status
|
||||
|
||||
vault = _minimal_vault(tmp_path)
|
||||
_ensure_domain_config(vault)
|
||||
_ensure_exports(vault)
|
||||
|
||||
ocr_dir = vault / "99_System" / "PaperForge" / "ocr" / "HLTH001"
|
||||
ocr_dir.mkdir(parents=True)
|
||||
health_dir = ocr_dir / "health"
|
||||
health_dir.mkdir(parents=True)
|
||||
(health_dir / "ocr_health.json").write_text(
|
||||
json.dumps({"overall": "green", "page_count": 3, "blocks_count": 50}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
code = run_status(vault, json_output=True)
|
||||
captured = capsys.readouterr().out
|
||||
assert code == 0
|
||||
envelope = json.loads(captured)
|
||||
data = envelope["data"]
|
||||
assert "structured_ocr_health" in data
|
||||
assert len(data["structured_ocr_health"]) == 1
|
||||
assert data["structured_ocr_health"][0]["key"] == "HLTH001"
|
||||
assert data["structured_ocr_health"][0]["overall"] == "green"
|
||||
|
||||
def test_status_text_output_includes_index_section(self, tmp_path: Path, capsys) -> None:
|
||||
"""Text output contains 'lifecycle:' line when index is present."""
|
||||
from paperforge.worker.status import run_status
|
||||
|
|
@ -245,3 +272,31 @@ class TestDoctorIndexHealth:
|
|||
output = self._run_doctor(vault)
|
||||
assert "Index Health" in output
|
||||
assert "No canonical index" in output
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Structured OCR Health in status
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_status_text_structured_ocr_health(tmp_path: Path, capsys) -> None:
|
||||
"""run_status() text output shows structured OCR health."""
|
||||
from paperforge.worker.status import run_status
|
||||
|
||||
vault = _minimal_vault(tmp_path)
|
||||
_ensure_domain_config(vault)
|
||||
_ensure_exports(vault)
|
||||
|
||||
ocr_dir = vault / "99_System" / "PaperForge" / "ocr" / "HLTH001"
|
||||
ocr_dir.mkdir(parents=True)
|
||||
health_dir = ocr_dir / "health"
|
||||
health_dir.mkdir(parents=True)
|
||||
(health_dir / "ocr_health.json").write_text(
|
||||
json.dumps({"overall": "yellow", "page_count": 5, "blocks_count": 100}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
code = run_status(vault)
|
||||
captured = capsys.readouterr().out
|
||||
assert code == 0
|
||||
assert "structured_ocr_health" in captured
|
||||
assert "HLTH001" in captured
|
||||
assert "yellow" in captured
|
||||
|
|
|
|||
Loading…
Reference in a new issue