use std::ops::Range;
use gaze_types::{LocaleTag, PiiClass};
use thiserror::Error;
pub trait LocaleAwareModel: Send + Sync {
fn name(&self) -> &str;
fn native_locales(&self) -> &[LocaleTag];
fn infer(&self, input: ModelInput, hints: ModelHints) -> Result<Vec<ModelSpan>, ModelError>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelInput {
pub text: String,
pub locale: LocaleTag,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ModelHints {
pub stage: ModelStage,
pub max_spans: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelStage {
Pass2Ner,
Pass3SafetyNet,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ModelSpan {
pub text: String,
pub byte_range: Range<usize>,
pub class: PiiClass,
pub confidence: Option<f32>,
pub model_name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum ModelError {
#[error("backend init failed: {0}")]
InitFailed(String),
#[error("inference failed: {0}")]
InferenceFailed(String),
#[error("locale not supported: {0:?}")]
LocaleNotSupported(LocaleTag),
#[error("model integrity mismatch")]
IntegrityMismatch,
#[error("no locale model coverage for {locale:?} at {stage:?}")]
NoLocaleModelCoverage {
locale: LocaleTag,
stage: ModelStage,
},
#[error("backend internal error: {0}")]
Internal(String),
}
#[derive(Default)]
pub struct LocaleAwareModelRegistry {
backends: Vec<Box<dyn LocaleAwareModel>>,
}
impl LocaleAwareModelRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn from_backends(backends: Vec<Box<dyn LocaleAwareModel>>) -> Self {
Self { backends }
}
pub fn register(&mut self, backend: impl LocaleAwareModel + 'static) {
self.backends.push(Box::new(backend));
}
pub fn len(&self) -> usize {
self.backends.len()
}
pub fn is_empty(&self) -> bool {
self.backends.is_empty()
}
pub fn resolve(
&self,
locale: &LocaleTag,
stage: ModelStage,
) -> Result<Vec<&dyn LocaleAwareModel>, ModelError> {
if let Some(matches) = self.matches_for(locale) {
return Ok(matches);
}
if let Some(parent) = parent_language(locale) {
if let Some(matches) = self.matches_for(&parent) {
return Ok(matches);
}
}
if let Some(matches) = self.matches_for(&LocaleTag::Global) {
return Ok(matches);
}
Err(ModelError::NoLocaleModelCoverage {
locale: locale.clone(),
stage,
})
}
fn matches_for(&self, locale: &LocaleTag) -> Option<Vec<&dyn LocaleAwareModel>> {
let matches: Vec<&dyn LocaleAwareModel> = self
.backends
.iter()
.filter(|backend| backend.native_locales().contains(locale))
.map(|backend| backend.as_ref())
.collect();
(!matches.is_empty()).then_some(matches)
}
}
fn parent_language(locale: &LocaleTag) -> Option<LocaleTag> {
match locale {
LocaleTag::DeDe | LocaleTag::DeAt | LocaleTag::DeCh => {
Some(LocaleTag::Other("de".to_string()))
}
LocaleTag::EnUs | LocaleTag::EnGb | LocaleTag::EnIe | LocaleTag::EnAu | LocaleTag::EnCa => {
Some(LocaleTag::Other("en".to_string()))
}
LocaleTag::Other(raw) => raw
.split_once('-')
.map(|(language, _)| LocaleTag::Other(language.to_ascii_lowercase())),
LocaleTag::Global => None,
_ => None,
}
}