use crate::Result;
use crate::core::config::LanguageDetectionConfig;
use crate::types::LanguageConfidence;
use whatlang::{Lang, detect};
pub mod processor;
pub use processor::LanguageDetector;
const AGGREGATE_RELIABLE_THRESHOLD: f64 = 0.9;
pub(crate) const CHUNK_SIZE: usize = 200;
#[cfg(all(feature = "pdf", any(feature = "ocr", feature = "ocr-pipeline")))]
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct ChunkReliability {
pub(crate) chunks: usize,
pub(crate) reliable_chunks: usize,
pub(crate) confidence_sum: f64,
}
#[cfg(all(feature = "pdf", any(feature = "ocr", feature = "ocr-pipeline")))]
impl ChunkReliability {
pub(crate) fn reliable_ratio(&self) -> f64 {
if self.chunks == 0 {
0.0
} else {
self.reliable_chunks as f64 / self.chunks as f64
}
}
pub(crate) fn mean_confidence(&self) -> f64 {
if self.chunks == 0 {
0.0
} else {
self.confidence_sum / self.chunks as f64
}
}
}
#[cfg(all(feature = "pdf", any(feature = "ocr", feature = "ocr-pipeline")))]
pub(crate) fn chunk_reliability(chunks: &[&str]) -> ChunkReliability {
let mut reliable_chunks = 0usize;
let mut confidence_sum = 0.0f64;
for chunk in chunks {
if let Some(info) = detect(chunk) {
confidence_sum += info.confidence();
if info.is_reliable() {
reliable_chunks += 1;
}
}
}
ChunkReliability {
chunks: chunks.len(),
reliable_chunks,
confidence_sum,
}
}
pub(crate) fn detect_languages(text: &str, config: &LanguageDetectionConfig) -> Result<Option<Vec<String>>> {
Ok(detect_language_details(text, config)?
.map(|details| details.into_iter().map(|detail| detail.language).collect()))
}
pub(crate) fn detect_language_details(
text: &str,
config: &LanguageDetectionConfig,
) -> Result<Option<Vec<LanguageConfidence>>> {
if !config.enabled {
return Ok(None);
}
if text.trim().is_empty() {
return Ok(None);
}
if !config.detect_multiple {
return detect_single_language_details(text, config);
}
detect_multiple_languages_details(text, config)
}
fn detect_single_language_details(
text: &str,
config: &LanguageDetectionConfig,
) -> Result<Option<Vec<LanguageConfidence>>> {
match detect(text) {
Some(info) => {
if info.confidence() >= config.min_confidence {
Ok(Some(vec![LanguageConfidence {
language: lang_to_iso639_3(info.lang()),
confidence: info.confidence(),
proportion: 1.0,
script: info.script().name().to_string(),
reliable: info.is_reliable(),
}]))
} else {
Ok(None)
}
}
None => Ok(None),
}
}
struct LangAggregate {
count: usize,
confidence_sum: f64,
script: whatlang::Script,
}
fn detect_multiple_languages_details(
text: &str,
config: &LanguageDetectionConfig,
) -> Result<Option<Vec<LanguageConfidence>>> {
let char_vec: Vec<char> = text.chars().collect();
let chunk_strings: Vec<String> = char_vec
.chunks(CHUNK_SIZE)
.map(|chunk| chunk.iter().collect::<String>())
.collect();
if chunk_strings.is_empty() {
return Ok(None);
}
let mut lang_aggregates: ahash::AHashMap<Lang, LangAggregate> = ahash::AHashMap::new();
let threshold = config.min_confidence;
for chunk in &chunk_strings {
if let Some(info) = detect(chunk)
&& info.confidence() >= threshold
{
let aggregate = lang_aggregates.entry(info.lang()).or_insert(LangAggregate {
count: 0,
confidence_sum: 0.0,
script: info.script(),
});
aggregate.count += 1;
aggregate.confidence_sum += info.confidence();
aggregate.script = info.script();
}
}
if lang_aggregates.is_empty() {
return detect_single_language_details(text, config);
}
let total_chunks = chunk_strings.len() as f64;
let mut lang_vec: Vec<(Lang, LangAggregate)> = lang_aggregates.into_iter().collect();
lang_vec.sort_by(|a, b| {
b.1.count
.cmp(&a.1.count)
.then_with(|| lang_to_iso639_3(a.0).cmp(&lang_to_iso639_3(b.0)))
});
let details = lang_vec
.into_iter()
.map(|(lang, aggregate)| {
let confidence = aggregate.confidence_sum / aggregate.count as f64;
LanguageConfidence {
language: lang_to_iso639_3(lang),
confidence,
proportion: aggregate.count as f64 / total_chunks,
script: aggregate.script.name().to_string(),
reliable: confidence > AGGREGATE_RELIABLE_THRESHOLD,
}
})
.collect();
Ok(Some(details))
}
fn lang_to_iso639_3(lang: Lang) -> String {
match lang {
Lang::Eng => "eng",
Lang::Rus => "rus",
Lang::Cmn => "cmn",
Lang::Spa => "spa",
Lang::Por => "por",
Lang::Ita => "ita",
Lang::Fra => "fra",
Lang::Deu => "deu",
Lang::Ukr => "ukr",
Lang::Kat => "kat",
Lang::Ara => "ara",
Lang::Hin => "hin",
Lang::Jpn => "jpn",
Lang::Heb => "heb",
Lang::Yid => "yid",
Lang::Pol => "pol",
Lang::Amh => "amh",
Lang::Jav => "jav",
Lang::Kor => "kor",
Lang::Nob => "nob",
Lang::Dan => "dan",
Lang::Swe => "swe",
Lang::Fin => "fin",
Lang::Tur => "tur",
Lang::Nld => "nld",
Lang::Hun => "hun",
Lang::Ces => "ces",
Lang::Ell => "ell",
Lang::Bul => "bul",
Lang::Bel => "bel",
Lang::Mar => "mar",
Lang::Kan => "kan",
Lang::Ron => "ron",
Lang::Slv => "slv",
Lang::Hrv => "hrv",
Lang::Srp => "srp",
Lang::Mkd => "mkd",
Lang::Lit => "lit",
Lang::Lav => "lav",
Lang::Est => "est",
Lang::Tam => "tam",
Lang::Vie => "vie",
Lang::Urd => "urd",
Lang::Tha => "tha",
Lang::Guj => "guj",
Lang::Uzb => "uzb",
Lang::Pan => "pan",
Lang::Aze => "aze",
Lang::Ind => "ind",
Lang::Tel => "tel",
Lang::Pes => "pes",
Lang::Mal => "mal",
Lang::Ori => "ori",
Lang::Mya => "mya",
Lang::Nep => "nep",
Lang::Sin => "sin",
Lang::Khm => "khm",
Lang::Tuk => "tuk",
Lang::Aka => "aka",
Lang::Zul => "zul",
Lang::Sna => "sna",
Lang::Afr => "afr",
Lang::Lat => "lat",
Lang::Slk => "slk",
Lang::Cat => "cat",
Lang::Tgl => "tgl",
Lang::Hye => "hye",
Lang::Epo => "epo",
Lang::Ben => "ben",
Lang::Cym => "cym",
}
.to_string()
}
#[cfg(test)]
mod tests;