1use std::panic::{AssertUnwindSafe, catch_unwind};
9use std::path::PathBuf;
10use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock};
11use std::time::{Duration, Instant};
12
13use pagesift_pdf::{OcrInfo, OcrLine, OcrOutput, PageOcr};
14
15use crate::device::{Device, ModelChoice, Quality, select_models};
16use crate::math::Math;
17use crate::models::{FORMULA, LAYOUT, Model, SMALL_REC};
18use crate::pipeline::Engine;
19use crate::{Image, Line};
20
21pub type Fetch = Arc<dyn Fn(&Model) -> Result<(), String> + Send + Sync>;
23
24pub struct PdfOcrConfig {
26 pub root: PathBuf,
27 pub quality: Quality,
28 pub device: Device,
29 pub gpu_required: bool,
31 pub formulas: bool,
33 pub fetch: Option<Fetch>,
35 pub background_download: bool,
37}
38
39const RETRY_AFTER: Duration = Duration::from_secs(60);
41
42enum Attempt {
44 Idle,
46 Loading,
48 Downloading,
50 Failed {
51 error: String,
52 at: Instant,
53 },
54}
55
56struct Gate {
58 attempt: Mutex<Attempt>,
59 changed: Condvar,
60}
61
62impl Gate {
63 fn lock(&self) -> MutexGuard<'_, Attempt> {
64 self.attempt.lock().unwrap_or_else(|e| e.into_inner())
65 }
66
67 fn finish(&self, next: Attempt) {
68 *self.lock() = next;
69 self.changed.notify_all();
70 }
71}
72
73fn failed(error: String) -> Attempt {
74 Attempt::Failed {
75 error,
76 at: Instant::now(),
77 }
78}
79
80struct Loaded {
81 engine: Engine,
82 choice: ModelChoice,
83}
84
85pub struct PdfOcr {
87 config: PdfOcrConfig,
88 ready: OnceLock<Loaded>,
89 gate: Arc<Gate>,
90 retry_after: Duration,
91}
92
93impl PdfOcr {
94 pub fn new(config: PdfOcrConfig) -> Self {
95 Self {
96 config,
97 ready: OnceLock::new(),
98 gate: Arc::new(Gate {
99 attempt: Mutex::new(Attempt::Idle),
100 changed: Condvar::new(),
101 }),
102 retry_after: RETRY_AFTER,
103 }
104 }
105
106 pub fn engine(&self) -> Option<&Engine> {
108 self.ready.get().map(|l| &l.engine)
109 }
110
111 pub fn from_dirs(
113 det_dir: PathBuf,
114 rec_dir: PathBuf,
115 math_dirs: Option<(PathBuf, PathBuf)>,
116 device: Device,
117 ) -> Result<Self, String> {
118 let this = Self::new(PdfOcrConfig {
119 root: PathBuf::new(),
120 quality: Quality::Fast,
121 device,
122 gpu_required: false,
123 formulas: false,
124 fetch: None,
125 background_download: false,
126 });
127 let mut engine = Engine::load_with(&det_dir, &rec_dir, device)?;
128 if let Some((layout, formula)) = math_dirs {
129 engine = engine.with_math(Math::load(&layout, &formula)?);
130 }
131 let choice = select_models(Quality::Fast, device);
132 let _ = this.ready.set(Loaded { engine, choice });
133 Ok(this)
134 }
135
136 fn loaded(&self, deadline: Option<Instant>) -> Result<&Loaded, String> {
138 if let Some(l) = self.ready.get() {
139 return Ok(l);
140 }
141 let mut attempt = self.gate.lock();
142 loop {
143 if let Some(l) = self.ready.get() {
144 return Ok(l);
145 }
146 match &*attempt {
147 Attempt::Idle => break,
148 Attempt::Downloading => {
149 return Err("OCR models are still downloading".into());
150 }
151 Attempt::Failed { error, at } if at.elapsed() < self.retry_after => {
152 return Err(error.clone());
153 }
154 Attempt::Failed { .. } => break,
155 Attempt::Loading => {}
156 }
157 attempt = match deadline {
158 None => self
159 .gate
160 .changed
161 .wait(attempt)
162 .unwrap_or_else(|e| e.into_inner()),
163 Some(d) => {
164 let wait = d.saturating_duration_since(Instant::now());
165 if wait.is_zero() {
166 return Err("the OCR deadline passed while models were loading".into());
167 }
168 self.gate
169 .changed
170 .wait_timeout(attempt, wait)
171 .unwrap_or_else(|e| e.into_inner())
172 .0
173 }
174 };
175 }
176 *attempt = Attempt::Loading;
177 drop(attempt);
178 self.load_or_download()
179 }
180
181 fn load_or_download(&self) -> Result<&Loaded, String> {
183 let c = &self.config;
184 let result = catch_unwind(AssertUnwindSafe(|| {
185 let choice = select_models(c.quality, c.device);
186 let math: &[Model] = if c.formulas { &[LAYOUT, FORMULA] } else { &[] };
187 let missing: Vec<Model> = [&choice.det, &choice.rec]
188 .into_iter()
189 .chain(math)
190 .filter(|m| !m.present(&c.root))
191 .copied()
192 .collect();
193 if !missing.is_empty() {
194 let Some(fetch) = &c.fetch else {
195 return Err(format!(
196 "model {} is missing from {}; run `pagesift models fetch`",
197 missing[0].repo,
198 c.root.display()
199 ));
200 };
201 if c.background_download {
202 return self
203 .download_in_background(fetch.clone(), missing)
204 .map(|()| None);
205 }
206 missing.iter().try_for_each(|m| fetch(m))?;
207 }
208 self.load(choice).map(Some)
209 }))
210 .unwrap_or_else(|_| Err("loading the OCR models panicked".into()));
211 match result {
212 Ok(Some(loaded)) => {
213 let _ = self.ready.set(loaded);
214 self.gate.finish(Attempt::Idle);
215 self.ready
216 .get()
217 .ok_or_else(|| "the OCR models are not loaded".to_owned())
218 }
219 Ok(None) => Err("OCR models are still downloading".into()),
221 Err(error) => {
222 self.gate.finish(failed(error.clone()));
223 Err(error)
224 }
225 }
226 }
227
228 fn download_in_background(&self, fetch: Fetch, missing: Vec<Model>) -> Result<(), String> {
230 *self.gate.lock() = Attempt::Downloading;
232 let gate = self.gate.clone();
233 std::thread::Builder::new()
234 .name("pagesift-ocr-download".into())
235 .spawn(move || {
236 let done = catch_unwind(AssertUnwindSafe(|| {
237 missing.iter().try_for_each(|m| fetch(m))
238 }))
239 .unwrap_or_else(|_| Err("downloading the OCR models panicked".into()));
240 gate.finish(match done {
241 Ok(()) => Attempt::Idle,
242 Err(error) => failed(error),
243 });
244 })
245 .map(|_| ())
246 .map_err(|e| format!("failed to start the model download thread: {e}"))
247 }
248
249 fn load(&self, choice: ModelChoice) -> Result<Loaded, String> {
250 let c = &self.config;
251 let mut engine =
252 Engine::load_with(&choice.det.dir(&c.root), &choice.rec.dir(&c.root), c.device)?;
253 if c.formulas {
254 engine = engine.with_math(Math::load(&LAYOUT.dir(&c.root), &FORMULA.dir(&c.root))?);
255 }
256 Ok(Loaded { engine, choice })
257 }
258}
259
260impl std::fmt::Debug for PdfOcr {
261 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262 f.debug_struct("PdfOcr")
263 .field("quality", &self.config.quality)
264 .field("device", &self.config.device)
265 .finish_non_exhaustive()
266 }
267}
268
269fn to_ocr_line(l: Line) -> OcrLine {
271 let xs = l.quad.map(|p| p.0);
272 let ys = l.quad.map(|p| p.1);
273 let min = |v: [f32; 4]| v.into_iter().fold(f32::MAX, f32::min);
274 let max = |v: [f32; 4]| v.into_iter().fold(f32::MIN, f32::max);
275 OcrLine {
276 x0: min(xs),
277 y0: min(ys),
278 x1: max(xs),
279 y1: max(ys),
280 formula: l.formula,
281 text: l.text,
282 score: l.score,
283 }
284}
285
286impl PageOcr for PdfOcr {
287 fn prepare(&self, deadline: Option<Instant>) -> Result<(), String> {
288 self.loaded(deadline).map(|_| ())
289 }
290
291 fn recognize(
292 &self,
293 width: u32,
294 height: u32,
295 gray: &[u8],
296 deadline: Option<Instant>,
297 ) -> Result<OcrOutput, String> {
298 let loaded = self.loaded(deadline)?;
299 let img = Image {
300 width: width as usize,
301 height: height as usize,
302 channels: 1,
303 data: gray.to_vec(),
304 };
305 let run = loaded.engine.run_until(&img, deadline)?;
306 let expected = self.config.gpu_required || pagesift_gpu::device_count() > 0;
308 Ok(OcrOutput {
309 lines: run.lines.into_iter().map(to_ocr_line).collect(),
310 device: Some(device_name(run.device)),
311 fallback: run.fallback.filter(|_| expected),
312 partial_error: run.partial_error,
313 })
314 }
315
316 fn info(&self) -> OcrInfo {
317 let mut info = OcrInfo {
318 engine: ENGINE.into(),
319 model: SMALL_REC.repo.into(),
320 device: "cpu".into(),
321 ..Default::default()
322 };
323 match self.ready.get() {
324 None => {
325 if let Attempt::Failed { error, .. } = &*self.gate.lock() {
326 info.error = Some(error.clone());
327 }
328 }
329 Some(l) => {
330 info.model = l.choice.rec.repo.into();
331 let status = l.engine.device_status();
332 info.device = device_name(status.actual);
333 let expected = self.config.gpu_required || pagesift_gpu::device_count() > 0;
335 if status.fell_back() && expected {
336 info.fallback = status.fallback;
337 }
338 }
339 }
340 info
341 }
342}
343
344const ENGINE: &str = "pp-ocrv6";
346
347fn device_name(d: Device) -> String {
349 match d {
350 Device::Cpu => "cpu".into(),
351 Device::Gpu { index } => format!("cuda:{index}"),
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use std::sync::atomic::{AtomicUsize, Ordering};
358 use std::sync::mpsc::{Sender, channel};
359
360 use super::*;
361
362 fn config(fetch: Option<Fetch>, background_download: bool) -> PdfOcrConfig {
364 PdfOcrConfig {
365 root: PathBuf::from("/nonexistent/pagesift-ocr-test"),
366 quality: Quality::Fast,
367 device: Device::Cpu,
368 gpu_required: false,
369 formulas: false,
370 fetch,
371 background_download,
372 }
373 }
374
375 fn counting(calls: &Arc<AtomicUsize>) -> Fetch {
376 let calls = calls.clone();
377 Arc::new(move |_: &Model| {
378 calls.fetch_add(1, Ordering::SeqCst);
379 Err("network down".to_owned())
380 })
381 }
382
383 #[test]
385 fn a_failed_download_is_retried_after_the_backoff() {
386 let calls = Arc::new(AtomicUsize::new(0));
387 let mut ocr = PdfOcr::new(config(Some(counting(&calls)), false));
388 assert_eq!(ocr.prepare(None), Err("network down".to_owned()));
389 assert_eq!(calls.load(Ordering::SeqCst), 1);
390 assert_eq!(ocr.prepare(None), Err("network down".to_owned()));
392 assert_eq!(calls.load(Ordering::SeqCst), 1);
393 assert_eq!(ocr.info().error.as_deref(), Some("network down"));
394 ocr.retry_after = Duration::ZERO;
396 assert_eq!(ocr.prepare(None), Err("network down".to_owned()));
397 assert_eq!(calls.load(Ordering::SeqCst), 2);
398 }
399
400 #[test]
402 fn missing_models_without_a_fetcher_say_what_to_do() {
403 let ocr = PdfOcr::new(config(None, true));
404 let e = ocr.prepare(None).unwrap_err();
405 assert!(e.contains("pagesift models fetch"), "{e}");
406 }
407
408 fn wait_for(ocr: &PdfOcr, f: impl Fn(&Attempt) -> bool) {
409 let end = Instant::now() + Duration::from_secs(30);
411 let mut a = ocr.gate.lock();
412 while !f(&a) {
413 assert!(Instant::now() < end, "等状态超时");
414 a = ocr
415 .gate
416 .changed
417 .wait_timeout(a, Duration::from_millis(50))
418 .unwrap()
419 .0;
420 }
421 }
422
423 #[test]
425 fn background_download_never_blocks_the_caller() {
426 let (go_tx, go_rx) = channel::<()>();
427 let (seen_tx, seen_rx) = channel::<()>();
428 let go = Mutex::new(go_rx);
429 let seen: Mutex<Sender<()>> = Mutex::new(seen_tx);
430 let fetch: Fetch = Arc::new(move |_: &Model| {
431 seen.lock().unwrap().send(()).unwrap();
432 go.lock().unwrap().recv().unwrap();
434 Err("network down".to_owned())
435 });
436 let mut ocr = PdfOcr::new(config(Some(fetch), true));
437 let e = ocr.prepare(None).unwrap_err();
438 assert!(e.contains("still downloading"), "{e}");
439 seen_rx.recv_timeout(Duration::from_secs(30)).unwrap();
440 let t = Instant::now();
443 assert!(ocr.prepare(None).unwrap_err().contains("still downloading"));
444 let e = ocr.recognize(1, 1, &[255], None).unwrap_err();
445 assert!(e.contains("still downloading"), "{e}");
446 assert!(t.elapsed() < Duration::from_secs(10));
447 go_tx.send(()).unwrap();
448 wait_for(&ocr, |a| matches!(a, Attempt::Failed { .. }));
449 assert_eq!(ocr.prepare(None), Err("network down".to_owned()));
450 ocr.retry_after = Duration::ZERO;
452 assert!(ocr.prepare(None).unwrap_err().contains("still downloading"));
453 seen_rx.recv_timeout(Duration::from_secs(30)).unwrap();
454 go_tx.send(()).unwrap();
455 wait_for(&ocr, |a| matches!(a, Attempt::Failed { .. }));
456 }
457
458 #[test]
460 fn waiters_give_up_at_their_deadline() {
461 let ocr = PdfOcr::new(config(None, false));
462 *ocr.gate.lock() = Attempt::Loading;
463 let e = ocr
464 .prepare(Some(Instant::now() + Duration::from_millis(20)))
466 .unwrap_err();
467 assert!(e.contains("deadline"), "{e}");
468 }
469}