use std::sync::OnceLock;
use docling_core::confidence::{nanmean, nanquantile, PageConfidence};
use regex::Regex;
use crate::layout::Region;
use crate::pdfium_backend::TextCell;
pub fn rate_text_quality(text: &str) -> f64 {
static GLYPH_RE: OnceLock<Regex> = OnceLock::new();
static SLASH_G_RE: OnceLock<Regex> = OnceLock::new();
static FRAG_RE: OnceLock<Regex> = OnceLock::new();
static SLASH_NUMBER_GARBAGE_RE: OnceLock<Regex> = OnceLock::new();
let glyph = GLYPH_RE.get_or_init(|| Regex::new(r"GLYPH<[0-9A-Fa-f]+>").unwrap());
let slash_g = SLASH_G_RE.get_or_init(|| Regex::new(r"(?:/G\d+){2,}").unwrap());
let frag =
FRAG_RE.get_or_init(|| Regex::new(r"\b[A-Za-z](?:/[a-z]{1,3}\.[a-z]{1,3}){2,}\b").unwrap());
let slash_number =
SLASH_NUMBER_GARBAGE_RE.get_or_init(|| Regex::new(r"^(?:/\w+\s*){2,}").unwrap());
if text.contains('\u{fffd}')
|| glyph.is_match(text)
|| slash_g.is_match(text)
|| slash_number.is_match(text)
{
return 0.0;
}
let frag_matches = frag.find_iter(text).count();
let mut penalty = 0.0;
if frag_matches >= 3 {
penalty += 0.1 * frag_matches as f64;
}
(1.0 - penalty).max(0.0)
}
pub fn parse_score(cells: &[TextCell]) -> Option<f64> {
let scores: Vec<Option<f64>> = cells
.iter()
.map(|c| Some(rate_text_quality(&c.text)))
.collect();
nanquantile(&scores, 0.10)
}
pub fn layout_score(regions: &[Region], ocr_mean: Option<f64>) -> Option<f64> {
if regions.is_empty() {
return Some(0.0);
}
let scores: Vec<Option<f64>> = regions
.iter()
.map(|r| {
if r.score == 0.0 {
Some(ocr_mean.unwrap_or(1.0))
} else {
Some(r.score as f64)
}
})
.collect();
nanmean(&scores)
}
pub fn ocr_score(confs: &[f32]) -> Option<f64> {
if confs.is_empty() {
return None;
}
Some(confs.iter().map(|&c| c as f64).sum::<f64>() / confs.len() as f64)
}
pub fn page_confidence(
parse: Option<f64>,
regions: &[Region],
ocr_confs: &[f32],
) -> PageConfidence {
let ocr = ocr_score(ocr_confs);
PageConfidence {
parse_score: parse,
layout_score: layout_score(regions, ocr),
table_score: None,
ocr_score: ocr,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_quality_matches_docling_rules() {
assert_eq!(rate_text_quality("A perfectly ordinary sentence."), 1.0);
assert_eq!(rate_text_quality("bad \u{fffd} char"), 0.0);
assert_eq!(rate_text_quality("see GLYPH<0a3F> here"), 0.0);
assert_eq!(rate_text_quality("/G12/G34 rest"), 0.0);
assert_eq!(rate_text_quality("/x1 /y2 tail"), 0.0);
assert_eq!(rate_text_quality("word /x1 /y2"), 1.0);
let frag = "W/or.ds/sp.lit";
assert_eq!(rate_text_quality(frag), 1.0, "one match is tolerated");
assert_eq!(rate_text_quality(&format!("{frag} {frag}")), 1.0);
let three = format!("{frag} {frag} {frag}");
assert!((rate_text_quality(&three) - 0.7).abs() < 1e-12, "{three}");
}
#[test]
fn parse_score_is_tenth_percentile() {
let cell = |text: &str| TextCell {
text: text.into(),
l: 0.0,
t: 0.0,
r: 1.0,
b: 1.0,
};
assert_eq!(parse_score(&[]), None);
let mut cells = vec![cell("ok"); 9];
cells.push(cell("GLYPH<12>"));
let s = parse_score(&cells).unwrap();
assert!((s - 0.9).abs() < 1e-9, "{s}");
}
#[test]
fn layout_score_substitutes_orphan_sentinel() {
let region = |score: f32| Region {
label: "text",
score,
l: 0.0,
t: 0.0,
r: 1.0,
b: 1.0,
};
assert_eq!(layout_score(&[], None), Some(0.0));
let s = layout_score(&[region(0.8), region(0.0)], None).unwrap();
assert!((s - 0.9).abs() < 1e-6, "{s}");
let s = layout_score(&[region(0.8), region(0.0)], Some(0.6)).unwrap();
assert!((s - 0.7).abs() < 1e-6, "{s}");
}
}