#[cfg(paddle_ocr)]
mod backend;
mod config;
pub(crate) mod model_manager;
#[cfg(all(test, feature = "paddle-ocr-ort", feature = "paddle-ocr-tract"))]
mod tract_parity;
#[cfg(paddle_ocr)]
pub use backend::PaddleOcrBackend;
pub use config::{PaddleInferenceBackend, PaddleLanguage, PaddleOcrConfig};
pub use model_manager::{ModelPaths, RecModelPaths, ResolvedRecModel, SharedModelPaths};
#[cfg(paddle_ocr)]
pub use model_manager::{ModelCacheStats, ModelManager, ModelManifestEntry};
pub const SUPPORTED_LANGUAGES: &[&str] = &[
"ch",
"en",
"french",
"german",
"korean",
"japan",
"chinese_cht",
"latin",
"cyrillic",
"thai",
"greek",
"arabic",
"devanagari",
"tamil",
"telugu",
];
#[cfg(paddle_ocr)]
pub(crate) fn is_language_supported(lang: &str) -> bool {
SUPPORTED_LANGUAGES.contains(&lang)
}
#[cfg(paddle_ocr)]
pub(crate) fn language_to_script_family(paddle_lang: &str) -> &'static str {
match paddle_lang {
"en" => "english",
"ch" | "japan" | "chinese_cht" => "chinese",
"korean" => "korean",
"french" | "german" | "latin" => "latin",
"cyrillic" => "eslav",
"thai" => "thai",
"greek" => "greek",
"arabic" => "arabic",
"devanagari" => "devanagari",
"tamil" => "tamil",
"telugu" => "telugu",
_ => "english",
}
}
#[cfg(paddle_ocr)]
pub(crate) fn map_language_code(xberg_code: &str) -> Option<&'static str> {
match xberg_code {
"ch" | "chi_sim" | "zho" | "zh" | "chinese" => Some("ch"),
"en" | "eng" | "english" => Some("en"),
"fr" | "fra" | "french" => Some("french"),
"de" | "deu" | "german" => Some("german"),
"ko" | "kor" | "korean" => Some("korean"),
"ja" | "jpn" | "jpn_vert" | "japanese" | "japan" => Some("japan"),
"chi_tra" | "zh_tw" | "zh_hant" | "chinese_cht" => Some("chinese_cht"),
"ru" | "rus" | "russian" | "uk" | "ukr" | "ukrainian" | "be" | "bel" | "belarusian" | "cyrillic" => {
Some("cyrillic")
}
"th" | "tha" | "thai" => Some("thai"),
"el" | "ell" | "greek" => Some("greek"),
"ar" | "ara" | "arabic" | "fa" | "fas" | "persian" | "ur" | "urd" | "urdu" => Some("arabic"),
"hi" | "hin" | "hindi" | "mr" | "mar" | "marathi" | "sa" | "san" | "sanskrit" | "ne" | "nep" | "nepali"
| "devanagari" => Some("devanagari"),
"ta" | "tam" | "tamil" => Some("tamil"),
"te" | "tel" | "telugu" => Some("telugu"),
"latin" | "es" | "spa" | "spanish" | "it" | "ita" | "italian" | "pt" | "por" | "portuguese" | "nl" | "nld"
| "dutch" | "pl" | "pol" | "polish" | "sv" | "swe" | "swedish" | "da" | "dan" | "danish" | "no" | "nor"
| "norwegian" | "fi" | "fin" | "finnish" | "cs" | "ces" | "czech" | "sk" | "slk" | "slovak" | "hr" | "hrv"
| "croatian" | "hu" | "hun" | "hungarian" | "ro" | "ron" | "romanian" | "tr" | "tur" | "turkish" | "id"
| "ind" | "indonesian" | "ms" | "msa" | "malay" | "vi" | "vie" | "vietnamese" => Some("latin"),
_ => None,
}
}
#[cfg(paddle_ocr)]
pub(crate) fn select_paddle_language(languages: &[String]) -> (&'static str, Vec<crate::types::ProcessingWarning>) {
use std::borrow::Cow;
let mut warnings = Vec::new();
let primary = languages.first().map(String::as_str).unwrap_or("eng");
let primary_code = map_language_code(primary);
let paddle_lang = match primary_code {
Some("en") => languages
.iter()
.filter_map(|language| map_language_code(language))
.find(|code| matches!(*code, "korean" | "japan"))
.unwrap_or("en"),
Some(code) => code,
None => {
tracing::warn!(
requested = %primary,
"paddle-ocr: requested language is not supported; falling back to the 'en' recognition model"
);
warnings.push(crate::types::ProcessingWarning {
source: Cow::Borrowed("paddle-ocr"),
message: Cow::Owned(format!(
"requested language '{primary}' is not supported by paddle-ocr; falling back to the 'en' recognition model"
)),
});
"en"
}
};
let uncovered: Vec<&str> = languages
.iter()
.skip(usize::from(primary_code.is_none()))
.map(String::as_str)
.filter(|lang| map_language_code(lang).is_none_or(|code| !paddle_model_covers(paddle_lang, code)))
.collect();
if !uncovered.is_empty() {
tracing::warn!(
selected = %paddle_lang,
uncovered = %uncovered.join(", "),
"paddle-ocr: single recognition model per run; requested languages are not covered by the selected model and their text may be dropped"
);
warnings.push(crate::types::ProcessingWarning {
source: Cow::Borrowed("paddle-ocr"),
message: Cow::Owned(format!(
"paddle-ocr uses a single recognition model per run; requested languages [{}] are not covered by the selected '{paddle_lang}' model and their text may be dropped",
uncovered.join(", ")
)),
});
}
(paddle_lang, warnings)
}
#[cfg(paddle_ocr)]
fn paddle_model_covers(selected: &str, requested: &str) -> bool {
language_to_script_family(selected) == language_to_script_family(requested)
|| (matches!(selected, "korean" | "japan") && requested == "en")
}
#[cfg(all(test, paddle_ocr))]
mod language_selection_tests {
use super::select_paddle_language;
fn langs(codes: &[&str]) -> Vec<String> {
codes.iter().map(|c| c.to_string()).collect()
}
#[test]
fn test_single_language_no_warnings() {
let (lang, warnings) = select_paddle_language(&langs(&["rus"]));
assert_eq!(lang, "cyrillic");
assert!(warnings.is_empty());
}
#[test]
fn test_same_family_languages_no_warnings() {
let (lang, warnings) = select_paddle_language(&langs(&["fra", "deu", "spa"]));
assert_eq!(lang, "french");
assert!(warnings.is_empty());
}
#[test]
fn test_cross_family_language_warns() {
let (lang, warnings) = select_paddle_language(&langs(&["eng", "rus"]));
assert_eq!(lang, "en");
assert_eq!(warnings.len(), 1);
assert!(warnings[0].message.contains("rus"));
assert!(warnings[0].message.contains("'en'"));
}
#[test]
fn test_unmapped_primary_falls_back_with_warning() {
let (lang, warnings) = select_paddle_language(&langs(&["xyz"]));
assert_eq!(lang, "en");
assert_eq!(warnings.len(), 1);
assert!(warnings[0].message.contains("xyz"));
}
#[test]
fn test_unmapped_secondary_warns() {
let (lang, warnings) = select_paddle_language(&langs(&["eng", "xyz"]));
assert_eq!(lang, "en");
assert_eq!(warnings.len(), 1);
assert!(warnings[0].message.contains("xyz"));
}
#[test]
fn test_empty_list_defaults_to_english() {
let (lang, warnings) = select_paddle_language(&[]);
assert_eq!(lang, "en");
assert!(warnings.is_empty());
}
#[test]
fn should_map_vertical_japanese_to_japanese_model() {
let (lang, warnings) = select_paddle_language(&langs(&["jpn_vert"]));
assert_eq!(lang, "japan");
assert!(warnings.is_empty());
}
#[test]
fn should_prioritize_korean_model_for_mixed_english_and_korean() {
let (lang, warnings) = select_paddle_language(&langs(&["eng", "kor"]));
assert_eq!(lang, "korean");
assert!(warnings.is_empty());
}
#[test]
fn should_prioritize_japanese_model_for_mixed_english_and_vertical_japanese() {
let (lang, warnings) = select_paddle_language(&langs(&["eng", "jpn_vert"]));
assert_eq!(lang, "japan");
assert!(warnings.is_empty());
}
}