use crate::core::geometry::TBox;
use crate::core::image::OcrImage;
use crate::core::recognition::TrainableModel;
use crate::utils::{OcrError, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecognitionResult {
pub text: String,
pub confidence: f32,
pub bounding_boxes: Vec<TBox>,
pub character_results: Vec<CharacterRecognitionResult>,
pub word_results: Vec<WordRecognitionResult>,
pub line_results: Vec<LineRecognitionResult>,
pub language: Option<String>,
pub model_type: ModelType,
pub processing_time_ms: u64,
}
impl RecognitionResult {
pub fn new(text: String, confidence: f32) -> Self {
Self {
text,
confidence,
bounding_boxes: Vec::new(),
character_results: Vec::new(),
word_results: Vec::new(),
line_results: Vec::new(),
language: None,
model_type: ModelType::LSTM,
processing_time_ms: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CharacterRecognitionResult {
pub character: char,
pub confidence: f32,
pub bounding_box: TBox,
pub unicode_category: UnicodeCategory,
pub script: ScriptType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WordRecognitionResult {
pub text: String,
pub confidence: f32,
pub bounding_box: TBox,
pub characters: Vec<CharacterRecognitionResult>,
pub language: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LineRecognitionResult {
pub text: String,
pub confidence: f32,
pub bounding_box: TBox,
pub words: Vec<WordRecognitionResult>,
pub reading_order: ReadingOrder,
}
use crate::core::ModelType;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum UnicodeCategory {
Latin,
LatinExtended,
CJKUnifiedIdeographs,
CJKUnifiedIdeographsExtensionA,
CJKUnifiedIdeographsExtensionB,
CJKUnifiedIdeographsExtensionC,
CJKUnifiedIdeographsExtensionD,
CJKUnifiedIdeographsExtensionE,
CJKUnifiedIdeographsExtensionF,
CJKUnifiedIdeographsExtensionG,
CJKUnifiedIdeographsExtensionH,
CJKUnifiedIdeographsExtensionI,
Hiragana,
Katakana,
KatakanaPhoneticExtensions,
HangulSyllables,
HangulJamo,
HangulCompatibilityJamo,
CJKRadicals,
CJKStrokes,
CJKSymbols,
CJKCompatibility,
Arabic,
Devanagari,
Cyrillic,
Greek,
Hebrew,
Thai,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ScriptType {
Latin,
Chinese,
Japanese,
Korean,
Arabic,
Devanagari,
Cyrillic,
Greek,
Hebrew,
Thai,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ReadingOrder {
LeftToRight,
RightToLeft,
TopToBottom,
BottomToTop,
Mixed,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum LanguageVariant {
English,
ChineseSimplified,
ChineseTraditional,
Japanese,
Korean,
Arabic,
Hindi,
Russian,
Other(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelConfig {
pub model_type: ModelType,
pub model_path: String,
pub supported_languages: Vec<LanguageVariant>,
pub input_shape: (usize, usize, usize), pub max_text_length: Option<usize>,
pub confidence_threshold: f32,
pub device: DeviceType,
pub quantization: Option<QuantizationType>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DeviceType {
CPU,
GPU,
NPU, Auto,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum QuantizationType {
FP32,
FP16,
INT8,
Dynamic,
}
pub trait OcrModel: Send + Sync {
fn predict(&self, input: &[u8]) -> Result<RecognitionResult>;
fn model_type(&self) -> ModelType;
fn supported_languages(&self) -> Vec<LanguageVariant>;
fn input_shape(&self) -> (usize, usize, usize);
fn config(&self) -> &ModelConfig;
fn supports_language(&self, language: &LanguageVariant) -> bool;
fn as_trainable(&mut self) -> Option<&mut dyn TrainableModel> {
None
}
fn as_trainable_ref(&self) -> Option<&dyn TrainableModel> {
None
}
}
#[allow(async_fn_in_trait)]
pub trait RecognitionEngine: Send + Sync {
async fn recognize(&self, image: &OcrImage) -> Result<RecognitionResult>;
async fn recognize_region(&self, image: &OcrImage, region: &TBox) -> Result<RecognitionResult>;
async fn recognize_with_language(
&self,
image: &OcrImage,
language_hint: Option<LanguageVariant>,
) -> Result<RecognitionResult>;
fn model_type(&self) -> ModelType;
fn supported_languages(&self) -> Vec<LanguageVariant>;
async fn switch_model(&mut self, model_type: ModelType) -> Result<()>;
}
pub struct ModelManager {
models: HashMap<ModelType, Box<dyn OcrModel>>,
active_model: Option<ModelType>,
device: DeviceType,
backend: Option<Box<dyn crate::compute::ComputeBackend>>,
}
impl ModelManager {
pub fn new(device: DeviceType) -> Self {
let backend = match device {
DeviceType::CPU => {
Some(crate::compute::create_backend(crate::compute::BackendType::Cpu).ok())
.flatten()
}
DeviceType::GPU => Some(crate::compute::create_auto_backend().ok()).flatten(),
DeviceType::NPU => {
Some(crate::compute::create_backend(crate::compute::BackendType::Cpu).ok())
.flatten()
}
DeviceType::Auto => Some(crate::compute::create_auto_backend().ok()).flatten(),
};
let actual_device = backend
.as_ref()
.map(|b| match b.backend_type() {
crate::compute::BackendType::Cpu => DeviceType::CPU,
#[cfg(feature = "cuda")]
crate::compute::BackendType::Cuda => DeviceType::GPU,
#[cfg(feature = "opencl")]
crate::compute::BackendType::OpenCl => DeviceType::GPU,
})
.unwrap_or(DeviceType::CPU);
Self {
models: HashMap::new(),
active_model: None,
device: actual_device,
backend,
}
}
pub fn backend(&self) -> Option<&dyn crate::compute::ComputeBackend> {
self.backend.as_ref().map(|b| b.as_ref())
}
pub fn device(&self) -> DeviceType {
self.device
}
pub fn backend_info(&self) -> String {
match &self.backend {
Some(b) => format!("{} ({})", b.backend_type().name(), b.device_name()),
None => "CPU (fallback)".to_string(),
}
}
pub async fn load_model<M: OcrModel + 'static>(&mut self, model: M) -> Result<()> {
let model_type = model.model_type();
self.models.insert(model_type.clone(), Box::new(model));
if self.active_model.is_none() {
self.active_model = Some(model_type);
}
Ok(())
}
pub fn switch_model(&mut self, model_type: ModelType) -> Result<()> {
if self.models.contains_key(&model_type) {
self.active_model = Some(model_type);
Ok(())
} else {
Err(OcrError::ModelNotFound(format!("Model {:?} not found", model_type)).into())
}
}
pub fn active_model(&self) -> Option<&dyn OcrModel> {
self.active_model
.as_ref()
.and_then(|model_type| self.models.get(model_type).map(|m| m.as_ref()))
}
pub fn get_model(&self, model_type: ModelType) -> Option<&dyn OcrModel> {
self.models.get(&model_type).map(|m| m.as_ref())
}
pub fn available_models(&self) -> Vec<ModelType> {
self.models.keys().cloned().collect()
}
}
pub struct CJKProcessor;
impl CJKProcessor {
pub fn is_cjk_character(c: char) -> bool {
let code = c as u32;
matches!(
code,
0x4E00..=0x9FFF | 0x3400..=0x4DBF | 0x20000..=0x2A6DF | 0x2A700..=0x2B73F | 0x2B740..=0x2B81F | 0x2B820..=0x2CEAF | 0x2CEB0..=0x2EBEF | 0x30000..=0x3134F | 0x31350..=0x323AF | 0x323B0..=0x32B2F | 0x3040..=0x309F | 0x30A0..=0x30FF | 0xAC00..=0xD7AF )
}
pub fn is_chinese_character(c: char) -> bool {
let code = c as u32;
matches!(
code,
0x4E00..=0x9FFF | 0x3400..=0x4DBF | 0x20000..=0x2A6DF | 0x2A700..=0x2B73F | 0x2B740..=0x2B81F | 0x2B820..=0x2CEAF | 0x2CEB0..=0x2EBEF | 0x30000..=0x3134F | 0x31350..=0x323AF | 0x323B0..=0x32B2F )
}
pub fn is_japanese_character(c: char) -> bool {
let code = c as u32;
matches!(
code,
0x3040..=0x309F | 0x30A0..=0x30FF | 0x4E00..=0x9FFF )
}
pub fn is_korean_character(c: char) -> bool {
let code = c as u32;
matches!(
code,
0xAC00..=0xD7AF | 0x1100..=0x11FF | 0x3130..=0x318F )
}
pub fn get_unicode_category(c: char) -> UnicodeCategory {
let code = c as u32;
match code {
0x4E00..=0x9FFF => UnicodeCategory::CJKUnifiedIdeographs,
0x3400..=0x4DBF => UnicodeCategory::CJKUnifiedIdeographsExtensionA,
0x20000..=0x2A6DF => UnicodeCategory::CJKUnifiedIdeographsExtensionB,
0x3040..=0x309F => UnicodeCategory::Hiragana,
0x30A0..=0x30FF => UnicodeCategory::Katakana,
0xAC00..=0xD7AF => UnicodeCategory::HangulSyllables,
0x1100..=0x11FF => UnicodeCategory::HangulJamo,
0x3130..=0x318F => UnicodeCategory::HangulCompatibilityJamo,
0x0000..=0x007F => UnicodeCategory::Latin,
0x0080..=0x00FF => UnicodeCategory::LatinExtended,
_ => UnicodeCategory::Other,
}
}
pub fn get_script_type(c: char) -> ScriptType {
if Self::is_chinese_character(c) {
ScriptType::Chinese
} else if Self::is_japanese_character(c) {
ScriptType::Japanese
} else if Self::is_korean_character(c) {
ScriptType::Korean
} else {
ScriptType::Latin
}
}
}
pub struct BasicRecognitionEngine {
config: ModelConfig,
}
impl BasicRecognitionEngine {
pub fn new(config: ModelConfig) -> Self {
Self { config }
}
}
impl RecognitionEngine for BasicRecognitionEngine {
async fn recognize(&self, image: &OcrImage) -> Result<RecognitionResult> {
let core_result = super::basic_ocr::BasicOcrEngine::new().recognize_sync(image)?;
let bbox = TBox::new(0, 0, image.width as i32, image.height as i32);
Ok(Self::convert_core_result(
core_result,
bbox,
self.config.model_type.clone(),
))
}
async fn recognize_region(&self, image: &OcrImage, region: &TBox) -> Result<RecognitionResult> {
let left = region.left().min(region.right()).max(0) as u32;
let right = region.left().max(region.right()).min(image.width as i32) as u32;
let top = region.bottom().min(region.top()).max(0) as u32;
let bottom = region.bottom().max(region.top()).min(image.height as i32) as u32;
if right <= left || bottom <= top {
return Ok(RecognitionResult::new(String::new(), 0.0));
}
let cropped = image.crop(left, top, right - left, bottom - top)?;
let core_result = super::basic_ocr::BasicOcrEngine::new().recognize_sync(&cropped)?;
let bbox = TBox::new(left as i32, top as i32, right as i32, bottom as i32);
Ok(Self::convert_core_result(
core_result,
bbox,
self.config.model_type.clone(),
))
}
async fn recognize_with_language(
&self,
image: &OcrImage,
language_hint: Option<LanguageVariant>,
) -> Result<RecognitionResult> {
let mut core_result = super::basic_ocr::BasicOcrEngine::new().recognize_sync(image)?;
if let Some(lang) = language_hint {
core_result.language = Some(format!("{:?}", lang));
}
let bbox = TBox::new(0, 0, image.width as i32, image.height as i32);
Ok(Self::convert_core_result(
core_result,
bbox,
self.config.model_type.clone(),
))
}
fn model_type(&self) -> ModelType {
self.config.model_type.clone()
}
fn supported_languages(&self) -> Vec<LanguageVariant> {
self.config.supported_languages.clone()
}
async fn switch_model(&mut self, model_type: ModelType) -> Result<()> {
self.config.model_type = model_type;
Ok(())
}
}
impl BasicRecognitionEngine {
fn convert_core_result(
core_result: crate::core::recognition::RecognitionResult,
bbox: TBox,
model_type: ModelType,
) -> RecognitionResult {
let mut character_results = Vec::new();
for ch in &core_result.characters {
character_results.push(CharacterRecognitionResult {
character: ch.character,
confidence: ch.confidence,
bounding_box: bbox,
unicode_category: CJKProcessor::get_unicode_category(ch.character),
script: CJKProcessor::get_script_type(ch.character),
});
}
let mut word_results = Vec::new();
for w in &core_result.words {
let mut word_chars = Vec::new();
for ch in &w.characters {
word_chars.push(CharacterRecognitionResult {
character: ch.character,
confidence: ch.confidence,
bounding_box: bbox,
unicode_category: CJKProcessor::get_unicode_category(ch.character),
script: CJKProcessor::get_script_type(ch.character),
});
}
word_results.push(WordRecognitionResult {
text: w.word.clone(),
confidence: w.confidence,
bounding_box: bbox,
characters: word_chars,
language: core_result.language.clone(),
});
}
let mut line_results = Vec::new();
for l in &core_result.lines {
line_results.push(LineRecognitionResult {
text: l.line.clone(),
confidence: l.confidence,
bounding_box: bbox,
words: Vec::new(),
reading_order: ReadingOrder::LeftToRight,
});
}
let has_text = !core_result.text.trim().is_empty();
let text = core_result.text;
RecognitionResult {
text,
confidence: core_result.confidence,
bounding_boxes: if has_text { vec![bbox] } else { Vec::new() },
character_results,
word_results,
line_results,
language: core_result.language,
model_type,
processing_time_ms: core_result.processing_time_ms.unwrap_or(0),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cjk_character_detection() {
assert!(CJKProcessor::is_cjk_character('中'));
assert!(CJKProcessor::is_cjk_character('日'));
assert!(CJKProcessor::is_cjk_character('한'));
assert!(CJKProcessor::is_cjk_character('ひ'));
assert!(CJKProcessor::is_cjk_character('カ'));
assert!(!CJKProcessor::is_cjk_character('A'));
assert!(!CJKProcessor::is_cjk_character('1'));
}
#[test]
fn test_language_detection() {
assert!(CJKProcessor::is_chinese_character('中'));
assert!(CJKProcessor::is_japanese_character('ひ'));
assert!(CJKProcessor::is_korean_character('한'));
}
#[test]
fn test_unicode_categories() {
assert_eq!(
CJKProcessor::get_unicode_category('中'),
UnicodeCategory::CJKUnifiedIdeographs
);
assert_eq!(
CJKProcessor::get_unicode_category('ひ'),
UnicodeCategory::Hiragana
);
assert_eq!(
CJKProcessor::get_unicode_category('カ'),
UnicodeCategory::Katakana
);
assert_eq!(
CJKProcessor::get_unicode_category('한'),
UnicodeCategory::HangulSyllables
);
assert_eq!(
CJKProcessor::get_unicode_category('A'),
UnicodeCategory::Latin
);
}
}