use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use anyhow::Result;
use crate::ocr;
use crate::ocr_queue::OcrQueue;
use crate::updater::IndexUpdater;
pub(crate) const MIN_WORKERS: usize = 2;
pub(crate) const MAX_WORKERS: usize = 4;
pub const DEFAULT_WORKERS: usize = MAX_WORKERS;
const IDLE_POLL: Duration = Duration::from_millis(200);
const OCR_BATCH_MAX: usize = 32;
const OCR_BATCH_WINDOW: Duration = Duration::from_secs(5);
pub struct OcrPipeline {
stop: Arc<AtomicBool>,
handles: Vec<JoinHandle<()>>,
}
impl OcrPipeline {
pub fn start(
updater: Arc<Mutex<IndexUpdater>>,
index_dir: PathBuf,
worker_count: usize,
on_progress: impl Fn(usize) + Send + Sync + 'static,
) -> Option<Self> {
if !ocr::is_available() {
eprintln!("未检测到可用的 OCR 语言包,OCR 管线已停用(截图/图片文字不会被索引)");
return None;
}
let queue = OcrQueue::for_index_dir(&index_dir);
let stop = Arc::new(AtomicBool::new(false));
let worker_count = worker_count.clamp(MIN_WORKERS, MAX_WORKERS);
let on_progress: Arc<dyn Fn(usize) + Send + Sync> = Arc::new(on_progress);
let handles = (0..worker_count)
.map(|_| {
let updater = updater.clone();
let queue = queue.clone();
let stop = stop.clone();
let on_progress = on_progress.clone();
std::thread::spawn(move || worker_loop(queue, updater, stop, on_progress))
})
.collect();
Some(Self { stop, handles })
}
pub fn stop(self) {
self.stop.store(true, Ordering::Relaxed);
for handle in self.handles {
let _ = handle.join();
}
}
}
fn worker_loop(
queue: Arc<OcrQueue>,
updater: Arc<Mutex<IndexUpdater>>,
stop: Arc<AtomicBool>,
on_progress: Arc<dyn Fn(usize) + Send + Sync>,
) {
let engine = match ocr::create_engine() {
Ok(engine) => engine,
Err(err) => {
eprintln!("OCR worker 创建引擎失败,该 worker 退出: {err}");
return;
}
};
let mut batch: Vec<(PathBuf, i64, u64, String)> = Vec::with_capacity(OCR_BATCH_MAX);
let mut batch_started: Option<Instant> = None;
loop {
match queue.pop_pending() {
Some((path, mtime, size)) => {
let content = match ocr::recognize(&engine, &path) {
Ok(raw) => ocr::dual_form_content(&raw),
Err(err) => {
eprintln!(
"OCR 识别出错,记为已处理(不重试,避免损坏图片反复卡住队列){}: {err}",
path.display()
);
String::new()
}
};
if batch_started.is_none() {
batch_started = Some(Instant::now());
}
batch.push((path, mtime, size, content));
let window_elapsed = batch_started
.map(|t| t.elapsed() >= OCR_BATCH_WINDOW)
.unwrap_or(false);
if batch.len() >= OCR_BATCH_MAX || window_elapsed {
flush_batch(&mut batch, &queue, &updater, &on_progress);
batch_started = None;
}
}
None => {
if !batch.is_empty() {
flush_batch(&mut batch, &queue, &updater, &on_progress);
batch_started = None;
}
if stop.load(Ordering::Relaxed) {
return;
}
std::thread::sleep(IDLE_POLL);
continue;
}
}
if stop.load(Ordering::Relaxed) {
if !batch.is_empty() {
flush_batch(&mut batch, &queue, &updater, &on_progress);
}
return;
}
}
}
fn flush_batch(
batch: &mut Vec<(PathBuf, i64, u64, String)>,
queue: &Arc<OcrQueue>,
updater: &Mutex<IndexUpdater>,
on_progress: &Arc<dyn Fn(usize) + Send + Sync>,
) {
let items = std::mem::take(batch);
if items.is_empty() {
return;
}
let commit_result = {
let mut guard = updater.lock().unwrap_or_else(|e| e.into_inner());
guard.stage_and_commit_ocr_batch(&items)
};
match commit_result {
Ok(()) => {
for (path, mtime, size, content) in items {
queue.mark_processed(path, mtime, size, content);
}
}
Err(err) => {
eprintln!(
"OCR 批量结果写入索引失败,{} 张退回队列下次重试: {err}",
items.len()
);
for (path, mtime, size, _content) in items {
queue.enqueue(path, mtime, size);
}
}
}
if let Err(err) = queue.save() {
eprintln!("OCR 队列进度落盘失败: {err}");
}
on_progress(queue.pending_len());
}
#[derive(Debug, Clone, Copy, Default)]
pub struct OcrDrainStats {
pub available: bool,
pub processed: usize,
}
const DRAIN_TIMEOUT: Duration = Duration::from_secs(60);
pub fn drain_ocr_queue(index_dir: &std::path::Path, worker_count: usize) -> Result<OcrDrainStats> {
if !ocr::is_available() {
return Ok(OcrDrainStats {
available: false,
processed: 0,
});
}
let queue = OcrQueue::for_index_dir(index_dir);
let before = queue.pending_len();
if before == 0 {
return Ok(OcrDrainStats {
available: true,
processed: 0,
});
}
let updater = Arc::new(Mutex::new(IndexUpdater::open(index_dir)?));
let Some(pipeline) = OcrPipeline::start(updater, index_dir.to_path_buf(), worker_count, |_| {})
else {
return Ok(OcrDrainStats {
available: false,
processed: 0,
});
};
let start = Instant::now();
while queue.pending_len() > 0 && start.elapsed() < DRAIN_TIMEOUT {
std::thread::sleep(Duration::from_millis(50));
}
let remaining = queue.pending_len();
pipeline.stop();
Ok(OcrDrainStats {
available: true,
processed: before.saturating_sub(remaining),
})
}