Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions crates/ogar-doc-ir/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,27 @@ pub struct TableCell {
pub text: String,
/// Where on the page the cell was read.
pub bbox: BBoxRail,
/// Per-source confidence, read through [`DocIr::source`] exactly like
/// [`TypedField::confidence`] — for [`Provenance::Ocr`] this is
/// recognition certainty.
///
/// **Why a cell needs its own confidence, not just the region's.** The
/// values a consumer actually imports — a lab result, an invoice line
/// amount — are *cells*, not [`TypedField`]s, so before this existed the
/// per-cell signal was dropped at the boundary and a review gate had
/// nothing to gate on.
///
/// The producer aggregates it as the **minimum** over the cell's words,
/// not the mean: one misread digit invalidates a whole value, and a mean
/// dilutes exactly the signal the gate exists to catch (a cell of 95 and
/// 40 reports 40, not 67).
///
/// **`0` is the fail-closed default**, and deliberately so: a `doc.v1`
/// payload predating cell confidence deserializes to `0`, which routes
/// everything to human review rather than auto-committing at an assumed
/// score. Never treat `0` as "measured zero-confidence" — it is
/// indistinguishable from "not reported".
pub confidence: u8,
}

/// A region node — the recursive awareness unit. Text-bearing kinds carry
Expand Down Expand Up @@ -412,6 +433,7 @@ mod tests {
col: 0,
text: "Pos".to_string(),
bbox: bbox(10, 40, 60, 55),
confidence: 97,
}],
children: vec![],
}],
Expand Down
36 changes: 35 additions & 1 deletion crates/ogar-from-docv1/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@ struct V1Cell {
col: u32,
bbox: [i32; 4],
text: String,
/// Per-cell OCR confidence, 0..=100. `#[serde(default)]` so a `doc.v1`
/// emitted BEFORE cells carried confidence still parses — it reads `0.0`,
/// which is fail-closed: a consumer's review gate rejects rather than
/// auto-commits. Never read `0.0` as "measured zero"; it is
/// indistinguishable from "not reported".
#[serde(default)]
conf: f32,
}

#[derive(Deserialize)]
Expand Down Expand Up @@ -227,6 +234,9 @@ pub fn from_doc_v1(
col: c.col.min(255) as u8,
text: c.text.clone(),
bbox: rail_bbox(c.bbox, w, h),
// Same 0..=100 -> u8 conversion the TypedField arm below
// uses, so the two confidence surfaces stay comparable.
confidence: c.conf.round().clamp(0.0, 255.0) as u8,
})
.collect();
regions.push(Region {
Expand Down Expand Up @@ -286,7 +296,7 @@ mod tests {
{"text":"GmbH","bbox":[210,0,500,100],"conf":98.0,"leading_space":true}]}]},
{"type":"table","bbox":[0,200,1000,600],"lines":[],
"rows":1,"cols":2,"cells":[
{"row":0,"col":0,"bbox":[0,200,500,300],"text":"Pos","header":true},
{"row":0,"col":0,"bbox":[0,200,500,300],"text":"Pos","header":true,"conf":97.0},
{"row":0,"col":1,"bbox":[500,200,1000,300],"text":"1","header":true}]},
{"type":"footer","bbox":[0,1900,1000,2000],"lines":[
{"bbox":[0,1900,300,2000],"words":[
Expand Down Expand Up @@ -319,6 +329,30 @@ mod tests {
// Table cells carried across with quantized bboxes.
assert_eq!(page.regions[1].cells.len(), 2);
assert_eq!(page.regions[1].cells[0].text, "Pos");

// Per-cell confidence survives the bridge — BOTH paths, which is why
// the fixture gives cell 0 a `"conf"` and deliberately withholds one
// from cell 1.
//
// Before this field existed, per-cell OCR confidence died here: the
// values a consumer actually imports (a lab result, an invoice line
// amount) are CELLS, not `TypedField`s, so a review gate downstream
// had nothing to gate on. `V1Cell` is a plain `Deserialize` with no
// `deny_unknown_fields`, so a producer emitting `conf` was silently
// discarded — the omission was invisible from either side.
assert_eq!(
page.regions[1].cells[0].confidence, 97,
"cell 0's doc.v1 conf:97.0 must reach TableCell::confidence"
);
// Cell 1 carries NO `conf` in the fixture: a legacy `doc.v1` predating
// per-cell confidence. `#[serde(default)]` gives 0 — fail-closed, so
// such a payload routes to human review instead of auto-committing at
// an assumed score. This asserts the legacy path is REACHABLE, not
// merely that the new one works.
assert_eq!(
page.regions[1].cells[1].confidence, 0,
"a cell with no conf in doc.v1 must default to 0 (fail-closed)"
);
// Field harvested to the document-level facts.
assert_eq!(ir.fields.len(), 1);
assert_eq!(ir.fields[0].key, "iban");
Expand Down
Loading
Loading