voirs_g2p/preprocessing/
unicode.rs1use crate::Result;
4use unicode_normalization::UnicodeNormalization;
5use unicode_segmentation::UnicodeSegmentation;
6
7pub fn normalize_text(text: &str) -> Result<String> {
9 let normalized = text.nfc().collect::<String>();
11
12 let cleaned = clean_text(&normalized)?;
14
15 Ok(cleaned)
16}
17
18fn clean_text(text: &str) -> Result<String> {
20 let mut result = String::new();
21
22 for grapheme in text.graphemes(true) {
23 match grapheme {
24 "‚" | "'" => result.push('\''),
26 "\u{201c}" | "\u{201d}" => result.push('"'),
27 "—" | "–" => result.push('-'),
28 "…" => result.push_str("..."),
29
30 g if is_valid_text_char(g) => result.push_str(g),
32
33 _ => result.push(' '),
35 }
36 }
37
38 Ok(collapse_spaces(&result))
40}
41
42fn is_valid_text_char(grapheme: &str) -> bool {
44 if grapheme.len() == 1 {
45 let ch = grapheme.chars().next().expect("grapheme has len == 1");
46 ch.is_alphabetic()
47 || ch.is_numeric()
48 || ch.is_whitespace()
49 || matches!(
50 ch,
51 '.' | ','
52 | '!'
53 | '?'
54 | ';'
55 | ':'
56 | '\''
57 | '"'
58 | '-'
59 | '('
60 | ')'
61 | '['
62 | ']'
63 | '{'
64 | '}'
65 )
66 } else {
67 grapheme
69 .chars()
70 .all(|c| c.is_alphabetic() || c.is_numeric())
71 }
72}
73
74fn collapse_spaces(text: &str) -> String {
76 let mut result = String::new();
77 let mut last_was_space = false;
78
79 for ch in text.chars() {
80 if ch.is_whitespace() {
81 if !last_was_space {
82 result.push(' ');
83 last_was_space = true;
84 }
85 } else {
86 result.push(ch);
87 last_was_space = false;
88 }
89 }
90
91 result.trim().to_string()
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
96pub enum ScriptType {
97 Latin,
98 Cyrillic,
99 Greek,
100 Arabic,
101 Hebrew,
102 CJK,
103 Hiragana,
104 Katakana,
105 Hangul,
106 Mixed,
107 Unknown,
108}
109
110pub fn detect_script(text: &str) -> ScriptType {
112 let mut script_counts = std::collections::HashMap::new();
113 let mut total_chars = 0;
114
115 for ch in text.chars() {
116 if ch.is_alphabetic() {
117 total_chars += 1;
118
119 let script = match ch as u32 {
120 0x0041..=0x007A | 0x00C0..=0x00FF | 0x0100..=0x017F => ScriptType::Latin,
122 0x0400..=0x04FF => ScriptType::Cyrillic,
124 0x0370..=0x03FF => ScriptType::Greek,
126 0x0600..=0x06FF => ScriptType::Arabic,
128 0x0590..=0x05FF => ScriptType::Hebrew,
130 0x4E00..=0x9FFF => ScriptType::CJK,
132 0x3040..=0x309F => ScriptType::Hiragana,
134 0x30A0..=0x30FF => ScriptType::Katakana,
136 0xAC00..=0xD7AF => ScriptType::Hangul,
138 _ => ScriptType::Unknown,
139 };
140
141 *script_counts.entry(script).or_insert(0) += 1;
142 }
143 }
144
145 if total_chars == 0 {
146 return ScriptType::Unknown;
147 }
148
149 let is_mixed = script_counts.len() > 2;
151
152 let dominant_script = script_counts
153 .into_iter()
154 .max_by_key(|(_, count)| *count)
155 .map(|(script, _)| script)
156 .unwrap_or(ScriptType::Unknown);
157
158 if is_mixed {
159 ScriptType::Mixed
160 } else {
161 dominant_script
162 }
163}
164
165pub fn filter_symbols(text: &str) -> String {
167 text.chars()
168 .filter(|ch| {
169 ch.is_alphabetic()
171 || ch.is_numeric()
172 || ch.is_whitespace()
173 || matches!(
174 *ch,
175 '.' | ','
176 | '!'
177 | '?'
178 | ';'
179 | ':'
180 | '\''
181 | '"'
182 | '-'
183 | '('
184 | ')'
185 | '['
186 | ']'
187 | '{'
188 | '}'
189 )
190 })
191 .collect()
192}
193
194pub fn is_rtl_text(text: &str) -> bool {
196 text.chars().any(|ch| {
197 matches!(ch as u32,
198 0x0590..=0x05FF | 0x0600..=0x06FF | 0x0750..=0x077F | 0x08A0..=0x08FF )
203 })
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_normalize_text() {
212 let text = "café"; let result = normalize_text(text).unwrap();
215 assert!(!result.is_empty());
216
217 let text = "\u{201c}Hello World\u{201d} \u{2014} it's great!";
219 let result = normalize_text(text).unwrap();
220 assert_eq!(result, "\"Hello World\" - it's great!");
221 }
222
223 #[test]
224 fn test_collapse_spaces() {
225 assert_eq!(collapse_spaces("hello world"), "hello world");
226 assert_eq!(collapse_spaces(" hello world "), "hello world");
227 assert_eq!(collapse_spaces("hello\n\t world"), "hello world");
228 }
229
230 #[test]
231 fn test_script_detection() {
232 assert_eq!(detect_script("Hello World"), ScriptType::Latin);
233 assert_eq!(detect_script("こんにちは"), ScriptType::Hiragana);
234 assert_eq!(detect_script("カタカナ"), ScriptType::Katakana);
235 assert_eq!(detect_script("한글"), ScriptType::Hangul);
236 assert_eq!(detect_script("中文"), ScriptType::CJK);
237 assert_eq!(detect_script("Привет"), ScriptType::Cyrillic);
238 }
239
240 #[test]
241 fn test_filter_symbols() {
242 let text = "Hello 👋 World! 🌍";
243 let result = filter_symbols(text);
244 assert_eq!(result, "Hello World! ");
245 }
246
247 #[test]
248 fn test_rtl_detection() {
249 assert!(is_rtl_text("שלום"));
250 assert!(is_rtl_text("مرحبا"));
251 assert!(!is_rtl_text("Hello"));
252 }
253
254 #[test]
255 fn test_valid_text_char() {
256 assert!(is_valid_text_char("a"));
257 assert!(is_valid_text_char("A"));
258 assert!(is_valid_text_char("1"));
259 assert!(is_valid_text_char(" "));
260 assert!(is_valid_text_char("."));
261 assert!(is_valid_text_char("é"));
262 assert!(!is_valid_text_char("👋"));
263 }
264}