msime_engine/multilingual/
dictionaries.rs1use std::ops::Range;
2use std::path::Path;
3
4use super::{decode, Conversion, Language, Lexeme, MAX_INPUT_BYTES, MAX_LEXEMES};
5use crate::assets;
6use crate::dictionary::english::EnglishDictionary;
7use crate::japanese::{JapaneseProvider, JapaneseRomajiScheme};
8use crate::pinyin::segment::cut_one_piece_min_segments;
9use crate::quanpin::QuanpinEngine;
10use crate::types::QueryRequest;
11use crate::RuntimePaths;
12
13const MAX_SPAN_QUERIES: usize = 4096;
15const MAX_SURFACE_BYTES: usize = 256;
17const MAX_CHINESE_SYLLABLES: usize = 7;
19
20pub struct DictionaryComposer {
24 languages: Vec<Language>,
25 chinese: Option<QuanpinEngine>,
26 english: Option<EnglishDictionary>,
27 japanese: Option<JapaneseProvider>,
28 japanese_ready: bool,
29 japanese_scheme: JapaneseRomajiScheme,
30 japanese_request: QueryRequest,
31 input: String,
32 lexemes: Vec<Lexeme>,
33 answer: Option<Conversion>,
34}
35
36impl DictionaryComposer {
37 pub fn new(paths: &RuntimePaths, languages: &[Language]) -> Self {
39 Self::with_japanese_model(paths, languages, &paths.resource(assets::JAPANESE_MODEL))
40 }
41
42 pub fn with_japanese_model(paths: &RuntimePaths, languages: &[Language], model: &Path) -> Self {
44 let languages: Vec<_> = [Language::Chinese, Language::English, Language::Japanese]
45 .into_iter()
46 .filter(|language| languages.contains(language))
47 .collect();
48 let active = languages.len() >= 2;
49 Self {
50 chinese: (active && languages.contains(&Language::Chinese))
51 .then(|| QuanpinEngine::new(paths)),
52 english: (active && languages.contains(&Language::English)).then(|| {
53 EnglishDictionary::open(&paths.dictionary(assets::ENGLISH_DICTIONARY), None, None)
54 }),
55 japanese: (active && languages.contains(&Language::Japanese))
56 .then(|| JapaneseProvider::new(model)),
57 languages,
58 japanese_ready: false,
59 japanese_scheme: JapaneseRomajiScheme::new(),
60 japanese_request: QueryRequest::default(),
61 input: String::new(),
62 lexemes: Vec::new(),
63 answer: None,
64 }
65 }
66
67 pub fn warm_up_in_background(&self) {
69 if let Some(japanese) = &self.japanese {
70 japanese.warm_up_in_background();
71 }
72 }
73
74 pub fn convert(&mut self, input: &str) -> Option<Conversion> {
77 let japanese_ready = self
78 .japanese
79 .as_mut()
80 .is_some_and(JapaneseProvider::prepare_if_cached);
81 if japanese_ready != self.japanese_ready {
82 self.forget_input();
83 self.japanese_ready = japanese_ready;
84 }
85 if input == self.input {
86 return self.answer.clone();
87 }
88 if self.languages.len() < 2
89 || input.is_empty()
90 || input.len() > MAX_INPUT_BYTES
91 || !input.bytes().all(|byte| byte.is_ascii_lowercase())
92 {
93 self.forget_input();
94 return None;
95 }
96 let common = self
97 .input
98 .bytes()
99 .zip(input.bytes())
100 .take_while(|(left, right)| left == right)
101 .count();
102 self.lexemes.retain(|lexeme| lexeme.input.end <= common);
103 let mut queried = 0;
104 for end in common + 1..=input.len() {
105 for start in 0..end {
106 queried += 1;
107 if queried > MAX_SPAN_QUERIES {
108 self.forget_input();
109 return None;
110 }
111 if self.query_span(input, start..end).is_none() || self.lexemes.len() > MAX_LEXEMES
112 {
113 self.forget_input();
114 return None;
115 }
116 }
117 }
118 let mut answer = decode(input, &self.lexemes, &self.languages);
119 if let Some(conversion) = &mut answer {
120 self.rerank_chinese(input, conversion);
121 }
122 self.input.clear();
123 self.input.push_str(input);
124 self.answer = answer;
125 self.answer.clone()
126 }
127
128 pub fn clear(&mut self) {
130 self.forget_input();
131 if let Some(chinese) = &mut self.chinese {
132 chinese.reset_cache();
133 }
134 if let Some(japanese) = &mut self.japanese {
135 japanese.reset_cache();
136 }
137 }
138
139 fn forget_input(&mut self) {
140 self.input.clear();
141 self.lexemes.clear();
142 self.answer = None;
143 }
144
145 fn push(&mut self, input: Range<usize>, language: Language, text: String) -> Option<()> {
146 if text.len() > MAX_SURFACE_BYTES {
147 return None;
148 }
149 if !text.is_empty() {
150 self.lexemes.push(Lexeme {
151 input,
152 language,
153 text,
154 rank: 0,
156 });
157 }
158 Some(())
159 }
160
161 fn query_span(&mut self, input: &str, range: Range<usize>) -> Option<()> {
162 let raw = &input[range.clone()];
163 if let Some(chinese) = &mut self.chinese {
164 let syllables = cut_one_piece_min_segments(raw, true);
165 if !syllables.is_empty() && syllables.len() <= MAX_CHINESE_SYLLABLES {
166 if let Some(row) = chinese.conversion_rows(&syllables, 1).into_iter().next() {
167 self.push(range.clone(), Language::Chinese, row.value)?;
168 }
169 }
170 }
171 if let Some(row) = self
172 .english
173 .as_ref()
174 .and_then(|english| english.query_exact(raw))
175 {
176 self.push(range.clone(), Language::English, row.word.to_string())?;
177 }
178 if let Some(japanese) = self.japanese.as_mut().filter(|_| self.japanese_ready) {
179 self.japanese_scheme.set_raw_input(raw, "");
180 self.japanese_scheme
181 .build_request_into(&mut self.japanese_request);
182 let exact = japanese
183 .whole_reading_lemma(&self.japanese_request)
184 .is_some_and(|lemma| lemma.exact && lemma.morae >= 3);
185 if exact {
186 if let Some(path) = japanese
187 .whole_reading_best_path(&self.japanese_request)
188 .filter(|path| path.unknown_kana == 0)
189 {
190 self.push(range, Language::Japanese, path.text)?;
191 }
192 }
193 }
194 Some(())
195 }
196
197 fn rerank_chinese(&mut self, input: &str, conversion: &mut Conversion) {
198 let Some(chinese) = &mut self.chinese else {
199 return;
200 };
201 for segment in &mut conversion.segments {
202 if segment.language != Language::Chinese {
203 continue;
204 }
205 let syllables = cut_one_piece_min_segments(&input[segment.input.clone()], true);
206 let spans = chinese.convert_pinned(&syllables, &[]);
207 if spans.first().is_some_and(|span| span.start == 0)
208 && spans.last().is_some_and(|span| span.end == syllables.len())
209 && spans.windows(2).all(|pair| pair[0].end == pair[1].start)
210 {
211 segment.text = spans.into_iter().map(|span| span.word).collect();
212 }
213 }
214 }
215}
216
217#[cfg(test)]
218#[path = "dictionary_tests.rs"]
219mod tests;