Coverage for src / quber / core / extractors / set_of_mark / inspection.py: 92%

48 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-09-23 22:14 -0400

1"""Verify every gap cell's proposed status against the table image. 

2 

3The classifier assigns each unboxed corrected cell a condition from text 

4evidence alone — a hypothesis. This stage has the status inspector LOOK at 

5the table crop and give a three-way verdict per cell, and holds authority 

6deterministically: a condition the inspector positively confirms keeps its 

7status with the evidence recorded on the cell; a condition the image 

8positively CONTRADICTS (the page shows something else there) becomes 

9`defect`; anything the image cannot settle — including unmentioned cells 

10and unreachable inspections (failed call, missing crop) — becomes 

11`unverified`. The inspector is never forced to pick the nearest option when 

12none fits. There is no upgrade path anywhere: inspection can move a cell 

13into the review or defect queues, never out of them. 

14""" 

15 

16from __future__ import annotations 

17 

18import asyncio 

19from pathlib import Path 

20from typing import Any, Dict, List, Optional, Sequence, Tuple 

21 

22from loguru import logger 

23 

24from quber.agents.completeness import page_words 

25from quber.agents.status_inspector import StatusInspector 

26from quber.core.extractors.base import GroundedCell 

27from quber.core.extractors.camelot.correspondence.geometry import crop_region_png, table_crop_box 

28 

29# The statuses that are hypotheses needing visual confirmation. `reconciled` 

30# has its box; `single_character` is definitional (text length); everything 

31# else claims something only the image can show. 

32INSPECTED_STATUSES = frozenset( 

33 { 

34 "header_printed_unlocated", 

35 "label_printed_unlocated", 

36 "total_label_added", 

37 "header_label_added", 

38 "unverified", 

39 } 

40) 

41 

42# The header/label split is a diagnostic convenience — both are pass-tier — 

43# so both carry the SAME position-agnostic condition: the claim to verify is 

44# that the text is printed on the page, not where. A position-specific 

45# framing let a row-kind misclassification turn a correctly printed label 

46# into a false defect. 

47# The condition states only what the IMAGE can verify. Naming the internal 

48# mechanism ("could not be located in the text grid") invited the model to 

49# dispute that unverifiable clause and contradict text it could see printed. 

50_PRINTED_CONDITION = ( 

51 "this text is printed on the page — as a header, a band, or a row label; " 

52 "it may wrap across printed lines, combine fragments printed apart, or " 

53 "share a line with neighboring text" 

54) 

55_CONDITION_TEXT = { 

56 "header_printed_unlocated": _PRINTED_CONDITION, 

57 "label_printed_unlocated": _PRINTED_CONDITION, 

58 "total_label_added": ( 

59 "the extraction ADDED this conventional label to a row printed without " 

60 "one; the page prints nothing at this position and the row visibly " 

61 "totals or summarizes its section" 

62 ), 

63 "header_label_added": ( 

64 "the extraction ADDED this conventional column heading; the page prints " 

65 "the table without a header over this column, so nothing is printed at " 

66 "this position" 

67 ), 

68 "unverified": ( 

69 "the extraction wrote this text without a known printed source. This " 

70 "condition holds when the page prints NOTHING at the position (or the " 

71 "text is nowhere to be seen); it is contradicted ONLY when the page " 

72 "prints a DIFFERENT word at the position that the extraction should " 

73 "have used instead" 

74 ), 

75} 

76 

77 

78async def inspect_gap_cells( 

79 corrected_grid: Sequence[Sequence[GroundedCell]], 

80 markdown: str, 

81 source: str, 

82 page: int, 

83 page_image: Optional[Path], 

84 bbox: Optional[Tuple[float, float, float, float]], 

85 dpi: int, 

86 inspector: Optional[StatusInspector], 

87) -> None: 

88 """Run one inspection over the table's flagged cells and gate the results.""" 

89 flagged: List[Tuple[int, int, GroundedCell]] = [ 

90 (r, c, cell) 

91 for r, row in enumerate(corrected_grid) 

92 for c, cell in enumerate(row) 

93 if cell.status in INSPECTED_STATUSES 

94 ] 

95 if not flagged or inspector is None: 

96 return 

97 if page_image is None or bbox is None: 

98 _downgrade_all(flagged, "no table image available for inspection", page) 

99 return 

100 

101 try: 

102 _pw, page_h, _words = await asyncio.to_thread(page_words, Path(source), page) 

103 crop_png = await asyncio.to_thread(crop_region_png, page_image, table_crop_box(bbox, page_h), dpi) 

104 except Exception as exc: 

105 logger.warning( 

106 "page {}: could not prepare the table image for inspection; its flagged cells " 

107 "are recorded as unverified in the review flags: {}", 

108 page, 

109 exc, 

110 ) 

111 _downgrade_all(flagged, "inspection crop failed", page) 

112 return 

113 

114 payload: List[Dict[str, Any]] = [ 

115 {"row": r, "col": c, "text": cell.text, "condition": _CONDITION_TEXT[cell.status or ""]} 

116 for r, c, cell in flagged 

117 ] 

118 report = await inspector.inspect(crop_png, markdown, payload, page) 

119 findings = {(f.row, f.col): f for f in report.findings} if report else {} 

120 

121 for r, c, cell in flagged: 

122 f = findings.get((r, c)) 

123 if f is None: 

124 _reassign(cell, "unverified", "not confirmed by inspection", page, r, c) 

125 elif f.verdict == "holds": 

126 cell.note = f.evidence 

127 elif f.verdict == "contradicted": 

128 _reassign(cell, "defect", f.evidence, page, r, c) 

129 else: 

130 _reassign(cell, "unverified", f.evidence, page, r, c) 

131 

132 

133def _reassign(cell: GroundedCell, status: str, evidence: str, page: int, r: int, c: int) -> None: 

134 # A confirmed defect is the one outcome a person must act on, so it alone 

135 # warns; downgrades to unverified reach the reviewer through the flags 

136 # record and leave only a debugging trace here. 

137 if cell.status != status: 

138 if status == "defect": 

139 logger.warning( 

140 "page {}: the page contradicts the output at row {}, col {} — the extraction " 

141 "wrote {!r}, but the inspector saw: {} (recorded as a defect in the review flags)", 

142 page, 

143 r, 

144 c, 

145 cell.text[:40], 

146 evidence, 

147 ) 

148 else: 

149 logger.debug( 

150 "page {}: cell (row {}, col {}) {!r} could not be confirmed against the page ({} -> {}): {}", 

151 page, 

152 r, 

153 c, 

154 cell.text[:40], 

155 cell.status, 

156 status, 

157 evidence, 

158 ) 

159 cell.status = status 

160 cell.note = evidence 

161 

162 

163def _downgrade_all(flagged: Sequence[Tuple[int, int, GroundedCell]], reason: str, page: int) -> None: 

164 for r, c, cell in flagged: 

165 _reassign(cell, "unverified", reason, page, r, c)