use std::panic::{AssertUnwindSafe, catch_unwind};
use std::path::PathBuf;
use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock};
use std::time::{Duration, Instant};
use pagesift_pdf::{OcrInfo, OcrLine, OcrOutput, PageOcr};
use crate::device::{Device, ModelChoice, Quality, select_models};
use crate::math::Math;
use crate::models::{FORMULA, LAYOUT, Model, SMALL_REC};
use crate::pipeline::Engine;
use crate::{Image, Line};
pub type Fetch = Arc<dyn Fn(&Model) -> Result<(), String> + Send + Sync>;
pub struct PdfOcrConfig {
pub root: PathBuf,
pub quality: Quality,
pub device: Device,
pub gpu_required: bool,
pub formulas: bool,
pub fetch: Option<Fetch>,
pub background_download: bool,
}
const RETRY_AFTER: Duration = Duration::from_secs(60);
enum Attempt {
Idle,
Loading,
Downloading,
Failed {
error: String,
at: Instant,
},
}
struct Gate {
attempt: Mutex<Attempt>,
changed: Condvar,
}
impl Gate {
fn lock(&self) -> MutexGuard<'_, Attempt> {
self.attempt.lock().unwrap_or_else(|e| e.into_inner())
}
fn finish(&self, next: Attempt) {
*self.lock() = next;
self.changed.notify_all();
}
}
fn failed(error: String) -> Attempt {
Attempt::Failed {
error,
at: Instant::now(),
}
}
struct Loaded {
engine: Engine,
choice: ModelChoice,
}
pub struct PdfOcr {
config: PdfOcrConfig,
ready: OnceLock<Loaded>,
gate: Arc<Gate>,
retry_after: Duration,
}
impl PdfOcr {
pub fn new(config: PdfOcrConfig) -> Self {
Self {
config,
ready: OnceLock::new(),
gate: Arc::new(Gate {
attempt: Mutex::new(Attempt::Idle),
changed: Condvar::new(),
}),
retry_after: RETRY_AFTER,
}
}
pub fn engine(&self) -> Option<&Engine> {
self.ready.get().map(|l| &l.engine)
}
pub fn from_dirs(
det_dir: PathBuf,
rec_dir: PathBuf,
math_dirs: Option<(PathBuf, PathBuf)>,
device: Device,
) -> Result<Self, String> {
let this = Self::new(PdfOcrConfig {
root: PathBuf::new(),
quality: Quality::Fast,
device,
gpu_required: false,
formulas: false,
fetch: None,
background_download: false,
});
let mut engine = Engine::load_with(&det_dir, &rec_dir, device)?;
if let Some((layout, formula)) = math_dirs {
engine = engine.with_math(Math::load(&layout, &formula)?);
}
let choice = select_models(Quality::Fast, device);
let _ = this.ready.set(Loaded { engine, choice });
Ok(this)
}
fn loaded(&self, deadline: Option<Instant>) -> Result<&Loaded, String> {
if let Some(l) = self.ready.get() {
return Ok(l);
}
let mut attempt = self.gate.lock();
loop {
if let Some(l) = self.ready.get() {
return Ok(l);
}
match &*attempt {
Attempt::Idle => break,
Attempt::Downloading => {
return Err("OCR models are still downloading".into());
}
Attempt::Failed { error, at } if at.elapsed() < self.retry_after => {
return Err(error.clone());
}
Attempt::Failed { .. } => break,
Attempt::Loading => {}
}
attempt = match deadline {
None => self
.gate
.changed
.wait(attempt)
.unwrap_or_else(|e| e.into_inner()),
Some(d) => {
let wait = d.saturating_duration_since(Instant::now());
if wait.is_zero() {
return Err("the OCR deadline passed while models were loading".into());
}
self.gate
.changed
.wait_timeout(attempt, wait)
.unwrap_or_else(|e| e.into_inner())
.0
}
};
}
*attempt = Attempt::Loading;
drop(attempt);
self.load_or_download()
}
fn load_or_download(&self) -> Result<&Loaded, String> {
let c = &self.config;
let result = catch_unwind(AssertUnwindSafe(|| {
let choice = select_models(c.quality, c.device);
let math: &[Model] = if c.formulas { &[LAYOUT, FORMULA] } else { &[] };
let missing: Vec<Model> = [&choice.det, &choice.rec]
.into_iter()
.chain(math)
.filter(|m| !m.present(&c.root))
.copied()
.collect();
if !missing.is_empty() {
let Some(fetch) = &c.fetch else {
return Err(format!(
"model {} is missing from {}; run `pagesift models fetch`",
missing[0].repo,
c.root.display()
));
};
if c.background_download {
return self
.download_in_background(fetch.clone(), missing)
.map(|()| None);
}
missing.iter().try_for_each(|m| fetch(m))?;
}
self.load(choice).map(Some)
}))
.unwrap_or_else(|_| Err("loading the OCR models panicked".into()));
match result {
Ok(Some(loaded)) => {
let _ = self.ready.set(loaded);
self.gate.finish(Attempt::Idle);
self.ready
.get()
.ok_or_else(|| "the OCR models are not loaded".to_owned())
}
Ok(None) => Err("OCR models are still downloading".into()),
Err(error) => {
self.gate.finish(failed(error.clone()));
Err(error)
}
}
}
fn download_in_background(&self, fetch: Fetch, missing: Vec<Model>) -> Result<(), String> {
*self.gate.lock() = Attempt::Downloading;
let gate = self.gate.clone();
std::thread::Builder::new()
.name("pagesift-ocr-download".into())
.spawn(move || {
let done = catch_unwind(AssertUnwindSafe(|| {
missing.iter().try_for_each(|m| fetch(m))
}))
.unwrap_or_else(|_| Err("downloading the OCR models panicked".into()));
gate.finish(match done {
Ok(()) => Attempt::Idle,
Err(error) => failed(error),
});
})
.map(|_| ())
.map_err(|e| format!("failed to start the model download thread: {e}"))
}
fn load(&self, choice: ModelChoice) -> Result<Loaded, String> {
let c = &self.config;
let mut engine =
Engine::load_with(&choice.det.dir(&c.root), &choice.rec.dir(&c.root), c.device)?;
if c.formulas {
engine = engine.with_math(Math::load(&LAYOUT.dir(&c.root), &FORMULA.dir(&c.root))?);
}
Ok(Loaded { engine, choice })
}
}
impl std::fmt::Debug for PdfOcr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PdfOcr")
.field("quality", &self.config.quality)
.field("device", &self.config.device)
.finish_non_exhaustive()
}
}
fn to_ocr_line(l: Line) -> OcrLine {
let xs = l.quad.map(|p| p.0);
let ys = l.quad.map(|p| p.1);
let min = |v: [f32; 4]| v.into_iter().fold(f32::MAX, f32::min);
let max = |v: [f32; 4]| v.into_iter().fold(f32::MIN, f32::max);
OcrLine {
x0: min(xs),
y0: min(ys),
x1: max(xs),
y1: max(ys),
formula: l.formula,
text: l.text,
score: l.score,
}
}
impl PageOcr for PdfOcr {
fn prepare(&self, deadline: Option<Instant>) -> Result<(), String> {
self.loaded(deadline).map(|_| ())
}
fn recognize(
&self,
width: u32,
height: u32,
gray: &[u8],
deadline: Option<Instant>,
) -> Result<OcrOutput, String> {
let loaded = self.loaded(deadline)?;
let img = Image {
width: width as usize,
height: height as usize,
channels: 1,
data: gray.to_vec(),
};
let run = loaded.engine.run_until(&img, deadline)?;
let expected = self.config.gpu_required || pagesift_gpu::device_count() > 0;
Ok(OcrOutput {
lines: run.lines.into_iter().map(to_ocr_line).collect(),
device: Some(device_name(run.device)),
fallback: run.fallback.filter(|_| expected),
partial_error: run.partial_error,
})
}
fn info(&self) -> OcrInfo {
let mut info = OcrInfo {
engine: ENGINE.into(),
model: SMALL_REC.repo.into(),
device: "cpu".into(),
..Default::default()
};
match self.ready.get() {
None => {
if let Attempt::Failed { error, .. } = &*self.gate.lock() {
info.error = Some(error.clone());
}
}
Some(l) => {
info.model = l.choice.rec.repo.into();
let status = l.engine.device_status();
info.device = device_name(status.actual);
let expected = self.config.gpu_required || pagesift_gpu::device_count() > 0;
if status.fell_back() && expected {
info.fallback = status.fallback;
}
}
}
info
}
}
const ENGINE: &str = "pp-ocrv6";
fn device_name(d: Device) -> String {
match d {
Device::Cpu => "cpu".into(),
Device::Gpu { index } => format!("cuda:{index}"),
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc::{Sender, channel};
use super::*;
fn config(fetch: Option<Fetch>, background_download: bool) -> PdfOcrConfig {
PdfOcrConfig {
root: PathBuf::from("/nonexistent/pagesift-ocr-test"),
quality: Quality::Fast,
device: Device::Cpu,
gpu_required: false,
formulas: false,
fetch,
background_download,
}
}
fn counting(calls: &Arc<AtomicUsize>) -> Fetch {
let calls = calls.clone();
Arc::new(move |_: &Model| {
calls.fetch_add(1, Ordering::SeqCst);
Err("network down".to_owned())
})
}
#[test]
fn a_failed_download_is_retried_after_the_backoff() {
let calls = Arc::new(AtomicUsize::new(0));
let mut ocr = PdfOcr::new(config(Some(counting(&calls)), false));
assert_eq!(ocr.prepare(None), Err("network down".to_owned()));
assert_eq!(calls.load(Ordering::SeqCst), 1);
assert_eq!(ocr.prepare(None), Err("network down".to_owned()));
assert_eq!(calls.load(Ordering::SeqCst), 1);
assert_eq!(ocr.info().error.as_deref(), Some("network down"));
ocr.retry_after = Duration::ZERO;
assert_eq!(ocr.prepare(None), Err("network down".to_owned()));
assert_eq!(calls.load(Ordering::SeqCst), 2);
}
#[test]
fn missing_models_without_a_fetcher_say_what_to_do() {
let ocr = PdfOcr::new(config(None, true));
let e = ocr.prepare(None).unwrap_err();
assert!(e.contains("pagesift models fetch"), "{e}");
}
fn wait_for(ocr: &PdfOcr, f: impl Fn(&Attempt) -> bool) {
let end = Instant::now() + Duration::from_secs(30);
let mut a = ocr.gate.lock();
while !f(&a) {
assert!(Instant::now() < end, "等状态超时");
a = ocr
.gate
.changed
.wait_timeout(a, Duration::from_millis(50))
.unwrap()
.0;
}
}
#[test]
fn background_download_never_blocks_the_caller() {
let (go_tx, go_rx) = channel::<()>();
let (seen_tx, seen_rx) = channel::<()>();
let go = Mutex::new(go_rx);
let seen: Mutex<Sender<()>> = Mutex::new(seen_tx);
let fetch: Fetch = Arc::new(move |_: &Model| {
seen.lock().unwrap().send(()).unwrap();
go.lock().unwrap().recv().unwrap();
Err("network down".to_owned())
});
let mut ocr = PdfOcr::new(config(Some(fetch), true));
let e = ocr.prepare(None).unwrap_err();
assert!(e.contains("still downloading"), "{e}");
seen_rx.recv_timeout(Duration::from_secs(30)).unwrap();
let t = Instant::now();
assert!(ocr.prepare(None).unwrap_err().contains("still downloading"));
let e = ocr.recognize(1, 1, &[255], None).unwrap_err();
assert!(e.contains("still downloading"), "{e}");
assert!(t.elapsed() < Duration::from_secs(10));
go_tx.send(()).unwrap();
wait_for(&ocr, |a| matches!(a, Attempt::Failed { .. }));
assert_eq!(ocr.prepare(None), Err("network down".to_owned()));
ocr.retry_after = Duration::ZERO;
assert!(ocr.prepare(None).unwrap_err().contains("still downloading"));
seen_rx.recv_timeout(Duration::from_secs(30)).unwrap();
go_tx.send(()).unwrap();
wait_for(&ocr, |a| matches!(a, Attempt::Failed { .. }));
}
#[test]
fn waiters_give_up_at_their_deadline() {
let ocr = PdfOcr::new(config(None, false));
*ocr.gate.lock() = Attempt::Loading;
let e = ocr
.prepare(Some(Instant::now() + Duration::from_millis(20)))
.unwrap_err();
assert!(e.contains("deadline"), "{e}");
}
}