Skip to main content

msime_engine/pinyin/
glide.rs

1//! 滑行输入:手指一笔滑过若干字母键,解码成它最可能拼出的全拼字母串,代价低的在前。
2//!
3//! 解码器只看几何,只认得音节表;按词典给这些字母串排序是会话的事。一个假设是一串字母,加上每个字母对齐到的轨迹采样点:第一个字母在第一个采样点,最后一个在最后一个,其余按次序落在中间。它的代价有四部分:
4//!
5//! - 每个对齐点付它到字母键中心距离的平方,所以笔画可以偏离键位一部分键宽;
6//! - 两个对齐字母之间的每个采样点付它到两键连线距离的平方,所以笔画拐弯的地方必须对齐一个字母,而笔画没有拐弯的键只有恰好在连线上时才不额外付费;沿两键方向往回走也要付费,所以沿一条线来回折返的笔画(`keneng`)不会被读成只走了一遍(`keng`);
7//! - 对齐时跳过的每一次停留(手指在键上停住)付固定罚分,所以在键上停一下,即使在直线上也能确认这个键;
8//! - 每个字母付一个小的固定价,所以笔画走得同样好的两个串里短的那个胜出,只是经过的键不会被当成按下。
9//!
10//! 坐标先除以键宽和键高,不论宿主的像素、密度和键距如何,一个键都是单位正方形。搜索对每个采样点只保留对齐在那里的少数几个最便宜的前缀(令牌传递),连线代价来自前缀和,所以一次扩展的耗时只与这个字母键附近的采样点数有关。
11
12use std::collections::{HashMap, HashSet};
13use std::sync::OnceLock;
14
15use super::syllables::{intact_pinyin_list, is_intact, MAX_SYLLABLE_LENGTH};
16
17/// 一笔最多重采样成多少个点;更长的笔画改为更稀的采样。
18const MAX_SAMPLES: usize = 128;
19/// 重采样的步长,以键为单位。
20const SAMPLE_STEP: f32 = 0.25;
21/// 对齐一个字母时代价为 1 所对应的距离平方(键为单位),约半个键。
22const ALIGN_SCALE: f32 = 0.3;
23/// 离两键连线多远(距离平方,键为单位)时,每单位笔画长度的代价为 1。
24const LINE_SCALE: f32 = 0.25;
25/// 每个字母的固定价。取得小:落在另外两个键连线上的键(`z` 与 `o` 之间的 `h`)对齐它只比跳过它贵一点,`zhong` 与 `zong` 应当由词典而不是几何来分。
26const LETTER_COST: f32 = 0.8;
27/// 字母与前一个相同时的额外价。笔画画不出连按两次,没有它的话每个经过的键都会被再读一遍(`de` 读成 `dee`)。拼音只在音节边界上出现重复字母,但那里并不少见(`zhong'guo`、`nan'ning`),所以它只需让重复的读法排在不重复的后面,其余交给词典。
28const REPEAT_COST: f32 = 0.8;
29/// [`rank_by_dictionary`] 重排时,词典权重的一个对数单位抵多少几何代价。
30const DICTIONARY_WEIGHT: f32 = 0.7;
31/// 不是词典词的串在按最罕见音节计分之外再付的代价。
32const UNKNOWN_WORD_COST: f32 = 6.0;
33/// 词典词每多一个音节,对数权重加多少。出货词典里常用词的权重在五十万上下、常用字在几百万,没有这一项的话,只要 `y` 落在笔画的连线上,企业(`qi'ye`)就会输给切(`qie`)。
34const WORD_SYLLABLE_BONUS: f32 = 1.5;
35/// 笔画逆着一个对齐键到下一个对齐键的方向每走一个键所付的代价。只有连线代价时,笔画沿一条线来回走不花钱,于是在键盘一行上来回两次的 `laishuo` 被读成了 `lao`。
36const BACKTRACK_COST: f32 = 2.0;
37/// 没有字母对齐到的停留付多少。
38const SKIPPED_DWELL_COST: f32 = 4.0;
39/// 离每个采样点都比这远(键为单位)的键不可能在串里。
40const NEAR_DISTANCE: f32 = 1.4;
41/// 每个采样点保留的前缀数。
42const TOKENS_PER_SAMPLE: usize = 6;
43/// 比同一采样点上最便宜的前缀贵出这么多的前缀会被丢掉。
44const BEAM: f32 = 10.0;
45/// 考虑的最长字母串;拼出更长的笔画不解码。
46pub const MAX_GLIDE_LETTERS: usize = 30;
47/// 手指在离停下处 [`DWELL_RADIUS`] 以内停这么久算一次停留。
48const DWELL_MS: u32 = 120;
49const DWELL_RADIUS: f32 = 0.35;
50
51#[derive(Debug, Clone, Copy, PartialEq)]
52pub struct GlidePoint {
53    pub x: f32,
54    pub y: f32,
55    /// 距笔画开始的毫秒数,宿主量了才有;只有带时间才能找出停留。
56    pub time_ms: Option<u32>,
57}
58
59/// 宿主画出来的字母键,用宿主自己的坐标。
60#[derive(Debug, Clone, PartialEq)]
61pub struct GlideKeyboard {
62    /// `a`..=`z` 各键的中心,按这个次序。
63    pub centers: [(f32, f32); 26],
64    pub key_width: f32,
65    pub key_height: f32,
66}
67
68#[derive(Debug, Clone, PartialEq)]
69pub struct GlideHypothesis {
70    /// 小写全拼字母,不带分隔符,每个音节都完整。
71    pub letters: String,
72    pub cost: f32,
73}
74
75#[derive(Debug, Clone, Copy)]
76struct Sample {
77    x: f32,
78    y: f32,
79    dwell: bool,
80}
81
82impl GlideKeyboard {
83    /// 这套键位能不能用来解码:键宽键高为正的有限数,键中心都有限。
84    pub fn is_valid(&self) -> bool {
85        self.key_width.is_finite()
86            && self.key_height.is_finite()
87            && self.key_width > 0.0
88            && self.key_height > 0.0
89            && self
90                .centers
91                .iter()
92                .all(|(x, y)| x.is_finite() && y.is_finite())
93    }
94
95    fn normalized_centers(&self) -> [(f32, f32); 26] {
96        self.centers
97            .map(|(x, y)| (x / self.key_width, y / self.key_height))
98    }
99}
100
101/// 这一笔至多 `limit` 个字母串,代价低的在前;键位无效、不足两个点、有非有限坐标,或者没有完整的全拼串跟得上这一笔时为空。
102pub fn decode_glide(
103    keyboard: &GlideKeyboard,
104    points: &[GlidePoint],
105    limit: usize,
106) -> Vec<GlideHypothesis> {
107    if limit == 0
108        || points.len() < 2
109        || !keyboard.is_valid()
110        || points.iter().any(|p| !p.x.is_finite() || !p.y.is_finite())
111    {
112        return Vec::new();
113    }
114    let normalized: Vec<GlidePoint> = points
115        .iter()
116        .map(|p| GlidePoint {
117            x: p.x / keyboard.key_width,
118            y: p.y / keyboard.key_height,
119            time_ms: p.time_ms,
120        })
121        .collect();
122    let samples = resample(&normalized);
123    if samples.len() < 2 {
124        return Vec::new();
125    }
126    Decoder::new(keyboard.normalized_centers(), &samples).run(limit)
127}
128
129/// `points` 里各次停留的弧长位置:手指在 [`DWELL_RADIUS`] 以内停了 [`DWELL_MS`] 或更久的地方。
130fn dwell_positions(points: &[GlidePoint], arc: &[f32]) -> Vec<f32> {
131    let mut dwells = Vec::new();
132    let mut start = 0;
133    while start < points.len() {
134        let Some(begin) = points[start].time_ms else {
135            start += 1;
136            continue;
137        };
138        let mut end = start;
139        while end + 1 < points.len() && distance(points[start], points[end + 1]) <= DWELL_RADIUS {
140            end += 1;
141        }
142        let rested = points[end]
143            .time_ms
144            .is_some_and(|stop| stop.saturating_sub(begin) >= DWELL_MS);
145        if rested {
146            dwells.push((arc[start] + arc[end]) / 2.0);
147            start = end + 1;
148        } else {
149            start += 1;
150        }
151    }
152    dwells
153}
154
155fn distance(a: GlidePoint, b: GlidePoint) -> f32 {
156    ((a.x - b.x).powi(2) + (a.y - b.y).powi(2)).sqrt()
157}
158
159/// 沿笔画等距的采样点,含两端,离每次停留最近的采样点做上标记。
160fn resample(points: &[GlidePoint]) -> Vec<Sample> {
161    let mut arc = Vec::with_capacity(points.len());
162    let mut length = 0.0;
163    arc.push(0.0);
164    for pair in points.windows(2) {
165        length += distance(pair[0], pair[1]);
166        arc.push(length);
167    }
168    let step = SAMPLE_STEP.max(length / (MAX_SAMPLES - 1) as f32);
169    let count = ((length / step).ceil() as usize + 1).clamp(2, MAX_SAMPLES);
170    let mut samples = Vec::with_capacity(count);
171    let mut segment = 0;
172    for index in 0..count {
173        let target = length * index as f32 / (count - 1) as f32;
174        while segment + 2 < arc.len() && arc[segment + 1] < target {
175            segment += 1;
176        }
177        let (a, b) = (points[segment], points[segment + 1]);
178        let span = arc[segment + 1] - arc[segment];
179        let t = if span > 0.0 {
180            ((target - arc[segment]) / span).clamp(0.0, 1.0)
181        } else {
182            0.0
183        };
184        samples.push(Sample {
185            x: a.x + (b.x - a.x) * t,
186            y: a.y + (b.y - a.y) * t,
187            dwell: false,
188        });
189    }
190    if length > 0.0 {
191        for dwell in dwell_positions(points, &arc) {
192            let index = ((dwell / length) * (count - 1) as f32).round() as usize;
193            samples[index.min(count - 1)].dwell = true;
194        }
195    }
196    samples
197}
198
199/// 完整音节组成的字母前缀树。节点 0 是根,也代表一个恰好结束在两个音节之间的前缀。
200struct SyllableTrie {
201    /// 每个字母的子节点;0 表示没有,因为根不会是谁的子节点。
202    children: Vec<[u16; 26]>,
203    intact: Vec<bool>,
204    /// 节点有字母 `l` 的子节点时第 `l` 位为 1。
205    masks: Vec<u32>,
206}
207
208fn syllable_trie() -> &'static SyllableTrie {
209    static TRIE: OnceLock<SyllableTrie> = OnceLock::new();
210    TRIE.get_or_init(|| {
211        let mut trie = SyllableTrie {
212            children: vec![[0; 26]],
213            intact: vec![false],
214            masks: Vec::new(),
215        };
216        for syllable in intact_pinyin_list() {
217            let mut node = 0;
218            for &letter in syllable.as_bytes() {
219                let slot = usize::from(letter - b'a');
220                if trie.children[node][slot] == 0 {
221                    trie.children.push([0; 26]);
222                    trie.intact.push(false);
223                    trie.children[node][slot] =
224                        u16::try_from(trie.children.len() - 1).expect("the syllable trie is small");
225                }
226                node = usize::from(trie.children[node][slot]);
227            }
228            trie.intact[node] = true;
229        }
230        trie.masks = trie
231            .children
232            .iter()
233            .map(|children| {
234                (0..26)
235                    .filter(|&letter| children[letter] != 0)
236                    .fold(0, |mask, letter| mask | 1 << letter)
237            })
238            .collect();
239        trie
240    })
241}
242
243impl SyllableTrie {
244    /// `states` 里某种切分后面可以接的字母。
245    fn followers(&self, states: &[u16]) -> u32 {
246        states
247            .iter()
248            .fold(0, |mask, &state| mask | self.masks[usize::from(state)])
249    }
250
251    /// 把在 `states` 所示切分的前缀后面接上 `letter` 之后、每种合法切分的状态写进 `next`:音节还能长时是长出来的那个音节,音节完整时是根。没有合法切分时 `next` 为空。
252    fn extend(&self, states: &[u16], letter: u8, next: &mut Vec<u16>) {
253        next.clear();
254        for &state in states {
255            let child = self.children[usize::from(state)][usize::from(letter)];
256            if child == 0 {
257                continue;
258            }
259            let node = usize::from(child);
260            if self.intact[node] && !next.contains(&0) {
261                next.push(0);
262            }
263            if self.children[node].iter().any(|&grand| grand != 0) && !next.contains(&child) {
264                next.push(child);
265            }
266        }
267    }
268}
269
270/// 一个字母前缀(字母记作 `0..26`)、它各种合法切分在前缀树里的状态,以及它对齐到各采样点的代价,按采样点排序。只保留剪枝后留下的采样点,所以一个前缀只有寥寥几项,而不是每个采样点一项。
271struct Prefix {
272    letters: Vec<u8>,
273    states: Vec<u16>,
274    costs: Vec<(usize, f32)>,
275}
276
277impl Prefix {
278    fn empty() -> Self {
279        Self {
280            letters: Vec::new(),
281            states: Vec::new(),
282            costs: Vec::new(),
283        }
284    }
285}
286
287/// `prune` 跨 beam 轮次复用的临时容器,避免每轮为采样点和前缀重新建嵌套向量。
288struct PruneScratch {
289    at: Vec<Vec<(f32, usize, usize)>>,
290    keep: Vec<Vec<bool>>,
291}
292
293impl PruneScratch {
294    fn new(count: usize) -> Self {
295        Self {
296            at: (0..count).map(|_| Vec::new()).collect(),
297            keep: Vec::new(),
298        }
299    }
300}
301
302struct Decoder {
303    /// 把字母 `l` 对齐到采样点 `j` 的代价是 `align[l][j]`;笔画从没靠近的字母为 `None`。
304    align: [Option<Vec<f32>>; 26],
305    /// 离各字母键 [`NEAR_DISTANCE`] 以内的采样点,升序:字母只对齐到这些地方。
306    reach: [Vec<usize>; 26],
307    /// 字母对 `from * 26 + to` 的逐点累计代价:偏离两键连线、沿连线往回走、跳过停留;第 `j` 项覆盖采样点 `0..=j`。只为两个字母都靠近笔画的字母对填写。
308    lines: Vec<Option<Vec<f32>>>,
309    count: usize,
310}
311
312impl Decoder {
313    fn new(centers: [(f32, f32); 26], samples: &[Sample]) -> Self {
314        let length: f32 = samples
315            .windows(2)
316            .map(|pair| ((pair[0].x - pair[1].x).powi(2) + (pair[0].y - pair[1].y).powi(2)).sqrt())
317            .sum();
318        // 每个采样点代表的笔画长度,这样连线代价与采样疏密无关。
319        let weight = length / (samples.len() - 1) as f32;
320        let near = NEAR_DISTANCE * NEAR_DISTANCE;
321        let squared: [Vec<f32>; 26] = std::array::from_fn(|letter| {
322            let (cx, cy) = centers[letter];
323            samples
324                .iter()
325                .map(|s| (s.x - cx).powi(2) + (s.y - cy).powi(2))
326                .collect()
327        });
328        let reach: [Vec<usize>; 26] = std::array::from_fn(|letter| {
329            (0..samples.len())
330                .filter(|&j| squared[letter][j] <= near)
331                .collect()
332        });
333        let align: [Option<Vec<f32>>; 26] = std::array::from_fn(|letter| {
334            (!reach[letter].is_empty())
335                .then(|| squared[letter].iter().map(|d| d / ALIGN_SCALE).collect())
336        });
337        let mut lines = vec![None; 26 * 26];
338        for from in 0..26 {
339            for to in 0..26 {
340                if align[from].is_none() || align[to].is_none() {
341                    continue;
342                }
343                let a = centers[from];
344                let b = centers[to];
345                let (dx, dy) = (b.0 - a.0, b.1 - a.1);
346                let span = dx * dx + dy * dy;
347                let length = span.sqrt();
348                let mut total = 0.0;
349                let mut previous: Option<&Sample> = None;
350                lines[from * 26 + to] = Some(
351                    samples
352                        .iter()
353                        .map(|s| {
354                            if let Some(p) = previous.filter(|_| length > 0.0) {
355                                let along = ((s.x - p.x) * dx + (s.y - p.y) * dy) / length;
356                                total += (-along).max(0.0) * BACKTRACK_COST;
357                            }
358                            previous = Some(s);
359                            let t = if span > 0.0 {
360                                (((s.x - a.0) * dx + (s.y - a.1) * dy) / span).clamp(0.0, 1.0)
361                            } else {
362                                0.0
363                            };
364                            let off = (s.x - a.0 - dx * t).powi(2) + (s.y - a.1 - dy * t).powi(2);
365                            total += off / LINE_SCALE * weight;
366                            if s.dwell {
367                                total += SKIPPED_DWELL_COST;
368                            }
369                            total
370                        })
371                        .collect(),
372                );
373            }
374        }
375        Self {
376            align,
377            reach,
378            lines,
379            count: samples.len(),
380        }
381    }
382
383    fn run(self, limit: usize) -> Vec<GlideHypothesis> {
384        let trie = syllable_trie();
385        let count = self.count;
386        let last = count - 1;
387        let near: Vec<u8> = (0..26u8)
388            .filter(|&letter| self.align[usize::from(letter)].is_some())
389            .collect();
390        let mut states = Vec::new();
391        let mut level: Vec<Prefix> = Vec::new();
392        for &letter in &near {
393            // 第一个字母在第一个采样点上。
394            if self.reach[usize::from(letter)].first() != Some(&0) {
395                continue;
396            }
397            trie.extend(&[0], letter, &mut states);
398            if states.is_empty() {
399                continue;
400            }
401            let cost = self.align[usize::from(letter)].as_ref().expect("near")[0] + LETTER_COST;
402            level.push(Prefix {
403                letters: vec![letter],
404                states: states.clone(),
405                costs: vec![(0, cost)],
406            });
407        }
408        let mut finals: Vec<GlideHypothesis> = Vec::new();
409        let mut floors = vec![f32::INFINITY; count];
410        let mut prune_scratch = PruneScratch::new(count);
411        let mut recycled = Vec::new();
412        for _ in 1..MAX_GLIDE_LETTERS {
413            floors.fill(f32::INFINITY);
414            let mut next = Vec::new();
415            for prefix in &level {
416                let from = usize::from(*prefix.letters.last().expect("prefixes are never empty"));
417                let followers = trie.followers(&prefix.states);
418                let earliest = prefix.costs[0].0;
419                for &letter in &near {
420                    // 只扩展音节允许接在后面、并且键位在前缀最早对齐点之后还会被经过的字母。
421                    if followers & 1 << letter == 0
422                        || self.reach[usize::from(letter)].last() <= Some(&earliest)
423                    {
424                        continue;
425                    }
426                    trie.extend(&prefix.states, letter, &mut states);
427                    if states.is_empty() {
428                        continue;
429                    }
430                    let lines = self.lines[from * 26 + usize::from(letter)]
431                        .as_deref()
432                        .expect("lines are filled for every pair of near letters");
433                    let align = self.align[usize::from(letter)].as_deref().expect("near");
434                    let step = if usize::from(letter) == from {
435                        LETTER_COST + REPEAT_COST
436                    } else {
437                        LETTER_COST
438                    };
439                    let mut candidate = recycled.pop().unwrap_or_else(Prefix::empty);
440                    candidate.costs.clear();
441                    // `best` 是所有对齐点 i < j 上 cost[i] - lines[i] 的最小值;i 与 j 之间的采样点再付 lines[j - 1] - lines[i]。
442                    let mut best = f32::INFINITY;
443                    let mut taken = 0;
444                    for &j in &self.reach[usize::from(letter)] {
445                        while taken < prefix.costs.len() && prefix.costs[taken].0 < j {
446                            let (i, cost) = prefix.costs[taken];
447                            best = best.min(cost - lines[i]);
448                            taken += 1;
449                        }
450                        if !best.is_finite() {
451                            continue;
452                        }
453                        let cost = best + lines[j - 1] + align[j] + step;
454                        if cost <= floors[j] + BEAM {
455                            floors[j] = floors[j].min(cost);
456                            candidate.costs.push((j, cost));
457                        }
458                    }
459                    if candidate.costs.is_empty() {
460                        recycled.push(candidate);
461                        continue;
462                    }
463                    candidate.letters.clear();
464                    candidate.letters.extend_from_slice(&prefix.letters);
465                    candidate.letters.push(letter);
466                    candidate.states.clear();
467                    candidate.states.extend_from_slice(&states);
468                    next.push(candidate);
469                }
470            }
471            recycled.append(&mut level);
472            prune(&mut next, &mut prune_scratch, &mut recycled);
473            finals.extend(next.iter().filter_map(|prefix| {
474                let &(at, cost) = prefix.costs.last()?;
475                (at == last && prefix.states.contains(&0)).then(|| GlideHypothesis {
476                    letters: prefix
477                        .letters
478                        .iter()
479                        .map(|&l| char::from(b'a' + l))
480                        .collect(),
481                    cost,
482                })
483            }));
484            // 字母带来的每一项代价都不为负,所以已经和第 `limit` 个找到的串一样贵的前缀只会排在它后面。
485            if finals.len() >= limit {
486                finals.select_nth_unstable_by(limit - 1, |a, b| a.cost.total_cmp(&b.cost));
487                finals.truncate(limit);
488                let ceiling = finals
489                    .iter()
490                    .map(|hypothesis| hypothesis.cost)
491                    .fold(f32::NEG_INFINITY, f32::max);
492                for prefix in &mut next {
493                    prefix.costs.retain(|&(_, cost)| cost < ceiling);
494                }
495                let current = std::mem::take(&mut next);
496                next = current
497                    .into_iter()
498                    .filter_map(|prefix| {
499                        if prefix.costs.is_empty() {
500                            recycled.push(prefix);
501                            None
502                        } else {
503                            Some(prefix)
504                        }
505                    })
506                    .collect();
507            }
508            if next.is_empty() {
509                break;
510            }
511            level = next;
512        }
513        finals.sort_by(|a, b| {
514            a.cost
515                .total_cmp(&b.cost)
516                .then_with(|| a.letters.cmp(&b.letters))
517        });
518        finals.truncate(limit);
519        finals
520    }
521}
522
523/// 在每个采样点上只保留 [`TOKENS_PER_SAMPLE`] 个最便宜、且与最便宜者相差不超过 [`BEAM`] 的前缀;哪里都没保留的前缀丢掉。
524fn prune(prefixes: &mut Vec<Prefix>, scratch: &mut PruneScratch, recycled: &mut Vec<Prefix>) {
525    for tokens in &mut scratch.at {
526        tokens.clear();
527    }
528    for (p, prefix) in prefixes.iter().enumerate() {
529        for (entry, &(j, cost)) in prefix.costs.iter().enumerate() {
530            scratch.at[j].push((cost, p, entry));
531        }
532    }
533    scratch.keep.resize_with(prefixes.len(), Vec::new);
534    for (prefix, keep) in prefixes.iter().zip(&mut scratch.keep) {
535        keep.clear();
536        keep.resize(prefix.costs.len(), false);
537    }
538    for tokens in &mut scratch.at {
539        if tokens.len() > TOKENS_PER_SAMPLE {
540            tokens.select_nth_unstable_by(TOKENS_PER_SAMPLE - 1, |a, b| a.0.total_cmp(&b.0));
541            tokens.truncate(TOKENS_PER_SAMPLE);
542        }
543        let floor = tokens
544            .iter()
545            .map(|token| token.0)
546            .fold(f32::INFINITY, f32::min);
547        for &(cost, p, entry) in tokens.iter() {
548            if cost <= floor + BEAM {
549                scratch.keep[p][entry] = true;
550            }
551        }
552    }
553    for (prefix, keep) in prefixes.iter_mut().zip(&scratch.keep) {
554        let mut flags = keep.iter();
555        prefix
556            .costs
557            .retain(|_| *flags.next().expect("one flag per entry"));
558    }
559    let current = std::mem::take(prefixes);
560    *prefixes = Vec::with_capacity(current.len());
561    for prefix in current {
562        if prefix.costs.is_empty() {
563            recycled.push(prefix);
564        } else {
565            prefixes.push(prefix);
566        }
567    }
568}
569
570/// 把 `letters` 切成完整音节的各种切法,至多 `limit` 种。
571struct SyllableSplits<'a> {
572    syllables: Vec<&'a str>,
573    ranges: Vec<SplitRange>,
574}
575
576struct SplitRange {
577    range: std::ops::Range<usize>,
578    key: String,
579}
580
581impl<'a> SyllableSplits<'a> {
582    fn new(letters: &'a str, limit: usize) -> Self {
583        Self {
584            syllables: Vec::with_capacity(letters.len().saturating_mul(limit)),
585            ranges: Vec::with_capacity(limit),
586        }
587    }
588
589    fn iter(&self) -> impl Iterator<Item = &[&'a str]> {
590        self.ranges
591            .iter()
592            .map(|range| &self.syllables[range.range.clone()])
593    }
594}
595
596fn syllable_splits(letters: &str, limit: usize) -> SyllableSplits<'_> {
597    fn walk<'a>(
598        rest: &'a str,
599        split: &mut Vec<&'a str>,
600        found: &mut SyllableSplits<'a>,
601        limit: usize,
602    ) {
603        if found.ranges.len() >= limit {
604            return;
605        }
606        if rest.is_empty() {
607            let start = found.syllables.len();
608            found.syllables.extend_from_slice(split);
609            found.ranges.push(SplitRange {
610                range: start..found.syllables.len(),
611                key: split.join("'"),
612            });
613            return;
614        }
615        for end in (1..=rest.len().min(MAX_SYLLABLE_LENGTH)).rev() {
616            if is_intact(&rest[..end]) {
617                split.push(&rest[..end]);
618                walk(&rest[end..], split, found, limit);
619                split.pop();
620            }
621        }
622    }
623    let mut found = SyllableSplits::new(letters, limit);
624    let mut split = Vec::with_capacity(letters.len());
625    walk(letters, &mut split, &mut found, limit);
626    found
627}
628
629/// 按笔画与串的吻合程度和词典给出的常用程度重排解码出的串。
630///
631/// `weights` 对一组词典键(以 `'` 连接的音节)回答它认得的每个键下最好那一行的权重。一个串按它是词的切分里权重最高的那种计分;完全不是词的串按最罕见的音节计分再减 [`UNKNOWN_WORD_COST`],所以笔画吻合程度相当时,词典词总排在一串零散音节前面。
632pub fn rank_by_dictionary(
633    hypotheses: Vec<GlideHypothesis>,
634    weights: impl FnOnce(&[String]) -> HashMap<String, i64>,
635) -> Vec<GlideHypothesis> {
636    const SPLITS: usize = 8;
637    let splits: Vec<SyllableSplits<'_>> = hypotheses
638        .iter()
639        .map(|hypothesis| syllable_splits(&hypothesis.letters, SPLITS))
640        .collect();
641    let key_capacity = splits
642        .iter()
643        .map(|hypothesis| {
644            hypothesis
645                .iter()
646                .map(|split| split.len() + 1)
647                .sum::<usize>()
648        })
649        .sum();
650    let mut unique = HashSet::with_capacity(key_capacity);
651    let mut keys = Vec::with_capacity(key_capacity);
652    for hypothesis in &splits {
653        for range in &hypothesis.ranges {
654            if unique.insert(range.key.as_str()) {
655                keys.push(range.key.clone());
656            }
657            for syllable in &hypothesis.syllables[range.range.clone()] {
658                if unique.insert(*syllable) {
659                    keys.push((*syllable).to_owned());
660                }
661            }
662        }
663    }
664    keys.sort();
665    let known = weights(&keys);
666    let scores: Vec<f32> = splits
667        .iter()
668        .map(|splits| {
669            let word = splits
670                .ranges
671                .iter()
672                .zip(splits.iter())
673                .filter_map(|(range, split)| {
674                    known
675                        .get(&range.key)
676                        .map(|&weight| (weight.max(0) as f32).ln_1p())
677                        .map(|weight| weight + WORD_SYLLABLE_BONUS * (split.len() - 1) as f32)
678                })
679                .fold(None, |best: Option<f32>, weight| {
680                    Some(best.map_or(weight, |b| b.max(weight)))
681                });
682            word.unwrap_or_else(|| {
683                splits
684                    .iter()
685                    .map(|split| {
686                        split
687                            .iter()
688                            .map(|syllable| {
689                                known
690                                    .get(*syllable)
691                                    .map(|&weight| (weight.max(0) as f32).ln_1p())
692                                    .unwrap_or(0.0)
693                            })
694                            .fold(f32::INFINITY, f32::min)
695                    })
696                    .fold(0.0, f32::max)
697                    - UNKNOWN_WORD_COST
698            })
699        })
700        .collect();
701    let mut ranked: Vec<GlideHypothesis> = hypotheses
702        .into_iter()
703        .zip(scores)
704        .map(|(hypothesis, score)| GlideHypothesis {
705            cost: hypothesis.cost - DICTIONARY_WEIGHT * score,
706            ..hypothesis
707        })
708        .collect();
709    ranked.sort_by(|a, b| {
710        a.cost
711            .total_cmp(&b.cost)
712            .then_with(|| a.letters.cmp(&b.letters))
713    });
714    ranked
715}
716
717#[cfg(test)]
718pub(crate) mod tests;