Skip to main content

a3s_use_ocr/
provider.rs

1use std::sync::Arc;
2
3use a3s_use_core::{Artifact, Readiness, UseError, UseResult};
4use async_trait::async_trait;
5
6use crate::OcrBlock;
7
8/// Stable identity and execution characteristics of an OCR provider.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct OcrProviderDescriptor {
11    pub id: String,
12    pub engine: String,
13    pub sends_source_off_device: bool,
14}
15
16impl OcrProviderDescriptor {
17    pub fn new(
18        id: impl Into<String>,
19        engine: impl Into<String>,
20        sends_source_off_device: bool,
21    ) -> UseResult<Self> {
22        let descriptor = Self {
23            id: id.into(),
24            engine: engine.into(),
25            sends_source_off_device,
26        };
27        descriptor.validate()?;
28        Ok(descriptor)
29    }
30
31    pub fn validate(&self) -> UseResult<()> {
32        if self.id.is_empty()
33            || self.id.len() > 64
34            || !self
35                .id
36                .bytes()
37                .next()
38                .is_some_and(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
39            || !self.id.bytes().all(|byte| {
40                byte.is_ascii_lowercase()
41                    || byte.is_ascii_digit()
42                    || matches!(byte, b'.' | b'-' | b'_')
43            })
44        {
45            return Err(UseError::new(
46                "use.ocr.provider_descriptor_invalid",
47                "OCR provider IDs must contain 1 through 64 lowercase ASCII letters, digits, dots, hyphens, or underscores and start with a letter or digit.",
48            ));
49        }
50        if self.engine.trim().is_empty() || self.engine.len() > 128 {
51            return Err(UseError::new(
52                "use.ocr.provider_descriptor_invalid",
53                "OCR provider engine names must contain 1 through 128 characters.",
54            ));
55        }
56        Ok(())
57    }
58}
59
60/// Provider-specific readiness projected through the common OCR interface.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct OcrProviderStatus {
63    pub readiness: Readiness,
64    pub model: Option<String>,
65    pub model_dir: Option<std::path::PathBuf>,
66    pub message: String,
67    pub suggestions: Vec<String>,
68}
69
70/// One validated image supplied to an OCR provider.
71#[derive(Debug, Clone)]
72pub struct OcrInput {
73    source: Artifact,
74    bytes: Arc<[u8]>,
75}
76
77impl OcrInput {
78    pub(crate) fn new(source: Artifact, bytes: Vec<u8>) -> Self {
79        Self {
80            source,
81            bytes: bytes.into(),
82        }
83    }
84
85    pub fn source(&self) -> &Artifact {
86        &self.source
87    }
88
89    pub fn bytes(&self) -> &[u8] {
90        &self.bytes
91    }
92}
93
94/// Provider-owned OCR content before the client adds source provenance.
95#[derive(Debug, Clone, PartialEq, Default)]
96pub struct OcrProviderOutput {
97    pub model: Option<String>,
98    pub text: String,
99    pub blocks: Vec<OcrBlock>,
100    pub warnings: Vec<String>,
101}
102
103/// Pluggable OCR implementation.
104///
105/// `OcrClient` validates and hashes the bounded local image before invoking
106/// this interface. Providers only own recognition and provider-specific
107/// readiness; they cannot replace the canonical source evidence.
108#[async_trait]
109pub trait OcrProvider: Send + Sync {
110    fn descriptor(&self) -> OcrProviderDescriptor;
111
112    fn diagnostic(&self) -> OcrProviderStatus;
113
114    async fn recognize(&self, input: OcrInput) -> UseResult<OcrProviderOutput>;
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn provider_ids_are_stable_machine_names() {
123        assert!(OcrProviderDescriptor::new("tesseract", "tesseract", false).is_ok());
124        assert!(OcrProviderDescriptor::new("Azure OCR", "azure", true).is_err());
125        assert!(OcrProviderDescriptor::new("-leading", "fixture", false).is_err());
126    }
127}