1use std::collections::{HashMap, HashSet};
13use std::sync::OnceLock;
14
15use super::syllables::{intact_pinyin_list, is_intact, MAX_SYLLABLE_LENGTH};
16
17const MAX_SAMPLES: usize = 128;
19const SAMPLE_STEP: f32 = 0.25;
21const ALIGN_SCALE: f32 = 0.3;
23const LINE_SCALE: f32 = 0.25;
25const LETTER_COST: f32 = 0.8;
27const REPEAT_COST: f32 = 0.8;
29const DICTIONARY_WEIGHT: f32 = 0.7;
31const UNKNOWN_WORD_COST: f32 = 6.0;
33const WORD_SYLLABLE_BONUS: f32 = 1.5;
35const BACKTRACK_COST: f32 = 2.0;
37const SKIPPED_DWELL_COST: f32 = 4.0;
39const NEAR_DISTANCE: f32 = 1.4;
41const TOKENS_PER_SAMPLE: usize = 6;
43const BEAM: f32 = 10.0;
45pub const MAX_GLIDE_LETTERS: usize = 30;
47const 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 pub time_ms: Option<u32>,
57}
58
59#[derive(Debug, Clone, PartialEq)]
61pub struct GlideKeyboard {
62 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 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 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
101pub 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
129fn 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
159fn 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
199struct SyllableTrie {
201 children: Vec<[u16; 26]>,
203 intact: Vec<bool>,
204 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 fn followers(&self, states: &[u16]) -> u32 {
246 states
247 .iter()
248 .fold(0, |mask, &state| mask | self.masks[usize::from(state)])
249 }
250
251 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
270struct 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
287struct 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 align: [Option<Vec<f32>>; 26],
305 reach: [Vec<usize>; 26],
307 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 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 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 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 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 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
523fn 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
570struct 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
629pub 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;