feat(analysis): make OCR the active backend, keep vision dormant
The worker now always dispatches through the in-process ocrs engine. `AnalysisConfig::effective_backend()` returns Ocr regardless of the parsed `ANALYSIS_BACKEND` (warning if `vision` was requested), and `spawn_analysis_daemon` selects on it. The vision dispatcher, client, and readiness probe stay compiled and intact behind the unreachable match arm, so re-enabling is a one-line change. The startup-reclaim regression test no longer leaned on the vision arm to skip model loading; it now points the engine at a missing model path and asserts the orphaned lease is still reclaimed before the (failing) engine build — proving reclaim runs independently of engine readiness. Also gitignore backend/models/*.rten so locally-downloaded ocrs models for native `cargo run` aren't committed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
4
backend/.gitignore
vendored
4
backend/.gitignore
vendored
@@ -1,3 +1,7 @@
|
||||
/target
|
||||
/.sqlx
|
||||
.env
|
||||
|
||||
# Local OCR models for native dev (downloaded, not source)
|
||||
models/
|
||||
*.rten
|
||||
|
||||
@@ -434,7 +434,7 @@ async fn spawn_analysis_daemon(
|
||||
let (dispatcher, readiness): (
|
||||
Arc<dyn crate::analysis::daemon::AnalyzeDispatcher>,
|
||||
Option<Arc<dyn crate::analysis::daemon::VisionReadiness>>,
|
||||
) = match cfg.backend {
|
||||
) = match cfg.effective_backend() {
|
||||
crate::config::AnalysisBackend::Ocr => {
|
||||
// Load the `.rten` models once; a bad path is a loud boot error.
|
||||
let engine = crate::analysis::ocr::OcrsEngine::from_model_paths(
|
||||
@@ -1473,29 +1473,30 @@ mod tests {
|
||||
// Bind to a local so the TempDir lives for the rest of the test.
|
||||
// `tempfile::tempdir().unwrap().path()` would drop the TempDir
|
||||
// at end-of-expression and `LocalStorage` would hold a path to
|
||||
// a deleted directory. (Today this is fine because the dispatch
|
||||
// fails before storage is touched, but it makes the test fragile
|
||||
// to any future code rearrangement.)
|
||||
// a deleted directory.
|
||||
let storage_dir = tempfile::tempdir().unwrap();
|
||||
let storage: Arc<dyn Storage> =
|
||||
Arc::new(LocalStorage::new(storage_dir.path()));
|
||||
let mut cfg = crate::config::AnalysisConfig::default();
|
||||
// Use the vision backend so the daemon doesn't try to load the ocrs
|
||||
// `.rten` models (absent in unit CI). This test only exercises the
|
||||
// pre-worker lease reclaim, which is engine-agnostic; no dispatch runs.
|
||||
cfg.backend = crate::config::AnalysisBackend::Vision;
|
||||
// The worker always runs OCR now (vision is dormant — see
|
||||
// `effective_backend`), and the `.rten` models aren't shipped to unit
|
||||
// CI. Point the engine at a path that can't exist so the *engine build*
|
||||
// fails deterministically. Reclaim runs at the very top of
|
||||
// `spawn_analysis_daemon`, before — and independently of — engine
|
||||
// readiness, so the row must still be reclaimed even though spawn
|
||||
// returns Err. That's exactly the regression this test guards (an
|
||||
// analysis-only deploy must reclaim orphaned leases at startup).
|
||||
cfg.ocr_detection_model = "/nonexistent/text-detection.rten".to_string();
|
||||
cfg.ocr_recognition_model = "/nonexistent/text-recognition.rten".to_string();
|
||||
cfg.workers = 1;
|
||||
cfg.job_timeout = Duration::from_secs(1);
|
||||
let events = Arc::new(crate::analysis::events::AnalysisEvents::new());
|
||||
|
||||
let handle = spawn_analysis_daemon(pool.clone(), storage, &cfg, events)
|
||||
.await
|
||||
.expect("spawn");
|
||||
// Immediately shut down — we're only here to prove reclaim ran.
|
||||
// The workers may briefly pick up the now-pending row; that's
|
||||
// fine, but we cancel before any real dispatch (the LocalStorage
|
||||
// key doesn't exist, so a dispatch would fail anyway).
|
||||
handle.shutdown().await;
|
||||
let spawned = spawn_analysis_daemon(pool.clone(), storage, &cfg, events).await;
|
||||
assert!(
|
||||
spawned.is_err(),
|
||||
"engine build must fail with a missing model path"
|
||||
);
|
||||
|
||||
// The reclaim must have moved the row back to pending with the
|
||||
// attempt refunded (attempts goes from 1 → 0). Reaching it on the
|
||||
|
||||
@@ -262,6 +262,24 @@ impl Default for AnalysisConfig {
|
||||
}
|
||||
|
||||
impl AnalysisConfig {
|
||||
/// The backend the worker actually dispatches through.
|
||||
///
|
||||
/// Vision is **temporarily disabled**: the engine code (`analysis::vision`,
|
||||
/// `RealAnalyzeDispatcher`, the readiness probe) is kept intact but never
|
||||
/// selected. Until it's re-enabled, this returns [`AnalysisBackend::Ocr`]
|
||||
/// regardless of the parsed `backend`, logging a warning if `vision` was
|
||||
/// requested so an env override isn't silently ignored. Re-enabling vision
|
||||
/// is then a one-line change here (return `self.backend`).
|
||||
pub fn effective_backend(&self) -> AnalysisBackend {
|
||||
if self.backend == AnalysisBackend::Vision {
|
||||
tracing::warn!(
|
||||
"ANALYSIS_BACKEND=vision requested but the vision backend is temporarily \
|
||||
disabled; running OCR instead"
|
||||
);
|
||||
}
|
||||
AnalysisBackend::Ocr
|
||||
}
|
||||
|
||||
pub fn from_env() -> Self {
|
||||
let d = AnalysisConfig::default();
|
||||
Self {
|
||||
@@ -840,6 +858,19 @@ mod tests {
|
||||
assert_eq!(AnalysisConfig::from_env().backend, AnalysisBackend::Ocr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_backend_is_ocr_even_when_vision_requested() {
|
||||
// Vision is temporarily disabled: the worker always runs OCR no matter
|
||||
// what `ANALYSIS_BACKEND` parsed to. `backend` still reflects the raw
|
||||
// request (so the override is visible/loggable), but `effective_backend`
|
||||
// is the value the daemon actually dispatches through.
|
||||
let mut cfg = AnalysisConfig::default();
|
||||
cfg.backend = AnalysisBackend::Vision;
|
||||
assert_eq!(cfg.effective_backend(), AnalysisBackend::Ocr);
|
||||
cfg.backend = AnalysisBackend::Ocr;
|
||||
assert_eq!(cfg.effective_backend(), AnalysisBackend::Ocr);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ocr_model_paths_parse_from_env() {
|
||||
let _g = ENV_GUARD.lock().unwrap_or_else(|p| p.into_inner());
|
||||
|
||||
Reference in New Issue
Block a user