use std::ops::Range;
use std::path::Path;
use super::{decode, Conversion, Language, Lexeme, MAX_INPUT_BYTES, MAX_LEXEMES};
use crate::assets;
use crate::dictionary::english::EnglishDictionary;
use crate::japanese::{JapaneseProvider, JapaneseRomajiScheme};
use crate::pinyin::segment::cut_one_piece_min_segments;
use crate::quanpin::QuanpinEngine;
use crate::types::QueryRequest;
use crate::RuntimePaths;
const MAX_SPAN_QUERIES: usize = 4096;
const MAX_SURFACE_BYTES: usize = 256;
const MAX_CHINESE_SYLLABLES: usize = 7;
pub struct DictionaryComposer {
languages: Vec<Language>,
chinese: Option<QuanpinEngine>,
english: Option<EnglishDictionary>,
japanese: Option<JapaneseProvider>,
japanese_ready: bool,
japanese_scheme: JapaneseRomajiScheme,
japanese_request: QueryRequest,
input: String,
lexemes: Vec<Lexeme>,
answer: Option<Conversion>,
}
impl DictionaryComposer {
pub fn new(paths: &RuntimePaths, languages: &[Language]) -> Self {
Self::with_japanese_model(paths, languages, &paths.resource(assets::JAPANESE_MODEL))
}
pub fn with_japanese_model(paths: &RuntimePaths, languages: &[Language], model: &Path) -> Self {
let languages: Vec<_> = [Language::Chinese, Language::English, Language::Japanese]
.into_iter()
.filter(|language| languages.contains(language))
.collect();
let active = languages.len() >= 2;
Self {
chinese: (active && languages.contains(&Language::Chinese))
.then(|| QuanpinEngine::new(paths)),
english: (active && languages.contains(&Language::English)).then(|| {
EnglishDictionary::open(&paths.dictionary(assets::ENGLISH_DICTIONARY), None, None)
}),
japanese: (active && languages.contains(&Language::Japanese))
.then(|| JapaneseProvider::new(model)),
languages,
japanese_ready: false,
japanese_scheme: JapaneseRomajiScheme::new(),
japanese_request: QueryRequest::default(),
input: String::new(),
lexemes: Vec::new(),
answer: None,
}
}
pub fn warm_up_in_background(&self) {
if let Some(japanese) = &self.japanese {
japanese.warm_up_in_background();
}
}
pub fn convert(&mut self, input: &str) -> Option<Conversion> {
let japanese_ready = self
.japanese
.as_mut()
.is_some_and(JapaneseProvider::prepare_if_cached);
if japanese_ready != self.japanese_ready {
self.forget_input();
self.japanese_ready = japanese_ready;
}
if input == self.input {
return self.answer.clone();
}
if self.languages.len() < 2
|| input.is_empty()
|| input.len() > MAX_INPUT_BYTES
|| !input.bytes().all(|byte| byte.is_ascii_lowercase())
{
self.forget_input();
return None;
}
let common = self
.input
.bytes()
.zip(input.bytes())
.take_while(|(left, right)| left == right)
.count();
self.lexemes.retain(|lexeme| lexeme.input.end <= common);
let mut queried = 0;
for end in common + 1..=input.len() {
for start in 0..end {
queried += 1;
if queried > MAX_SPAN_QUERIES {
self.forget_input();
return None;
}
if self.query_span(input, start..end).is_none() || self.lexemes.len() > MAX_LEXEMES
{
self.forget_input();
return None;
}
}
}
let mut answer = decode(input, &self.lexemes, &self.languages);
if let Some(conversion) = &mut answer {
self.rerank_chinese(input, conversion);
}
self.input.clear();
self.input.push_str(input);
self.answer = answer;
self.answer.clone()
}
pub fn clear(&mut self) {
self.forget_input();
if let Some(chinese) = &mut self.chinese {
chinese.reset_cache();
}
if let Some(japanese) = &mut self.japanese {
japanese.reset_cache();
}
}
fn forget_input(&mut self) {
self.input.clear();
self.lexemes.clear();
self.answer = None;
}
fn push(&mut self, input: Range<usize>, language: Language, text: String) -> Option<()> {
if text.len() > MAX_SURFACE_BYTES {
return None;
}
if !text.is_empty() {
self.lexemes.push(Lexeme {
input,
language,
text,
rank: 0,
});
}
Some(())
}
fn query_span(&mut self, input: &str, range: Range<usize>) -> Option<()> {
let raw = &input[range.clone()];
if let Some(chinese) = &mut self.chinese {
let syllables = cut_one_piece_min_segments(raw, true);
if !syllables.is_empty() && syllables.len() <= MAX_CHINESE_SYLLABLES {
if let Some(row) = chinese.conversion_rows(&syllables, 1).into_iter().next() {
self.push(range.clone(), Language::Chinese, row.value)?;
}
}
}
if let Some(row) = self
.english
.as_ref()
.and_then(|english| english.query_exact(raw))
{
self.push(range.clone(), Language::English, row.word.to_string())?;
}
if let Some(japanese) = self.japanese.as_mut().filter(|_| self.japanese_ready) {
self.japanese_scheme.set_raw_input(raw, "");
self.japanese_scheme
.build_request_into(&mut self.japanese_request);
let exact = japanese
.whole_reading_lemma(&self.japanese_request)
.is_some_and(|lemma| lemma.exact && lemma.morae >= 3);
if exact {
if let Some(path) = japanese
.whole_reading_best_path(&self.japanese_request)
.filter(|path| path.unknown_kana == 0)
{
self.push(range, Language::Japanese, path.text)?;
}
}
}
Some(())
}
fn rerank_chinese(&mut self, input: &str, conversion: &mut Conversion) {
let Some(chinese) = &mut self.chinese else {
return;
};
for segment in &mut conversion.segments {
if segment.language != Language::Chinese {
continue;
}
let syllables = cut_one_piece_min_segments(&input[segment.input.clone()], true);
let spans = chinese.convert_pinned(&syllables, &[]);
if spans.first().is_some_and(|span| span.start == 0)
&& spans.last().is_some_and(|span| span.end == syllables.len())
&& spans.windows(2).all(|pair| pair[0].end == pair[1].start)
{
segment.text = spans.into_iter().map(|span| span.word).collect();
}
}
}
}
#[cfg(test)]
#[path = "dictionary_tests.rs"]
mod tests;