1use crate::types::DocumentType;
2use anyhow::Result;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fs;
6
7fn default_true() -> bool {
9 true
10}
11
12fn default_min_alpha_ratio() -> f32 {
13 0.5
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct ParsingConfig {
18 pub document_type: DocumentType,
19 #[serde(default)]
20 pub section_and_hierarchy: SectionAndHierarchyConfig,
21 pub spatial_clustering: SpatialClusteringConfig,
22 pub section_patterns: Vec<String>,
23 #[serde(default)]
25 pub include_raw_tika: bool,
26 #[serde(default)]
28 pub pipeline: PipelineConfig,
29 #[serde(default)]
31 pub list_detection: ListDetectionConfig,
32 #[serde(default)]
34 pub size_enforcer: SizeEnforcerConfig,
35 #[serde(default)]
37 pub minimal_parse: bool,
38 #[serde(default)]
41 pub section_detection_v2: SectionDetectionV2Config,
42 #[serde(default, alias = "paragraph_clustering")]
48 pub node_type_clustering: NodeTypeClusteringConfig,
49 #[serde(default)]
53 pub graph_sanity: GraphSanityConfig,
54 #[serde(default = "default_true")]
60 pub dump_analytics: bool,
61}
62
63#[derive(Debug, Clone, Serialize)]
70pub struct NodeTypeClusteringConfig {
71 pub section: NodeTypeMergeConfig,
72 pub paragraph: NodeTypeMergeConfig,
73 pub list: NodeTypeMergeConfig,
74 pub list_item: NodeTypeMergeConfig,
75 pub header: NodeTypeMergeConfig,
76 pub footer: NodeTypeMergeConfig,
77 pub margin: NodeTypeMergeConfig,
78}
79
80impl Default for NodeTypeClusteringConfig {
81 fn default() -> Self {
82 Self {
83 section: NodeTypeMergeConfig::default_section(),
84 paragraph: NodeTypeMergeConfig::default_paragraph(),
85 list: NodeTypeMergeConfig::default_paragraph(),
86 list_item: NodeTypeMergeConfig::default_paragraph(),
87 header: NodeTypeMergeConfig::default_header_footer(),
88 footer: NodeTypeMergeConfig::default_header_footer(),
89 margin: NodeTypeMergeConfig::default_margin(),
90 }
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct NodeTypeMergeConfig {
99 #[serde(default)]
102 pub same_line: bool,
103
104 #[serde(default)]
108 pub same_paragraph: bool,
109
110 #[serde(default)]
116 pub ignore_region_label: bool,
117
118 #[serde(default)]
138 pub region_overflow_threshold: Option<u32>,
139
140 #[serde(default)]
145 pub same_depth: bool,
146
147 #[serde(default)]
152 pub max_y_gap: Option<f32>,
153
154 #[serde(default = "default_prose_separator")]
158 pub prose_line_separator: String,
159
160 #[serde(default = "default_table_separator")]
163 pub table_line_separator: String,
164}
165
166fn default_prose_separator() -> String {
167 " ".to_string()
168}
169fn default_table_separator() -> String {
170 "\n".to_string()
171}
172
173impl NodeTypeMergeConfig {
174 pub fn default_section() -> Self {
178 Self {
179 same_line: false,
180 same_paragraph: false,
181 ignore_region_label: false,
182 same_depth: true,
183 max_y_gap: Some(50.0),
184 region_overflow_threshold: None,
185 prose_line_separator: " ".to_string(),
186 table_line_separator: "\n".to_string(),
187 }
188 }
189
190 pub fn default_paragraph() -> Self {
201 Self {
202 same_line: false,
203 same_paragraph: true,
204 ignore_region_label: false,
205 same_depth: false,
206 max_y_gap: None,
207 region_overflow_threshold: Some(10),
208 prose_line_separator: " ".to_string(),
209 table_line_separator: "\n".to_string(),
210 }
211 }
212
213 pub fn default_header_footer() -> Self {
218 Self {
219 same_line: false,
220 same_paragraph: false,
221 ignore_region_label: true,
222 same_depth: false,
223 max_y_gap: None,
224 region_overflow_threshold: None,
225 prose_line_separator: " ".to_string(),
226 table_line_separator: "\n".to_string(),
227 }
228 }
229
230 pub fn default_margin() -> Self {
235 Self {
236 same_line: false,
237 same_paragraph: false,
238 ignore_region_label: false,
239 same_depth: false,
240 max_y_gap: None,
241 region_overflow_threshold: None,
242 prose_line_separator: " ".to_string(),
243 table_line_separator: "\n".to_string(),
244 }
245 }
246}
247
248impl<'de> Deserialize<'de> for NodeTypeClusteringConfig {
256 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
257 where
258 D: serde::Deserializer<'de>,
259 {
260 let value = serde_yaml::Value::deserialize(deserializer)?;
265 let mapping = value
266 .as_mapping()
267 .ok_or_else(|| serde::de::Error::custom("node_type_clustering: expected a mapping"))?;
268
269 let legacy_keys = [
271 "merge_segments",
272 "merge_lines",
273 "merge_columns",
274 "merge_bands",
275 ];
276 let is_legacy = legacy_keys
277 .iter()
278 .any(|k| mapping.contains_key(serde_yaml::Value::String((*k).to_string())));
279
280 if is_legacy {
281 #[derive(Deserialize)]
288 struct LegacyParagraphClustering {
289 #[serde(default = "default_true")]
290 merge_segments: bool,
291 #[serde(default = "default_true")]
292 merge_lines: bool,
293 #[serde(default)]
294 #[allow(dead_code)]
295 merge_columns: bool,
296 #[serde(default)]
297 #[allow(dead_code)]
298 merge_bands: bool,
299 #[serde(default = "default_prose_separator")]
300 prose_line_separator: String,
301 #[serde(default = "default_table_separator")]
302 table_line_separator: String,
303 }
304 let l: LegacyParagraphClustering =
305 serde_yaml::from_value(value).map_err(serde::de::Error::custom)?;
306
307 let unified = NodeTypeMergeConfig {
308 same_line: l.merge_segments && !l.merge_lines,
309 same_paragraph: l.merge_lines,
310 ignore_region_label: false,
311 same_depth: false,
312 max_y_gap: None,
313 region_overflow_threshold: None,
314 prose_line_separator: l.prose_line_separator,
315 table_line_separator: l.table_line_separator,
316 };
317 return Ok(Self {
318 section: unified.clone(),
319 paragraph: unified.clone(),
320 list: unified.clone(),
321 list_item: unified.clone(),
322 header: NodeTypeMergeConfig::default_header_footer(),
323 footer: NodeTypeMergeConfig::default_header_footer(),
324 margin: NodeTypeMergeConfig::default_margin(),
325 });
326 }
327
328 #[derive(Deserialize)]
331 struct NewShape {
332 #[serde(default = "NodeTypeMergeConfig::default_section")]
333 section: NodeTypeMergeConfig,
334 #[serde(default = "NodeTypeMergeConfig::default_paragraph")]
335 paragraph: NodeTypeMergeConfig,
336 #[serde(default = "NodeTypeMergeConfig::default_paragraph")]
337 list: NodeTypeMergeConfig,
338 #[serde(default = "NodeTypeMergeConfig::default_paragraph")]
339 list_item: NodeTypeMergeConfig,
340 #[serde(default = "NodeTypeMergeConfig::default_header_footer")]
341 header: NodeTypeMergeConfig,
342 #[serde(default = "NodeTypeMergeConfig::default_header_footer")]
343 footer: NodeTypeMergeConfig,
344 #[serde(default = "NodeTypeMergeConfig::default_margin")]
345 margin: NodeTypeMergeConfig,
346 }
347 let n: NewShape = serde_yaml::from_value(value).map_err(serde::de::Error::custom)?;
348 Ok(Self {
349 section: n.section,
350 paragraph: n.paragraph,
351 list: n.list,
352 list_item: n.list_item,
353 header: n.header,
354 footer: n.footer,
355 margin: n.margin,
356 })
357 }
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct PipelineConfig {
362 pub rules: Vec<RuleConfig>,
364}
365
366#[derive(Debug, Clone, Serialize, Deserialize)]
367pub struct RuleConfig {
368 pub name: String,
370 #[serde(default = "default_true")]
372 pub enabled: bool,
373}
374
375impl Default for PipelineConfig {
376 fn default() -> Self {
377 Self {
378 rules: vec![
379 RuleConfig {
380 name: "SectionDetectionV2".to_string(),
381 enabled: true,
382 },
383 RuleConfig {
384 name: "ParagraphClustering".to_string(),
385 enabled: true,
386 },
387 RuleConfig {
388 name: "Validation".to_string(),
389 enabled: true,
390 },
391 ],
392 }
393 }
394}
395
396#[derive(Debug, Clone, Serialize, Deserialize)]
397pub struct SectionAndHierarchyConfig {
398 pub large_header_threshold: f32,
401 pub medium_header_threshold: f32,
403 pub small_header_threshold: f32,
405 pub min_header_size: f32,
407 pub use_bold_indicator: bool,
409 pub bold_size_strict: bool,
412
413 pub max_depth: u32,
416 pub font_size_tolerance: f32,
418 pub enforce_max_depth: bool,
420 pub starting_section_level: u32,
422
423 #[serde(default = "default_min_alpha_ratio")]
427 pub min_alpha_ratio: f32,
428
429 pub pattern_detection: PatternDetectionConfig,
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct PatternDetectionConfig {
435 pub enabled: bool,
437 pub patterns: Vec<String>,
439 pub respect_font_constraints: bool,
441}
442
443impl Default for PatternDetectionConfig {
444 fn default() -> Self {
445 Self {
446 enabled: true,
447 patterns: vec![
448 r"^[A-Z][A-Z\s]{2,}$".to_string(), r"^\d+\.\s+[A-Z][a-z]{3,}".to_string(), r"^(Chapter|Section|Part|Article)\s+\d+".to_string(), r"^[A-Z][a-z]{2,}(?:\s+[A-Z][a-z]{2,})*:$".to_string(), ],
454 respect_font_constraints: true,
455 }
456 }
457}
458
459impl Default for SectionAndHierarchyConfig {
460 fn default() -> Self {
461 Self {
462 large_header_threshold: 0.7,
463 medium_header_threshold: 0.3,
464 small_header_threshold: 0.1,
465 min_header_size: 8.5,
466 use_bold_indicator: true,
467 bold_size_strict: true, max_depth: 5,
469 font_size_tolerance: 0.1,
470 enforce_max_depth: true,
471 starting_section_level: 1,
472 min_alpha_ratio: 0.5,
473 pattern_detection: PatternDetectionConfig::default(),
474 }
475 }
476}
477
478#[derive(Debug, Clone, Serialize, Deserialize)]
479pub struct SpatialClusteringConfig {
480 pub enabled: bool,
482 #[serde(default = "default_true")]
484 pub enable_paragraph_merging: bool,
485 #[serde(default)]
487 pub enable_spatial_adjacency: bool,
488 pub min_line_height: f32,
490 pub vertical_gap_threshold_multiplier: f32,
492 pub horizontal_alignment_tolerance: f32,
494 pub line_grouping_tolerance: f32,
496 pub sections: ElementClusteringConfig,
498 pub paragraphs: ElementClusteringConfig,
500}
501
502#[derive(Debug, Clone, Serialize, Deserialize)]
503pub struct ElementClusteringConfig {
504 pub min_segment_size: usize,
506 pub max_segment_size: usize,
508}
509
510fn default_y_tolerance() -> f32 {
512 15.0
513}
514
515fn default_false() -> bool {
516 false
517}
518
519fn default_bullet_patterns() -> Vec<String> {
520 vec![
521 "•".to_string(),
522 "·".to_string(),
523 "●".to_string(),
524 "■".to_string(),
525 "▪".to_string(),
526 "▫".to_string(),
527 "◦".to_string(),
528 "‣".to_string(),
529 "⁃".to_string(),
530 "-".to_string(),
531 "*".to_string(),
532 "→".to_string(),
533 "➤".to_string(),
534 "✓".to_string(),
535 "•".to_string(),
536 "·".to_string(),
537 ]
538}
539
540fn default_numbered_patterns() -> Vec<String> {
541 vec![
542 r"^\d+\.".to_string(), r"^\d+\)".to_string(), r"^\(\d+\)".to_string(), r"^[a-z]\.".to_string(), r"^[a-z]\)".to_string(), r"^[A-Z]\.".to_string(), r"^[A-Z]\)".to_string(), r"^[ivx]+\.".to_string(), r"^[IVX]+\.".to_string(), ]
552}
553
554#[derive(Debug, Clone, Serialize, Deserialize)]
555pub struct ListDetectionConfig {
556 #[serde(default = "default_true")]
558 pub enabled: bool,
559
560 #[serde(default = "default_sequence_lookahead_elements")]
563 pub sequence_lookahead_elements: usize,
564
565 #[serde(default = "default_sequence_boundary_extension")]
567 pub sequence_boundary_extension: usize,
568
569 #[serde(default = "default_y_tolerance")]
572 pub y_tolerance: f32,
573
574 #[serde(default = "default_bullet_patterns")]
577 pub bullet_patterns: Vec<String>,
578
579 #[serde(default = "default_numbered_patterns")]
581 pub numbered_patterns: Vec<String>,
582
583 #[serde(default = "default_true")]
586 pub create_list_containers: bool,
587
588 #[serde(default = "default_false")]
590 pub preserve_list_items: bool,
591
592 #[serde(default = "default_max_lookahead_elements")]
594 pub max_lookahead_elements: usize,
595
596 #[serde(default = "default_last_item_boundary_gap")]
600 pub last_item_boundary_gap: f32,
601
602 #[serde(default)]
605 pub validation: ListValidationConfig,
606}
607
608fn default_sequence_lookahead_elements() -> usize {
609 10 }
611
612fn default_sequence_boundary_extension() -> usize {
613 3 }
615
616fn default_max_lookahead_elements() -> usize {
617 25 }
619
620fn default_last_item_boundary_gap() -> f32 {
621 80.0 }
623
624fn default_validation_enabled() -> bool {
626 true
627}
628
629#[derive(Debug, Clone, Serialize, Deserialize)]
631pub struct SequentialNumberingConfig {
632 #[serde(default = "default_true")]
634 pub allow_letter_sequences: bool,
635
636 #[serde(default = "default_zero")]
638 pub max_gap_tolerance: u32,
639}
640
641impl Default for SequentialNumberingConfig {
642 fn default() -> Self {
643 Self {
644 allow_letter_sequences: true,
645 max_gap_tolerance: 0,
646 }
647 }
648}
649
650#[derive(Debug, Clone, Serialize, Deserialize)]
651pub struct MathematicalContextConfig {
652 #[serde(default = "default_mathematical_symbols")]
654 pub symbols: Vec<String>,
655
656 #[serde(default = "default_mathematical_terms")]
658 pub terms: Vec<String>,
659}
660
661impl Default for MathematicalContextConfig {
662 fn default() -> Self {
663 Self {
664 symbols: default_mathematical_symbols(),
665 terms: default_mathematical_terms(),
666 }
667 }
668}
669
670#[derive(Debug, Clone, Serialize, Deserialize)]
671pub struct HyphenContextConfig {
672 #[serde(default = "default_hyphen_strategy")]
674 pub strategy: String,
675
676 #[serde(default = "default_true")]
678 pub require_space_after: bool,
679}
680
681impl Default for HyphenContextConfig {
682 fn default() -> Self {
683 Self {
684 strategy: default_hyphen_strategy(),
685 require_space_after: true,
686 }
687 }
688}
689
690fn default_zero() -> u32 {
692 0
693}
694
695fn default_mathematical_symbols() -> Vec<String> {
696 vec![
697 "→".to_string(),
698 "←".to_string(),
699 "⇒".to_string(),
700 "⇐".to_string(),
701 "∀".to_string(),
702 "∃".to_string(),
703 ]
704}
705
706fn default_mathematical_terms() -> Vec<String> {
707 vec![
708 "equation".to_string(),
709 "formula".to_string(),
710 "coordinates".to_string(),
711 "system".to_string(),
712 "transform".to_string(),
713 ]
714}
715
716fn default_hyphen_strategy() -> String {
717 "strict".to_string()
718}
719
720#[derive(Debug, Clone, Serialize, Deserialize)]
721pub struct ListValidationConfig {
722 #[serde(default = "default_validation_enabled")]
724 pub enabled: bool,
725
726 #[serde(default = "default_true")]
728 pub minimum_size_check: bool,
729
730 #[serde(default = "default_true")]
732 pub first_item_validation: bool,
733
734 #[serde(default = "default_true")]
736 pub parenthetical_context_check: bool,
737
738 #[serde(default = "default_true")]
740 pub sequential_numbering_check: bool,
741
742 #[serde(default = "default_true")]
743 pub mathematical_context_check: bool,
744
745 #[serde(default = "default_true")]
746 pub hyphen_context_check: bool,
747
748 #[serde(default)]
750 pub sequential_numbering: SequentialNumberingConfig,
751
752 #[serde(default)]
753 pub mathematical_context: MathematicalContextConfig,
754
755 #[serde(default)]
756 pub hyphen_context: HyphenContextConfig,
757
758 #[serde(default = "default_false")]
760 pub sequence_pattern_check: bool,
761
762 #[serde(default = "default_false")]
763 pub content_quality_check: bool,
764
765 #[serde(default = "default_false")]
766 pub spatial_coherence_check: bool,
767}
768
769impl Default for ListValidationConfig {
770 fn default() -> Self {
771 Self {
772 enabled: true,
773 minimum_size_check: true,
774 first_item_validation: true,
775 parenthetical_context_check: true,
776 sequential_numbering_check: true,
777 mathematical_context_check: true,
778 hyphen_context_check: true,
779 sequential_numbering: SequentialNumberingConfig::default(),
780 mathematical_context: MathematicalContextConfig::default(),
781 hyphen_context: HyphenContextConfig::default(),
782 sequence_pattern_check: false,
783 content_quality_check: false,
784 spatial_coherence_check: false,
785 }
786 }
787}
788
789fn default_max_size() -> usize {
791 800 }
793
794fn default_size_unit() -> String {
795 "characters".to_string()
796}
797
798fn default_min_split_size_ratio() -> f32 {
799 0.25 }
801
802fn default_max_iterations() -> usize {
803 10 }
805
806fn default_split_direction() -> String {
807 "vertical".to_string() }
809
810impl Default for ListDetectionConfig {
811 fn default() -> Self {
812 Self {
813 enabled: true,
814 sequence_lookahead_elements: default_sequence_lookahead_elements(),
815 sequence_boundary_extension: default_sequence_boundary_extension(),
816 y_tolerance: default_y_tolerance(),
817 bullet_patterns: default_bullet_patterns(),
818 numbered_patterns: default_numbered_patterns(),
819 create_list_containers: true,
820 preserve_list_items: false,
821 max_lookahead_elements: default_max_lookahead_elements(),
822 last_item_boundary_gap: default_last_item_boundary_gap(),
823 validation: ListValidationConfig::default(),
824 }
825 }
826}
827
828#[derive(Debug, Clone, Serialize, Deserialize)]
829pub struct SizeEnforcerConfig {
830 #[serde(default = "default_true")]
832 pub enabled: bool,
833
834 #[serde(default = "default_max_size")]
836 pub max_size: usize,
837
838 #[serde(default = "default_size_unit")]
840 pub size_unit: String,
841
842 #[serde(default = "default_true")]
844 pub preserve_sentences: bool,
845
846 #[serde(default = "default_min_split_size_ratio")]
848 pub min_split_size_ratio: f32,
849
850 #[serde(default = "default_true")]
852 pub recursive: bool,
853
854 #[serde(default = "default_max_iterations")]
856 pub max_iterations: usize,
857
858 #[serde(default = "default_split_direction")]
860 pub split_direction: String,
861}
862
863impl Default for SizeEnforcerConfig {
864 fn default() -> Self {
865 Self {
866 enabled: true,
867 max_size: 800,
868 size_unit: "characters".to_string(),
869 preserve_sentences: true,
870 min_split_size_ratio: 0.25,
871 recursive: true,
872 max_iterations: 10,
873 split_direction: "vertical".to_string(),
874 }
875 }
876}
877
878#[derive(Debug, Clone, Serialize, Deserialize)]
885pub struct SectionDetectionV2Config {
886 pub line_height_tolerance: f32,
892
893 pub font_size_tolerance: f32,
900
901 pub structural_size_margin: f32,
904
905 pub structural_size_ratio: Option<f32>,
908
909 pub min_alpha_ratio: f32,
912
913 pub max_depth: u32,
915 pub enforce_max_depth: bool,
916 pub starting_section_level: u32,
917
918 pub inclusion_patterns: Vec<InclusionPattern>,
925
926 pub inclusion_max_length: usize,
933
934 pub exclusion_patterns: Vec<String>,
937
938 pub tiebreaker_keywords: Vec<TiebreakerKeyword>,
947}
948
949#[derive(Debug, Clone, Serialize, Deserialize)]
952pub struct TiebreakerKeyword {
953 pub name: String,
954 pub pattern: String,
955}
956
957#[derive(Debug, Clone, Serialize, Deserialize)]
972pub struct InclusionPattern {
973 pub pattern: String,
974 #[serde(default = "default_true")]
975 pub require_bold: bool,
976 #[serde(default = "default_true")]
977 pub require_isolation: bool,
978}
979
980#[derive(Debug, Clone, Serialize, Deserialize)]
985pub struct InvariantToggle {
986 pub check: bool,
987 pub correct: bool,
988}
989
990impl Default for InvariantToggle {
991 fn default() -> Self {
992 Self {
993 check: true,
994 correct: true,
995 }
996 }
997}
998
999#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1003pub struct GraphSanityInvariants {
1004 pub depth_consistency: InvariantToggle,
1007}
1008
1009#[derive(Debug, Clone, Serialize, Deserialize)]
1013pub struct GraphSanityConfig {
1014 pub enabled: bool,
1015 pub invariants: GraphSanityInvariants,
1016}
1017
1018impl Default for GraphSanityConfig {
1019 fn default() -> Self {
1020 Self {
1021 enabled: true,
1022 invariants: GraphSanityInvariants::default(),
1023 }
1024 }
1025}
1026
1027impl Default for SectionDetectionV2Config {
1028 fn default() -> Self {
1029 Self {
1030 line_height_tolerance: 3.0,
1031 font_size_tolerance: 0.1,
1032 structural_size_margin: 4.0,
1033 structural_size_ratio: None,
1034 min_alpha_ratio: 0.5,
1035 max_depth: 6,
1036 enforce_max_depth: true,
1037 starting_section_level: 1,
1038 inclusion_patterns: vec![
1039 InclusionPattern {
1040 pattern: r"^\d+\.".to_string(), require_bold: true,
1042 require_isolation: true,
1043 },
1044 InclusionPattern {
1045 pattern: r"^\d+\.\d+".to_string(), require_bold: true,
1047 require_isolation: true,
1048 },
1049 InclusionPattern {
1050 pattern: r"^Chapter\s+\d+".to_string(),
1051 require_bold: true,
1052 require_isolation: true,
1053 },
1054 InclusionPattern {
1055 pattern: r"^Appendix\s+[A-Z]".to_string(),
1056 require_bold: true,
1057 require_isolation: true,
1058 },
1059 ],
1060 inclusion_max_length: 30,
1061 exclusion_patterns: vec![r"^Figure\s".to_string(), r"^Table\s".to_string()],
1062 tiebreaker_keywords: vec![
1063 TiebreakerKeyword {
1064 name: "part".into(),
1065 pattern: r"(?i)^part\s+[IVXLCDM\d]+".into(),
1066 },
1067 TiebreakerKeyword {
1068 name: "chapter".into(),
1069 pattern: r"(?i)^chapter\s+[IVXLCDM\d]+".into(),
1070 },
1071 TiebreakerKeyword {
1072 name: "article".into(),
1073 pattern: r"(?i)^article\s+\d+[a-z]?".into(),
1074 },
1075 TiebreakerKeyword {
1076 name: "section".into(),
1077 pattern: r"(?i)^section\s+\d+[a-z]?".into(),
1078 },
1079 TiebreakerKeyword {
1080 name: "appendix".into(),
1081 pattern: r"(?i)^appendix\s+[A-Z\d]+".into(),
1082 },
1083 TiebreakerKeyword {
1084 name: "schedule".into(),
1085 pattern: r"(?i)^schedule\s+\d+".into(),
1086 },
1087 TiebreakerKeyword {
1088 name: "annex".into(),
1089 pattern: r"(?i)^annex\s+[IVX\d]+".into(),
1090 },
1091 TiebreakerKeyword {
1092 name: "numbered".into(),
1093 pattern: r"^\d+\s+[A-Z]".into(),
1094 },
1095 ],
1096 }
1097 }
1098}
1099
1100#[derive(Debug, Clone)]
1101pub struct ConfigManager {
1102 configs: HashMap<DocumentType, ParsingConfig>,
1103 default_config: ParsingConfig,
1104}
1105
1106impl ConfigManager {
1107 pub fn new() -> Result<Self> {
1108 let mut manager = Self {
1109 configs: HashMap::new(),
1110 default_config: Self::create_default_generic_config(),
1111 };
1112
1113 manager.load_builtin_configs()?;
1115
1116 Ok(manager)
1117 }
1118
1119 pub fn get_config(&self, doc_type: &DocumentType) -> &ParsingConfig {
1120 self.configs.get(doc_type).unwrap_or(&self.default_config)
1121 }
1122
1123 pub fn load_config_from_file(&mut self, path: &str) -> Result<()> {
1124 let content = fs::read_to_string(path)?;
1125 let config: ParsingConfig = serde_yaml::from_str(&content)?;
1126 self.configs.insert(config.document_type.clone(), config);
1127 Ok(())
1128 }
1129
1130 fn load_builtin_configs(&mut self) -> Result<()> {
1131 let generic_config = Self::create_default_generic_config();
1133 self.configs.insert(DocumentType::Generic, generic_config);
1134
1135 let academic_config = ParsingConfig {
1137 document_type: DocumentType::AcademicPaper,
1138 section_and_hierarchy: SectionAndHierarchyConfig {
1139 large_header_threshold: 0.8, medium_header_threshold: 0.4,
1141 small_header_threshold: 0.15,
1142 min_header_size: 10.0,
1143 use_bold_indicator: true,
1144 bold_size_strict: true,
1145 max_depth: 4,
1146 font_size_tolerance: 0.1,
1147 enforce_max_depth: true,
1148 starting_section_level: 1,
1149 min_alpha_ratio: 0.5,
1150 pattern_detection: PatternDetectionConfig::default(),
1151 },
1152 spatial_clustering: SpatialClusteringConfig {
1153 enabled: true,
1154 enable_paragraph_merging: true,
1155 enable_spatial_adjacency: false,
1156 min_line_height: 9.0, vertical_gap_threshold_multiplier: 1.2, horizontal_alignment_tolerance: 8.0, line_grouping_tolerance: 0.25, sections: ElementClusteringConfig {
1161 min_segment_size: 50, max_segment_size: 500, },
1164 paragraphs: ElementClusteringConfig {
1165 min_segment_size: 200, max_segment_size: 12000, },
1168 },
1169 section_patterns: vec![
1170 "abstract".to_string(),
1171 "introduction".to_string(),
1172 "methodology".to_string(),
1173 "results".to_string(),
1174 "discussion".to_string(),
1175 "conclusion".to_string(),
1176 "references".to_string(),
1177 ],
1178 include_raw_tika: false, pipeline: PipelineConfig::default(),
1180 list_detection: ListDetectionConfig::default(),
1181 size_enforcer: SizeEnforcerConfig::default(), minimal_parse: false,
1183 section_detection_v2: SectionDetectionV2Config::default(),
1184 node_type_clustering: NodeTypeClusteringConfig::default(),
1185 graph_sanity: GraphSanityConfig::default(),
1186 dump_analytics: true,
1187 };
1188 self.configs
1189 .insert(DocumentType::AcademicPaper, academic_config);
1190
1191 let legal_config = ParsingConfig {
1193 document_type: DocumentType::LegalContract,
1194 section_and_hierarchy: SectionAndHierarchyConfig {
1195 large_header_threshold: 0.6,
1196 medium_header_threshold: 0.3,
1197 small_header_threshold: 0.1,
1198 min_header_size: 9.0,
1199 use_bold_indicator: true,
1200 bold_size_strict: true,
1201 max_depth: 5,
1202 font_size_tolerance: 0.1,
1203 enforce_max_depth: true,
1204 starting_section_level: 1,
1205 min_alpha_ratio: 0.5,
1206 pattern_detection: PatternDetectionConfig::default(),
1207 },
1208 spatial_clustering: SpatialClusteringConfig {
1209 enabled: true,
1210 enable_paragraph_merging: true,
1211 enable_spatial_adjacency: false,
1212 min_line_height: 8.5,
1213 vertical_gap_threshold_multiplier: 0.6, horizontal_alignment_tolerance: 12.0, line_grouping_tolerance: 0.2, sections: ElementClusteringConfig {
1217 min_segment_size: 30, max_segment_size: 200, },
1220 paragraphs: ElementClusteringConfig {
1221 min_segment_size: 50, max_segment_size: 5000, },
1224 },
1225 section_patterns: vec![
1226 "article".to_string(),
1227 "section".to_string(),
1228 "clause".to_string(),
1229 "whereas".to_string(),
1230 "terms".to_string(),
1231 "conditions".to_string(),
1232 ],
1233 include_raw_tika: false, pipeline: PipelineConfig::default(),
1235 list_detection: ListDetectionConfig::default(),
1236 size_enforcer: SizeEnforcerConfig::default(), minimal_parse: false,
1238 section_detection_v2: SectionDetectionV2Config::default(),
1239 node_type_clustering: NodeTypeClusteringConfig::default(),
1240 graph_sanity: GraphSanityConfig::default(),
1241 dump_analytics: true,
1242 };
1243 self.configs
1244 .insert(DocumentType::LegalContract, legal_config);
1245
1246 Ok(())
1247 }
1248
1249 fn create_default_generic_config() -> ParsingConfig {
1250 ParsingConfig {
1251 document_type: DocumentType::Generic,
1252 section_and_hierarchy: SectionAndHierarchyConfig::default(),
1253 spatial_clustering: SpatialClusteringConfig {
1254 enabled: true, enable_paragraph_merging: true, enable_spatial_adjacency: false, min_line_height: 8.0, vertical_gap_threshold_multiplier: 0.8, horizontal_alignment_tolerance: 10.0, line_grouping_tolerance: 0.3, sections: ElementClusteringConfig {
1262 min_segment_size: 20, max_segment_size: 300, },
1265 paragraphs: ElementClusteringConfig {
1266 min_segment_size: 100, max_segment_size: 8000, },
1269 },
1270 section_patterns: vec![
1271 "chapter".to_string(),
1273 "section".to_string(),
1274 "part".to_string(),
1275 "overview".to_string(),
1276 "summary".to_string(),
1277 "background".to_string(),
1278 "principles".to_string(),
1279 "approach".to_string(),
1280 ],
1281 include_raw_tika: false, pipeline: PipelineConfig::default(),
1283 list_detection: ListDetectionConfig::default(),
1284 size_enforcer: SizeEnforcerConfig::default(), minimal_parse: false,
1286 section_detection_v2: SectionDetectionV2Config::default(),
1287 node_type_clustering: NodeTypeClusteringConfig::default(),
1288 graph_sanity: GraphSanityConfig::default(),
1289 dump_analytics: true,
1290 }
1291 }
1292}
1293
1294impl Default for ConfigManager {
1295 fn default() -> Self {
1296 Self::new().expect("Failed to create default ConfigManager")
1297 }
1298}
1299
1300impl ParsingConfig {
1301 pub fn load_from_file(path: &str) -> Result<Self> {
1303 let content = std::fs::read_to_string(path)?;
1304 let config: ParsingConfig = serde_yaml::from_str(&content)?;
1305 Ok(config)
1306 }
1307
1308 pub fn load_with_fallback(path: Option<&str>) -> Self {
1310 match path {
1311 Some(p) => Self::load_from_file(p).unwrap_or_else(|_| {
1312 eprintln!("⚠️ Failed to load config from {}, using defaults", p);
1313 Self::default()
1314 }),
1315 None => Self::default(),
1316 }
1317 }
1318}
1319
1320impl Default for ParsingConfig {
1321 fn default() -> Self {
1322 Self {
1324 document_type: DocumentType::Generic,
1325 section_and_hierarchy: SectionAndHierarchyConfig::default(),
1326 spatial_clustering: SpatialClusteringConfig {
1327 enabled: true,
1328 enable_paragraph_merging: true,
1329 enable_spatial_adjacency: false,
1330 min_line_height: 8.0,
1331 vertical_gap_threshold_multiplier: 0.8,
1332 horizontal_alignment_tolerance: 10.0,
1333 line_grouping_tolerance: 0.3,
1334 sections: ElementClusteringConfig {
1335 min_segment_size: 20,
1336 max_segment_size: 300,
1337 },
1338 paragraphs: ElementClusteringConfig {
1339 min_segment_size: 100,
1340 max_segment_size: 8000,
1341 },
1342 },
1343 section_patterns: vec![],
1344 include_raw_tika: false,
1345 pipeline: PipelineConfig::default(),
1346 list_detection: ListDetectionConfig::default(),
1347 size_enforcer: SizeEnforcerConfig::default(),
1348 minimal_parse: false,
1349 section_detection_v2: SectionDetectionV2Config::default(),
1350 node_type_clustering: NodeTypeClusteringConfig::default(),
1351 graph_sanity: GraphSanityConfig::default(),
1352 dump_analytics: true,
1353 }
1354 }
1355}