1use anno::{Error, Model, Result};
38use serde::{Deserialize, Serialize};
39use std::collections::HashMap;
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct MorphemeConfig {
44 pub boundary_char: char,
46 pub char_level_fallback: bool,
48 pub min_morpheme_len: usize,
50}
51
52impl Default for MorphemeConfig {
53 fn default() -> Self {
54 Self {
55 boundary_char: '-',
56 char_level_fallback: true,
57 min_morpheme_len: 1,
58 }
59 }
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct OrthographicConfig {
65 pub unicode_normalize: bool,
67 pub case_insensitive: bool,
69 pub ignore_diacritics: bool,
71 pub char_mappings: HashMap<char, char>,
73}
74
75impl Default for OrthographicConfig {
76 fn default() -> Self {
77 Self {
78 unicode_normalize: true,
79 case_insensitive: false,
80 ignore_diacritics: false,
81 char_mappings: HashMap::new(),
82 }
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct LowResourceResults {
89 pub token_f1: f64,
91 pub morpheme_f1: Option<f64>,
93 pub char_f1: f64,
95 pub entity_density_ratio: f64,
97 pub transfer_efficiency: Option<f64>,
99 pub per_type: HashMap<String, TypeMetrics>,
101 pub normalization_impact: Option<NormalizationImpact>,
103 pub metadata: LowResourceMetadata,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct TypeMetrics {
110 pub precision: f64,
112 pub recall: f64,
114 pub f1: f64,
116 pub support: usize,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct NormalizationImpact {
123 pub raw_f1: f64,
125 pub normalized_f1: f64,
127 pub improvement: f64,
129 pub entities_affected: usize,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct LowResourceMetadata {
136 pub language_code: String,
138 pub language_family: Option<String>,
140 pub is_polysynthetic: bool,
142 pub has_standard_orthography: bool,
144 pub speaker_population: Option<u64>,
146 pub endangerment_level: Option<EndangermentLevel>,
148 pub training_examples: Option<usize>,
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
154pub enum EndangermentLevel {
155 Safe,
157 Vulnerable,
159 DefinitelyEndangered,
161 SeverelyEndangered,
163 CriticallyEndangered,
165 Extinct,
167}
168
169pub struct LowResourceEvaluator {
171 morpheme_config: Option<MorphemeConfig>,
172 orthographic_config: Option<OrthographicConfig>,
173 english_baseline_f1: Option<f64>,
174}
175
176impl LowResourceEvaluator {
177 pub fn new() -> Self {
179 Self {
180 morpheme_config: None,
181 orthographic_config: None,
182 english_baseline_f1: None,
183 }
184 }
185
186 pub fn with_morpheme_boundaries(mut self, config: MorphemeConfig) -> Self {
188 self.morpheme_config = Some(config);
189 self
190 }
191
192 pub fn with_orthographic_normalization(mut self, config: OrthographicConfig) -> Self {
194 self.orthographic_config = Some(config);
195 self
196 }
197
198 pub fn with_english_baseline(mut self, f1: f64) -> Self {
200 self.english_baseline_f1 = Some(f1);
201 self
202 }
203
204 pub fn evaluate(
206 &self,
207 model: &dyn Model,
208 test_cases: &[(String, Vec<super::GoldEntity>)],
209 metadata: LowResourceMetadata,
210 ) -> Result<LowResourceResults> {
211 if test_cases.is_empty() {
212 return Err(Error::InvalidInput("Empty test cases".to_string()));
213 }
214
215 let standard_results = super::evaluate_ner_model(model, test_cases)?;
217
218 let char_f1 = self.calculate_char_f1(model, test_cases)?;
220
221 let morpheme_f1 = if self.morpheme_config.is_some() && metadata.is_polysynthetic {
223 Some(self.calculate_morpheme_f1(model, test_cases)?)
224 } else {
225 None
226 };
227
228 let total_chars: usize = test_cases.iter().map(|(text, _)| text.len()).sum();
230 let total_entities: usize = test_cases.iter().map(|(_, entities)| entities.len()).sum();
231 let entity_density = if total_chars > 0 {
232 total_entities as f64 / total_chars as f64
233 } else {
234 0.0
235 };
236 let english_baseline_density = 0.05;
238 let entity_density_ratio = entity_density / english_baseline_density;
239
240 let transfer_efficiency = self
242 .english_baseline_f1
243 .map(|baseline| standard_results.f1 / baseline);
244
245 let normalization_impact = if self.orthographic_config.is_some() {
247 Some(self.calculate_normalization_impact(model, test_cases)?)
248 } else {
249 None
250 };
251
252 let per_type: HashMap<String, TypeMetrics> = standard_results
254 .per_type
255 .into_iter()
256 .map(|(k, v)| {
257 (
258 k,
259 TypeMetrics {
260 precision: v.precision,
261 recall: v.recall,
262 f1: v.f1,
263 support: v.expected,
264 },
265 )
266 })
267 .collect();
268
269 Ok(LowResourceResults {
270 token_f1: standard_results.f1,
271 morpheme_f1,
272 char_f1,
273 entity_density_ratio,
274 transfer_efficiency,
275 per_type,
276 normalization_impact,
277 metadata,
278 })
279 }
280
281 fn calculate_char_f1(
285 &self,
286 model: &dyn Model,
287 test_cases: &[(String, Vec<super::GoldEntity>)],
288 ) -> Result<f64> {
289 let mut total_gold_chars = 0;
290 let mut total_pred_chars = 0;
291 let mut total_correct_chars = 0;
292
293 for (text, gold_entities) in test_cases {
294 let predictions = model.extract_entities(text, None)?;
296
297 let text_char_len = text.chars().count();
299 let mut gold_mask = vec![false; text_char_len];
300 for entity in gold_entities {
301 let start = entity.start.min(text_char_len);
302 let end = entity.end.min(text_char_len);
303 for slot in gold_mask.iter_mut().take(end).skip(start) {
304 *slot = true;
305 }
306 }
307
308 let mut pred_mask = vec![false; text_char_len];
310 for entity in &predictions {
311 let start = entity.start().min(text_char_len);
312 let end = entity.end().min(text_char_len);
313 for slot in pred_mask.iter_mut().take(end).skip(start) {
314 *slot = true;
315 }
316 }
317
318 for i in 0..text_char_len {
320 if gold_mask[i] {
321 total_gold_chars += 1;
322 }
323 if pred_mask[i] {
324 total_pred_chars += 1;
325 }
326 if gold_mask[i] && pred_mask[i] {
327 total_correct_chars += 1;
328 }
329 }
330 }
331
332 let precision = if total_pred_chars > 0 {
333 total_correct_chars as f64 / total_pred_chars as f64
334 } else {
335 0.0
336 };
337 let recall = if total_gold_chars > 0 {
338 total_correct_chars as f64 / total_gold_chars as f64
339 } else {
340 0.0
341 };
342 let f1 = if precision + recall > 0.0 {
343 2.0 * precision * recall / (precision + recall)
344 } else {
345 0.0
346 };
347
348 Ok(f1)
349 }
350
351 fn calculate_morpheme_f1(
353 &self,
354 model: &dyn Model,
355 test_cases: &[(String, Vec<super::GoldEntity>)],
356 ) -> Result<f64> {
357 let config = self.morpheme_config.as_ref().ok_or_else(|| {
358 Error::evaluation(
359 "morpheme-level evaluation requested without MorphemeConfig (call with_morpheme_boundaries(true))",
360 )
361 })?;
362
363 let mut total_gold_morphemes = 0;
364 let mut total_pred_morphemes = 0;
365 let mut total_correct_morphemes = 0;
366
367 for (text, gold_entities) in test_cases {
368 let predictions = model.extract_entities(text, None)?;
369
370 for entity in gold_entities {
372 let morpheme_count = entity
373 .text
374 .split(config.boundary_char)
375 .filter(|m| m.len() >= config.min_morpheme_len)
376 .count()
377 .max(1);
378 total_gold_morphemes += morpheme_count;
379 }
380
381 for entity in &predictions {
383 let char_count = text.chars().count();
385 let entity_text: String = text
386 .chars()
387 .skip(entity.start())
388 .take(entity.end().min(char_count).saturating_sub(entity.start()))
389 .collect();
390 let morpheme_count = entity_text
391 .split(config.boundary_char)
392 .filter(|m| m.len() >= config.min_morpheme_len)
393 .count()
394 .max(1);
395 total_pred_morphemes += morpheme_count;
396
397 for gold in gold_entities {
399 if entity.start() == gold.start && entity.end() == gold.end {
400 total_correct_morphemes += morpheme_count;
401 break;
402 }
403 }
404 }
405 }
406
407 let precision = if total_pred_morphemes > 0 {
408 total_correct_morphemes as f64 / total_pred_morphemes as f64
409 } else {
410 0.0
411 };
412 let recall = if total_gold_morphemes > 0 {
413 total_correct_morphemes as f64 / total_gold_morphemes as f64
414 } else {
415 0.0
416 };
417 let f1 = if precision + recall > 0.0 {
418 2.0 * precision * recall / (precision + recall)
419 } else {
420 0.0
421 };
422
423 Ok(f1)
424 }
425
426 fn calculate_normalization_impact(
428 &self,
429 model: &dyn Model,
430 test_cases: &[(String, Vec<super::GoldEntity>)],
431 ) -> Result<NormalizationImpact> {
432 let config = self.orthographic_config.as_ref().ok_or_else(|| {
433 Error::evaluation(
434 "normalization impact requested without OrthographicConfig (call with_orthographic_normalization(true))",
435 )
436 })?;
437
438 let raw_results = super::evaluate_ner_model(model, test_cases)?;
440
441 let normalized_cases: Vec<(String, Vec<super::GoldEntity>)> = test_cases
443 .iter()
444 .map(|(text, entities)| {
445 let normalized_text = self.normalize_text(text, config);
446 let normalized_entities: Vec<super::GoldEntity> = entities
447 .iter()
448 .map(|e| super::GoldEntity {
449 text: self.normalize_text(&e.text, config),
450 entity_type: e.entity_type.clone(),
451 original_label: e.original_label.clone(),
452 start: e.start,
453 end: e.end,
454 })
455 .collect();
456 (normalized_text, normalized_entities)
457 })
458 .collect();
459
460 let normalized_results = super::evaluate_ner_model(model, &normalized_cases)?;
462
463 let mut entities_affected = 0;
465 for ((orig_text, _), (norm_text, _)) in test_cases.iter().zip(normalized_cases.iter()) {
466 if orig_text != norm_text {
467 entities_affected += 1;
468 }
469 }
470
471 Ok(NormalizationImpact {
472 raw_f1: raw_results.f1,
473 normalized_f1: normalized_results.f1,
474 improvement: normalized_results.f1 - raw_results.f1,
475 entities_affected,
476 })
477 }
478
479 fn normalize_text(&self, text: &str, config: &OrthographicConfig) -> String {
481 let mut result = text.to_string();
482
483 if config.unicode_normalize {
485 use unicode_normalization::UnicodeNormalization;
486 result = result.nfc().collect();
487 }
488
489 if config.case_insensitive {
491 result = result.to_lowercase();
492 }
493
494 if config.ignore_diacritics {
496 result = remove_diacritics(&result);
497 }
498
499 for (from, to) in &config.char_mappings {
501 result = result.replace(*from, &to.to_string());
502 }
503
504 result
505 }
506}
507
508impl Default for LowResourceEvaluator {
509 fn default() -> Self {
510 Self::new()
511 }
512}
513
514fn remove_diacritics(text: &str) -> String {
516 use unicode_normalization::UnicodeNormalization;
517 text.nfd()
518 .filter(|c| !unicode_normalization::char::is_combining_mark(*c))
519 .collect()
520}
521
522pub fn language_metadata(language_code: &str) -> Option<LowResourceMetadata> {
524 match language_code {
525 "qxo" => Some(LowResourceMetadata {
527 language_code: "qxo".to_string(),
528 language_family: Some("Quechuan".to_string()),
529 is_polysynthetic: false, has_standard_orthography: false,
531 speaker_population: Some(200_000),
532 endangerment_level: Some(EndangermentLevel::Vulnerable),
533 training_examples: Some(12), }),
535 "chr" => Some(LowResourceMetadata {
537 language_code: "chr".to_string(),
538 language_family: Some("Iroquoian".to_string()),
539 is_polysynthetic: true,
540 has_standard_orthography: true, speaker_population: Some(2_000),
542 endangerment_level: Some(EndangermentLevel::SeverelyEndangered),
543 training_examples: None,
544 }),
545 "nav" => Some(LowResourceMetadata {
547 language_code: "nav".to_string(),
548 language_family: Some("Na-Dené".to_string()),
549 is_polysynthetic: true,
550 has_standard_orthography: true,
551 speaker_population: Some(170_000),
552 endangerment_level: Some(EndangermentLevel::Vulnerable),
553 training_examples: None,
554 }),
555 "gn" | "grn" => Some(LowResourceMetadata {
557 language_code: "grn".to_string(),
558 language_family: Some("Tupian".to_string()),
559 is_polysynthetic: false,
560 has_standard_orthography: true,
561 speaker_population: Some(6_000_000),
562 endangerment_level: Some(EndangermentLevel::Safe),
563 training_examples: None,
564 }),
565 "nah" => Some(LowResourceMetadata {
567 language_code: "nah".to_string(),
568 language_family: Some("Uto-Aztecan".to_string()),
569 is_polysynthetic: true,
570 has_standard_orthography: false, speaker_population: Some(1_700_000),
572 endangerment_level: Some(EndangermentLevel::Vulnerable),
573 training_examples: None,
574 }),
575 "shp" => Some(LowResourceMetadata {
577 language_code: "shp".to_string(),
578 language_family: Some("Panoan".to_string()),
579 is_polysynthetic: false,
580 has_standard_orthography: true,
581 speaker_population: Some(35_000),
582 endangerment_level: Some(EndangermentLevel::Vulnerable),
583 training_examples: None,
584 }),
585
586 "sw" | "swa" => Some(LowResourceMetadata {
592 language_code: "swa".to_string(),
593 language_family: Some("Atlantic-Congo (Bantu)".to_string()),
594 is_polysynthetic: false, has_standard_orthography: true,
596 speaker_population: Some(100_000_000), endangerment_level: Some(EndangermentLevel::Safe),
598 training_examples: Some(9_418), }),
600
601 "yo" | "yor" => Some(LowResourceMetadata {
603 language_code: "yor".to_string(),
604 language_family: Some("Atlantic-Congo (Volta-Niger)".to_string()),
605 is_polysynthetic: false,
606 has_standard_orthography: true, speaker_population: Some(45_000_000),
608 endangerment_level: Some(EndangermentLevel::Safe),
609 training_examples: Some(9_824), }),
611
612 "ha" | "hau" => Some(LowResourceMetadata {
614 language_code: "hau".to_string(),
615 language_family: Some("Afro-Asiatic (Chadic)".to_string()),
616 is_polysynthetic: false,
617 has_standard_orthography: true, speaker_population: Some(80_000_000),
619 endangerment_level: Some(EndangermentLevel::Safe),
620 training_examples: Some(8_165), }),
622
623 "am" | "amh" => Some(LowResourceMetadata {
625 language_code: "amh".to_string(),
626 language_family: Some("Afro-Asiatic (Semitic)".to_string()),
627 is_polysynthetic: false,
628 has_standard_orthography: true, speaker_population: Some(57_000_000),
630 endangerment_level: Some(EndangermentLevel::Safe),
631 training_examples: Some(1_750), }),
633
634 "ig" | "ibo" => Some(LowResourceMetadata {
636 language_code: "ibo".to_string(),
637 language_family: Some("Atlantic-Congo (Volta-Niger)".to_string()),
638 is_polysynthetic: false,
639 has_standard_orthography: true, speaker_population: Some(45_000_000),
641 endangerment_level: Some(EndangermentLevel::Safe),
642 training_examples: Some(10_905), }),
644
645 "rw" | "kin" => Some(LowResourceMetadata {
647 language_code: "kin".to_string(),
648 language_family: Some("Atlantic-Congo (Bantu)".to_string()),
649 is_polysynthetic: false, has_standard_orthography: true,
651 speaker_population: Some(12_000_000),
652 endangerment_level: Some(EndangermentLevel::Safe),
653 training_examples: Some(11_178), }),
655
656 "pcm" => Some(LowResourceMetadata {
658 language_code: "pcm".to_string(),
659 language_family: Some("English Creole".to_string()),
660 is_polysynthetic: false,
661 has_standard_orthography: false, speaker_population: Some(100_000_000), endangerment_level: Some(EndangermentLevel::Safe),
664 training_examples: Some(7_746), }),
666
667 "wo" | "wol" => Some(LowResourceMetadata {
669 language_code: "wol".to_string(),
670 language_family: Some("Atlantic-Congo (Atlantic)".to_string()),
671 is_polysynthetic: false,
672 has_standard_orthography: true,
673 speaker_population: Some(12_000_000),
674 endangerment_level: Some(EndangermentLevel::Safe),
675 training_examples: Some(6_561), }),
677
678 "zu" | "zul" => Some(LowResourceMetadata {
680 language_code: "zul".to_string(),
681 language_family: Some("Atlantic-Congo (Bantu/Nguni)".to_string()),
682 is_polysynthetic: false, has_standard_orthography: true,
684 speaker_population: Some(27_000_000),
685 endangerment_level: Some(EndangermentLevel::Safe),
686 training_examples: Some(8_354), }),
688
689 "xh" | "xho" => Some(LowResourceMetadata {
691 language_code: "xho".to_string(),
692 language_family: Some("Atlantic-Congo (Bantu/Nguni)".to_string()),
693 is_polysynthetic: false, has_standard_orthography: true,
695 speaker_population: Some(19_000_000),
696 endangerment_level: Some(EndangermentLevel::Safe),
697 training_examples: Some(8_168), }),
699
700 "lg" | "lug" => Some(LowResourceMetadata {
702 language_code: "lug".to_string(),
703 language_family: Some("Atlantic-Congo (Bantu)".to_string()),
704 is_polysynthetic: false, has_standard_orthography: true,
706 speaker_population: Some(10_000_000),
707 endangerment_level: Some(EndangermentLevel::Safe),
708 training_examples: Some(7_060), }),
710
711 "luo" => Some(LowResourceMetadata {
713 language_code: "luo".to_string(),
714 language_family: Some("Nilo-Saharan (Nilotic)".to_string()),
715 is_polysynthetic: false,
716 has_standard_orthography: true,
717 speaker_population: Some(6_000_000),
718 endangerment_level: Some(EndangermentLevel::Safe),
719 training_examples: Some(7_372), }),
721
722 "tw" | "twi" | "aka" => Some(LowResourceMetadata {
724 language_code: "twi".to_string(),
725 language_family: Some("Atlantic-Congo (Kwa)".to_string()),
726 is_polysynthetic: false,
727 has_standard_orthography: true, speaker_population: Some(11_000_000),
729 endangerment_level: Some(EndangermentLevel::Safe),
730 training_examples: Some(6_056), }),
732
733 "sn" | "sna" => Some(LowResourceMetadata {
735 language_code: "sna".to_string(),
736 language_family: Some("Atlantic-Congo (Bantu)".to_string()),
737 is_polysynthetic: false, has_standard_orthography: true,
739 speaker_population: Some(15_000_000),
740 endangerment_level: Some(EndangermentLevel::Safe),
741 training_examples: Some(8_867), }),
743
744 "ti" | "tir" => Some(LowResourceMetadata {
746 language_code: "tir".to_string(),
747 language_family: Some("Afro-Asiatic (Semitic)".to_string()),
748 is_polysynthetic: false,
749 has_standard_orthography: true, speaker_population: Some(9_000_000),
751 endangerment_level: Some(EndangermentLevel::Safe),
752 training_examples: None, }),
754
755 "bm" | "bam" => Some(LowResourceMetadata {
757 language_code: "bam".to_string(),
758 language_family: Some("Atlantic-Congo (Mande)".to_string()),
759 is_polysynthetic: false,
760 has_standard_orthography: true, speaker_population: Some(14_000_000),
762 endangerment_level: Some(EndangermentLevel::Safe),
763 training_examples: Some(6_375), }),
765
766 "ee" | "ewe" => Some(LowResourceMetadata {
768 language_code: "ewe".to_string(),
769 language_family: Some("Atlantic-Congo (Kwa)".to_string()),
770 is_polysynthetic: false,
771 has_standard_orthography: true, speaker_population: Some(7_000_000),
773 endangerment_level: Some(EndangermentLevel::Safe),
774 training_examples: Some(5_007), }),
776
777 "fon" => Some(LowResourceMetadata {
779 language_code: "fon".to_string(),
780 language_family: Some("Atlantic-Congo (Kwa)".to_string()),
781 is_polysynthetic: false,
782 has_standard_orthography: true, speaker_population: Some(2_200_000),
784 endangerment_level: Some(EndangermentLevel::Safe),
785 training_examples: Some(6_204), }),
787
788 "mos" => Some(LowResourceMetadata {
790 language_code: "mos".to_string(),
791 language_family: Some("Atlantic-Congo (Gur)".to_string()),
792 is_polysynthetic: false,
793 has_standard_orthography: true,
794 speaker_population: Some(8_000_000),
795 endangerment_level: Some(EndangermentLevel::Safe),
796 training_examples: Some(6_793), }),
798
799 "tn" | "tsn" => Some(LowResourceMetadata {
801 language_code: "tsn".to_string(),
802 language_family: Some("Atlantic-Congo (Bantu)".to_string()),
803 is_polysynthetic: false, has_standard_orthography: true,
805 speaker_population: Some(8_000_000),
806 endangerment_level: Some(EndangermentLevel::Safe),
807 training_examples: Some(4_784), }),
809
810 "ny" | "nya" => Some(LowResourceMetadata {
812 language_code: "nya".to_string(),
813 language_family: Some("Atlantic-Congo (Bantu)".to_string()),
814 is_polysynthetic: false, has_standard_orthography: true,
816 speaker_population: Some(15_000_000),
817 endangerment_level: Some(EndangermentLevel::Safe),
818 training_examples: Some(8_928), }),
820
821 "bbj" => Some(LowResourceMetadata {
823 language_code: "bbj".to_string(),
824 language_family: Some("Atlantic-Congo (Grassfields Bantu)".to_string()),
825 is_polysynthetic: false,
826 has_standard_orthography: false, speaker_population: Some(1_000_000),
828 endangerment_level: Some(EndangermentLevel::Vulnerable),
829 training_examples: Some(4_833), }),
831
832 _ => None,
833 }
834}
835
836pub const MASAKHANER2_LANGUAGES: &[(&str, &str)] = &[
841 ("bam", "Bambara"),
842 ("bbj", "Ghomala"),
843 ("ewe", "Ewe"),
844 ("fon", "Fon"),
845 ("hau", "Hausa"),
846 ("ibo", "Igbo"),
847 ("kin", "Kinyarwanda"),
848 ("lug", "Luganda"),
849 ("luo", "Dholuo"),
850 ("mos", "Mossi"),
851 ("nya", "Chichewa"),
852 ("pcm", "Nigerian Pidgin"),
853 ("sna", "Shona"),
854 ("swa", "Swahili"),
855 ("tsn", "Setswana"),
856 ("twi", "Twi"),
857 ("wol", "Wolof"),
858 ("xho", "Xhosa"),
859 ("yor", "Yoruba"),
860 ("zul", "Zulu"),
861];
862
863pub fn masakhaner2_language_name(code: &str) -> Option<&'static str> {
865 MASAKHANER2_LANGUAGES
866 .iter()
867 .find(|(c, _)| *c == code)
868 .map(|(_, name)| *name)
869}
870
871#[cfg(test)]
872mod tests {
873 use super::*;
874
875 #[test]
876 fn test_language_metadata() {
877 let quechua = language_metadata("qxo").unwrap();
878 assert_eq!(quechua.language_family, Some("Quechuan".to_string()));
879 assert!(!quechua.is_polysynthetic);
880
881 let cherokee = language_metadata("chr").unwrap();
882 assert!(cherokee.is_polysynthetic);
883 assert_eq!(
884 cherokee.endangerment_level,
885 Some(EndangermentLevel::SeverelyEndangered)
886 );
887 }
888
889 #[test]
890 fn test_orthographic_normalization() {
891 let config = OrthographicConfig {
892 unicode_normalize: true,
893 case_insensitive: true,
894 ignore_diacritics: true,
895 char_mappings: HashMap::new(),
896 };
897
898 let evaluator = LowResourceEvaluator::new();
899 let normalized = evaluator.normalize_text("Café", &config);
900 assert_eq!(normalized, "cafe");
901 }
902
903 #[test]
904 fn test_evaluator_creation() {
905 let evaluator = LowResourceEvaluator::new()
906 .with_morpheme_boundaries(MorphemeConfig::default())
907 .with_orthographic_normalization(OrthographicConfig::default())
908 .with_english_baseline(0.92);
909
910 assert!(evaluator.morpheme_config.is_some());
911 assert!(evaluator.orthographic_config.is_some());
912 assert_eq!(evaluator.english_baseline_f1, Some(0.92));
913 }
914}