1use std::sync::Arc;
2
3use a3s_use_core::{Artifact, Readiness, UseError, UseResult};
4use async_trait::async_trait;
5
6use crate::OcrBlock;
7
8#[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#[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#[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#[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#[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}