T9 拼音是如何实现的?
Rime 本质上是 PC 输入法引擎,它有许多配置都基于实体按键。
key_binder:
bindings:
# 空格键选第一候选词
- { when: has_menu, accept: space, send: 1 }
# 回车键提交输入编码
- { when: composing, accept: Return, send: Return }
# 退格键删除
- { when: composing, accept: BackSpace, send: BackSpace }
//...
Xime 输入法安卓版在实现的时候,大部分配置都在安卓层实现。 Rime 大多数配置被无视,只提供了基本的输入法按键映射。
九宫格拼音诞生于早期的实体按键手机。现在使用九宫格拼音的用户基本来自安卓之前的实体智能手机时代。
这意味着,Rime 是不会有九宫格拼音的按键映射的,也就是 Rime 天生不支持九宫格拼音。
难点
从信息论的角度来看,26键到9键,每按键的信息熵都在降低。由于多个字母映射到同一个数字键,按键序列携带的信息量减少(熵降低)。用户输入相同长度的拼音时,9键产生的按键序列总信息熵低于26键序列。为了弥补这种信息损失,输入法需要借助词典、语言模型、上下文消歧等机制来推测用户意图。
其他项目做法
通过雨燕输入法和 rime-ice 的的配置来看,似乎是把转换逻辑全程交给 Rime,通过一些规则配置来实现(当然,我没细看,不一定对)。
我基于类似的原理做了一个初版,效果比较差,于是放弃这个方案。
微信输入法是一个很好的观察对象,通过逆向分析了发现,微信输入法的逻辑基本是C++实现的,非常复杂,所有逻辑都集成在一个叫 libwxhld.so 中。并使用了大量 Augmenter 来处理各种模糊音、方言等输入问题。
仔细分析
既然 Rime 不支持 T9 拼音,那有什么是可以复用的呢? 答案是全键盘拼音。
正常的全键盘输入,一般是链路如下:
输入事件 → Composition(编码区) → Speller(拼写计算) → Translators(翻译器链)-> 返回候选
我们只需要实现一个转换器(T9Decoder),把 T9 拼音的数字码转换成可能的拼音组合,然后送入到 Rime 处理,就做到了复用全键盘拼音方案。
转换器(T9Decoder)
想象一下,T9 输入就像在一条有很多条岔路(歧义)的路径上,我们需要找到概率最高的那一条路线。
比如 64426,
6 出现了3条路径,m、n、o,
4 同样有3条路径:g、h、i。
如果从6走到4,按概率算的话,靠前的排名可能有 mi、ni。如果是全拼,基本64 就这2个组合,其他的都是无效路径。如果要支持简拼和模糊音,则其他组合也必须是有效的,只是排名不会靠前。
我们需要把用户输入的数字串(如 64426 )看作一个地图,算法要找出最有可能对应的拼音串(如nihao)。
这个算法叫 维特比(Viterbi)解码算法。
整体路径:
数字输入 "64426"
↓
T9Decoder.decode("64426") ← Viterbi DP + Bigram LM
↓
List<Path> eg. [ni+hao,mi+han ...]
↓ UI 选择音节
"ni" + "hao" → "nihao"
↓ rimeEngine.processKey() 逐字符
Rime → 汉字候选项
Viterbi DP 解码
把数字串切分成拼音序列,要求:
- 每个切分片段对应一个合法拼音(或其模糊变体)
- 相邻拼音之间有合理的二元转移概率
- 长拼音获得正奖励(鼓励合并成完整音节)
DP 定义:
dp[i][pinyin] = 到位置 i、最后一个拼音是 pinyin 的最大 log 概率
back[i][pinyin] = (前一个位置, 前一个拼音) // 回溯用
初始化:dp[0]["<BOS>"] = 0.0,其中 BOS 表示句子开始。
对于数字串中的每个位置 i,算法尝试三种方式往前走:
方式一:精确匹配 + 模糊匹配
从位置 i 开始,尝试长度为 1 到 6(最长拼音长度)的编码段 code = digits[i..i+len],在预先构建的索引中查找匹配的拼音(同时包含精确匹配和模糊音扩展的结果)。对于每一个匹配到的拼音 pinyin,计算转移得分:
newScore = lastScore
+ lm.transitionLogProb(lastPinyin, pinyin) // 语言模型得分
+ max(0, (len(pinyin) - 2) * 0.3) // 长音节奖励
- (模糊匹配 ? 0.5 : 0.0) // 模糊音轻微惩罚
“长音节奖励”的用意是:算法倾向于把数字合并成完整的音节(如 26 → an),而不是拆成多个单字母(如 2→a + 6→n),因为完整音节往往对应真实的拼音。
方式二:键盘邻居容错
考虑到用户可能在九宫格键盘上按偏,每个数字键都有其邻居键集合。例如 5 的邻居包括附近的 2、4、6、8 等。算法会尝试把编码段中的某一位替换成邻居数字,再查拼音:
“64” → 尝试 “54”、“34”、“84”、“94” → 找到 “li”、“di”、“ti”、“yi” 邻居匹配的得分惩罚更大(-1.5),因为它本身就是一种容错兜底。
方式三:单字母回退
每个数字对应的字母都可以作为单独的“拼音”进入 DP 状态。比如 7 可以变成 p、q、r、s。虽然单字母不是合法拼音,但它们确保了 DP 图始终连通,不会出现死胡同。语言模型会对不合理的序列自动给出低分,从而在最终排序中被淘汰。
PinyinBigramLM
PinyinBigramLM 是打分的基石,通过词库文件生成。
T9Decoder 完全依赖 PinyinBigramLM 的 transitionLogProb 来区分“合理”和“不合理”的拼音序列。单字母回退路径虽然数量多,但因为 LM 给的单字母转移概率极低,自然会被 DP 剪枝掉。
查询逻辑如下:
fun transitionLogProb(prev: String, cur: String): Double {
// 优先查二元概率
// 查不到就退回 cur 的一元概率(backoff)
// 连一元也没有 → 全局最小概率(-20)
}