fanlusky
ccbaa3a631
fix(engine): 送进 Google 解码器前把 ü 换成 nue/lue/ju 写法
...
小鹤双拼 ntdddswu 出的是「女蛾黛动物」而不是「虐待动物」:nt 转成全拼后
是 nve(shuangpin_pinyin_set 把 nue 抹掉了,词库与词格那一侧统一用 v 形),
而本地 googlepinyinime-rev 的拼写表和 inputtools 云输入都不认 nve/lve/jv,
只有 nue/lue/ju/jue,也不做 v→ü 的转写。带分隔符的串照样送过去了,解码器
却把 nve 重新切成 nv + e,于是有了「女蛾」。
加一个 quanpin::to_google_spelling,按音节整体改写 nve→nue、lve→lue、
jv→ju 这一类拼写,只在两个 Google 解码器的边界上用:云输入的 query_text、
双拼与全拼的整句回退。词库查询和词格用的仍然是 canonical_syllable 那一侧的
v 形写法,没有动。nv'er、lv'se 这些 Google 也认的写法原样通过。
2026-09-19 12:29:01 +08:00
fanlusky
e2a5f5f988
fix(engine): 词格整句只用精确读音,并补上多音字读音先验
...
长句联想此前会把读音对不上的词拿去造句,也分不清多音字的常见读音与生僻读音。
词格的跨度查询改用批量精确键查询。原先走的 query_segments_keyed_flat 带降级:
精确键查不到就退到前缀区间扫描(key >= "gun'qi" AND key < …),gun'qi 因此捞出
gun'qiu 的「滚球」,简拼那两级同理。联想出来的每个词,读音都必须精确对上用户
敲的那段拼音。顺带省掉了白扫的前缀区间,也不再需要 QuerySource——区分全拼与
双拼的只有简拼那几级。
新增读音先验:每一行按「自己的权重 / 该拼音跨度的权重和」算读音占比,占比低于
免罚线的部分按 log10 扣分。语言模型只看汉字,分不出「卷」读 gun、「而」读 neng
这种词库里权重为 0 的多音字读音,于是 gun'qi 出「卷七」、neng'fa'sheng 出
「而发生」。不能整体按 log10 P(词|拼音) 扣,那等于把字频算两遍(语言模型已经
算过一次),「跑得很快」会被「跑的很快」压掉——得/地 各占 de 的 7%~9%,本来就是
常见读音;真正的生僻读音占比是 1e-5 量级,离免罚线还差好几个数量级。
ü 的两种拼法归一化到词库存的那一种。intact_pinyin_list() 两种都认,词库只存
一种(j/q/x/y 后写 u,l/n 后写 v),切出来的 jv、lue 这类音节因此一行都查不到。
改精确键查询之后这会让整张词格断在那里——wo'men'lue'de 解不出任何路径,长句
联想整个消失。lue 和 lve 本来就是同一个读音的两种拼写,归一化之后仍是精确匹配。
新增两条回归用例:生僻读音不得赢下词格;421 个合法音节每个都必须在词库里至少
查得到一行,切分器与词库哪边漂了都先在这里炸。
2026-09-19 10:08:39 +08:00
fanlusky
03a5b4fe26
feat(engine): 整句候选把词格排到 Google 解码器之前
...
词格换成 kenlm 三元模型打分之后,它出的整句整体比 Google 解码器那条准,
所以由它占首位。
merge_lattice_candidates 的插入点本来就在 Fallback 之前,word_lattice.h
里也是这么写的不变量;真正把次序翻过来的是词典层在合并之后的一个后置
步骤——它把 Fallback 重新提回表头。全拼和双拼各有一份,这次都删掉。
这一步此前没有任何测试盯着:test_pinyin 里的用例断的是
merge_lattice_candidates 自身,词典层怎么再排它看不见。补一个查询层的
回归用例,断相对位置而不是具体句子——词格出什么取决于 msime.db 和
sc.lm,钉死了会随词库更新而碎;两条来源凑不齐时按既有风格优雅跳过。
顺带修掉 ScopedLocalAppDataOverride 上一条已经过期的注释:dict_pinyin.dat
现在随安装包发布了,不发布的只有 role=user 的 user_dict.dat。
2026-09-19 10:08:39 +08:00
fanlusky
80b1fc42a1
feat(engine): 词格整句改用 kenlm 三元模型打分
...
词格原先的整句打分是一份没校准过的启发式 unigram,质量很差。这里内嵌 libime
在用的 kenlm 查询子集,改由它的三元模型 sc.lm 来打路径分。
- engine/ngram 只编 kenlm 的 query / trie-build 路径。不定义 WITH_THREADS,
parallel_read.cc 因此退化成串行实现,整个子集零 Boost 依赖。
KENLM_MAX_ORDER=3 必须与建模阶数一致,改它等于读不懂已发布的模型文件。
- ngram::LanguageModel 是唯一 include kenlm 头文件的地方,对外只给出一个纯
std 的头,_HAS_AUTO_PTR_ETC 和 kenlm 的 include 目录都保持 PRIVATE。
- shared_language_model 按资源路径共享一份 mmap。kenlm 的查询是只读的且把
上下文放在调用方的 State 里,所以并发会话不需要额外加锁。
- 模型缺失时 valid() 为假,词格退回原来的启发式打分:整句候选只是变差,不会
停止工作。测试覆盖了缺模型降级与按路径共享。
- 出货的 ARPA 不带 <s>/</s>,所以解码从 null_state() 起算而非 begin_state()。
kenlm 是上游副本,不重排版、不进 clang-format,其 LGPL-2.1+ 与 BSD 例外清单
已补进 THIRD_PARTY_NOTICES.txt。googlepinyinime 那条 Fallback 整句路径不变。
2026-09-19 10:08:39 +08:00
Lu Fan(SonnyCalcr)
e1d53dd8f0
Merge pull request #426 from ouchihao/codex/fix-299-double-helpcode-cache
...
fix(engine): isolate ordered double-helpcode query caches
2026-09-18 21:34:50 +08:00
Ouchihao
0925937bfe
fix(engine): isolate ordered double-helpcode query caches
2026-09-18 20:43:17 +08:00
Ouchihao
5e641fb5a0
fix(engine): accept yo as a complete shuangpin syllable
2026-09-18 20:35:14 +08:00
Lu Fan(SonnyCalcr)
4dea1cd12f
Merge pull request #420 from metasequoiaime/feat/ctrl-arrows-segment-caret
...
feat(input): 组词内按分词的光标与删除(#35 / #187 / #347)
2026-09-18 12:35:52 +08:00
Jia Sui
2576986881
feat(windows): Ctrl+左右键按分词移动光标
...
组词中 Ctrl+Left/Right 按一个字的拼音(音节段)移动光标,
与 Ctrl+Backspace 的删除粒度共用同一套引擎分段边界。
- 闸门归纳为 SegmentEditFunction(code, modifiers):Ctrl-only + 组词存在,
VK_BACK/LEFT/RIGHT 分别映射到 segment 函数;三处判据与延迟投影共用
- 新增 FUNCTION_MOVE_LEFT_SEGMENT / FUNCTION_MOVE_RIGHT_SEGMENT,
编辑会话只按 CompositionRestored 的 caret 移动本地光标,不重建 buffer
- Server 用 segment_raw_boundaries() 取前/后边界,能力位与非 UiLess 时回包;
非拼音/本地模式/UiLess/旧端仍按单字符 ±1 回退
Refs #187
2026-09-17 23:36:34 +08:00
Jia Sui
b1ec3ab1e0
feat(server): Ctrl+Backspace 按分词删除前置拼音(#187)
...
组词中 Ctrl+Backspace 删除光标前的一个分词单元:剩余拼音非空时删
最后一个字的拼音(音节段),剩余拼音为空时删除最后一段已选分词。
engine:
- InputSession::segment_raw_boundaries() 作为按段删除/移动的唯一分段来源:
全拼把纠错显示串的分隔符映回 raw 下标,双拼与 pinyin_segmentation 共用贪心谓词,
2 字辅助码自成一段;非拼音方案与本地模式返回空表示无单位模型
server:
- IsSegmentBackspaceKey(Ctrl-only)、整段丢弃 / 边界截断 / 单字符回退三分支,
截断点残留分隔符清理
- 复用 CompositionRestored 回包(含 caret),仅协商能力位且非 UiLess 时发送;
新出现「只剩已选汉字、无拼音」的中间态
windows:
- 两处 Ctrl 闸门对组词中的 Ctrl+VK_BACK 放行,新增 FUNCTION_BACKSPACE_SEGMENT
与 _HandleCompositionBackspaceSegment(读回包重建,失败回退单字符)
- 空 raw 状态渲染、终态清理 word_for_creating_word,延迟投影失效处理
Closes #187
2026-09-17 23:01:31 +08:00
fanlusky
bc37ac9a9e
feat(installer): 安装时可自定义数据目录位置
...
词库、用户配置和皮肤此前固定在 %LOCALAPPDATA%\metasequoiaime,整包几百 MB
全压在 C 盘。安装向导新增一页,允许把这部分数据放到任意盘。
程序本体仍固定装在 Program Files:MetasequoiaImeServer.exe 带 uiAccess=true,
只有在受信任目录里这个标志才生效。
运行期的权威来源是 HKLM\Software\Metasequoia\MetasequoiaIME 的 DataDir。
Server、TSF DLL 和引擎三个组件各自按同一顺序解析:METASEQUOIA_IME_DATA_DIR
环境变量 -> 该注册表值 -> %LOCALAPPDATA%\metasequoiaime。三处各持一份实现,
因为组件边界规则禁止 windows/ 与 server/ 互相 include。32 位 TSF DLL 用
KEY_WOW64_64KEY 读,避开 WOW64 重定向。
安全相关的两个约束:
- 安装器在数据目录里写 .metasequoiaime-data 标记。覆盖安装的清理和卸载的整目录
删除只在看到这个标记(或目录就是历史默认位置)时才执行——用户可能把数据目录
指到一个本来就有自己文件的文件夹。
- 选择页与 /DATADIR= 走同一套校验:拒绝相对路径、盘符根目录、不存在的盘、程序
目录内部,以及 Windows/Program Files/各 AppData/用户主目录本身。
升级时改了位置,PrepareToInstall 在停掉输入法进程后用 robocopy /MOVE 搬运用户
拥有的数据(msime_user.db 及其 WAL/SHM/journal、config.toml、config.base.toml、
skins\),词库和前端资源由本次安装重新写入。跨卷移动目录 MoveFile 会失败,所以用
robocopy。用户词库搬运失败会中止安装:否则新版本对着空的 msime_user.db 启动,
ssPostInstall 的回放只记一句 skipped,用户的自造词就静悄悄没了。
测试侧新增 METASEQUOIA_IME_CONFIG_DIR,只改 config.toml 的位置而不搬词库。原先
靠重定向 LOCALAPPDATA 做隔离的办法在装了正式版的机器上已不再成立——注册表值
的优先级高于它。
2026-09-17 22:49:19 +08:00
Jia Sui
e64195731e
feat(server): Backspace 删空剩余拼音后退选已选分词(#35)
...
已选分词是造词中间态:删空剩余拼音时不再整条 composition 丢弃,
而是退选最后一段并恢复其原始拼音与候选。
engine:
- SelectionTransition 暴露本次选择消费掉的原始输入(保留大小写与双拼按键)
engine/contracts:
- 新增回复类型 CompositionRestored=14 与能力位 CompositionRestore;
MaxKnown 同步抬高,Server 按协商能力位门控发送,旧 DLL 不会收到未知帧
server:
- 造词中间态压 (consumed_raw, previous_creating_word) 快照,边界 Backspace 弹栈退选
- 任何清空 raw 的路径同时清 creating_word 与快照历史,避免跨 composition 泄漏
windows:
- _HandleCompositionBackspace 提示退选时读回包重建 keystroke buffer / word / 预编辑 / 光标
- NeedToCreateWord 与 CompositionRestored 共用载荷解析;旧 Server 与 UiLess 回退旧行为
Closes #35
2026-09-17 18:45:01 +08:00
Jia Sui
d4a0796400
feat(windows): 重构智能标点
...
原「智能标点」是小狼毫 digit_separators 的语义:字母或数字后的 , . : 直出英文,
2 秒内再按同一标点转回中文——后半段靠 SendInput 往应用里补一个 Backspace 再打
字符,落点全靠光标当时在哪。这次换掉主路径:输入中文标点后按空格,用 TSF 编辑
会话就地把它改写成对应英文(。,!?;:、 → . , ! ? ; : /),2 秒内再按同一标点
键撤回中文;原来的直出降为子开关,整个家族默认全关、改为显式开启。
- 按键与会话:转换/撤回是本地文档改写,不进 IPC、不进延迟队列;判据落在
_IsKeyEaten,OnTestKeyDown 的预判与 _DispatchKeyDown 的第二遍预判共用同一份,
声称吃掉的键不交还宿主。改写走编辑会话,请求不上时另起一个最小会话把键本身
的字符写回去,避免已吃掉的键静默丢失。
- 状态:原来散落的九个成员收成一个 SmartPunctuationAction(None / ChineseCommitted
/ AsciiConverted),带焦点令牌、前台窗口和 beforeChar 指纹;清理以分类结果为准,
丢焦点与上下文栈变化一并清掉。
- 转换范围:。,!?;:、 与单独出现的 “”‘’【】《》() 都是映射表里的单字符
条目,路径完全一致;成对补全产生的一对(autoClosedPair)不 arm,按空格只落空格,
不会留下 <》 这类孤儿。
- 开关:总开关 + 中文标点后按空格转换 + 同键转回中文 + 数字后直出 + 字母后直出,
五键全部默认关;直出拆成数字/字母两个独立子开关(IPC 24 收窄为数字、25 追加为
字母),TSF 按前字符类型分别查键。
- 命名:子开关「独立符号」改名「中文标点后按空格转换」,配置键
smart_punctuation_standalone → smart_punctuation_space_convert,IPC 22 只改常量名
(SmartPunctuationSpaceConvertChanged),编号与语义不变、新旧测试包互通;设置页
文案同步重写,五处配置链路与死宿主一并跟改。
- 兼容:旧键未出厂、不迁移(升级合并按模板缺键丢弃、取默认 false);编号 23 随
「按空格整体转换一对」的设计退役并留空不复用。
- 验证:TSF x86/x64 与 Server Release 编译、Server 335 单测、windows_ipc_contract
x64/x86、设置页 build + 27 测试、clang-format 18.1.8 全绿。真机未跑。
Signed-off-by: Jia Sui <jsfaint@gmail.com >
2026-09-17 09:29:57 +08:00
fanlusky
5a5dea6b7e
feat(japanese): 日语模式禁用 -/= 翻页并用 - 打长音符
...
日语模式下把 - 让给长音符(ー)输入,同时禁掉 -/= 的候选翻页:
- 罗马字方案接受 VK_OEM_MINUS,set_raw_input 不再把 - 过滤掉;
罗马字表里本来就有 - 到 ー 的条目,只缺按键准入。
- n 后面接 - 时按辅音处理,让前面的 n 收成 ん,避免 n- 卡在 pending。
- 空编码时单独按 - 也起头组合,候选框第一项是 ー、第二项是普通 -。
- 组词流程同样适用:判据只看日语模式加组合状态,不再走翻页和词转字。
- -/= 在日语模式下不翻页,_ = + 这些没有假名写法的字符退回标点上屏,
并把 = _ 补进服务端的标点集合,和 TSF 侧保持一致,否则会等不到回包。
TSF 侧的影子分类、IsVirtualKeyNeed(ForFreshComposition) 和服务端按键策略
三处同时改,统一以配置项 input_mode == japanese 为判据(R 模式不在范围内,
TSF 侧看不到临时会话,用会话类型判断会让两侧分类打架)。
2026-09-15 01:57:50 +08:00
Jia Sui
9bbd86b633
fix(engine): 调频权重基准限定同音节尺度
...
置顶/提升把一个候选的权重算成「显示列表首位权重 + 500」。显示列表会混入
备选切分词和不同切分的候选,它们的权重是另一套尺度(单字表是语料计数、
多字表是词组词权),于是基准取错就把候选写成垃圾值:用户机器上 xi'e 的西鄂
(w=6) 坐在 xie 列表首位时,置顶写会写出 506(= 6+500),低于些/血/谢,反而
被埋到三十名开外;将也以同样方式被写成 24240(= 激昂 23740+500)。
- 候选的 rank 与权重基准改在同音节尺度内计算:同音节数的表互相可比(九宫格
数字上下混多个单字表、简拼上下文混多个 key 都保留原语义),跨音节数不再
互相充当中点基准。选中词已是同尺度首位时不再写权重,避免被跨尺度反降。
- 写入仍按 entry_key 走,rebalance 不会落到别的 key 的行上(#36 的护栏不变)。
test_user_dictionary_journal 增加回归:混表列表里置顶写必须以些为基准,
且不再把些本身写成 506。
2026-09-14 20:14:28 +08:00
Jia Sui
809c62995b
fix(engine): 全拼模糊音下备选切分抢位
...
模糊音开启时 QuanpinDictionary::query 对整个候选列表按 pinyin 字符串长度降序
排序(本意是压住模糊/前缀候选)。备选切分的 canonical 键带撇号且音节更多
(xi'e / ji'ang / you'di'an),字符串比主切分键(xie / jiang / you'dian)更长,
于是权重只有 6 的西鄂、23740 的激昂从保护位第 2 跳到第 1,顶掉 xie/jiang 的
首候选;xie 连模糊变体都没有,仅因排序无条件执行就被波及。
- 排序键改为候选覆盖的输入字母数(去掉撇号),与用户实际敲的长度对齐:备选
切分键与主键同层交给词频,模糊/前缀候选仍排在整串匹配之后。
- 备选切分保护位加词频门槛:备选最佳词权重达到主切分首位 1/100 以上才提升。
真歧义保留(xian -> 西安 55K 对 先 1.66M),罕见重码被拒(xie -> 西鄂 6、
jiang -> 激昂 23.7K、youdian -> 尤迪安 7)。
test_fuzzy_pinyin 补齐四个场景回归断言(夹具权重取自出货词库);
test_quanpin_scheme 的保护位用例移到新契约:夹具备选词权重 1 -> 100,
让它留在门槛内继续覆盖「真歧义保留保护位」。
2026-09-14 20:14:28 +08:00
Evan
30a22e6f3d
feat(ipc): reserve dedicated voice control pipe
2026-09-14 19:04:20 +08:00
Evan
497b46e067
feat(ipc): validate voice control leases
...
* feat(ipc): validate voice control leases
* style(ipc): format voice control dispatcher
* fix(ipc): use local engine contract path
2026-09-14 18:21:49 +08:00
fanlusky
529060a359
feat(settings): API Key 配置项增加连通性测试按钮
...
设置页中所有需要用户自填 API Key 的配置项(AI 助手、候选词翻译、
语音识别、语音润色)旁增加「测试配置」按钮,点击后用当前填写的凭据
向服务发一个最小请求,即时反馈配置是否有效。
- server: 新增 ApiCredentialTest::Run 统一分发(chat 发 1-token 请求、
ASR 传静音 WAV、翻译发单词校验、豆包走 DoubaoAsrClient::TestCredentials),
settings 可执行文件补上 curl/zlib/bcrypt/winhttp 及 cloud/voice 源。
- engine/contracts: 新增 apiCredentialTest / apiCredentialTestResult 消息,
已 generate.py 重新生成并 sync-contracts 同步至 ui-html/shared。
- ui-html: credential-test 模块负责发消息与渲染结果;测试按钮改小两号;
外观页补充字体下拉框加宽至 1.5 倍、加号按钮宽度减半。
- docs(agents): 补充格式化会卡 CI 的注意事项——clang-format 须为 18.1.8,
且 format.sh 在 Windows 上因 venv 布局失效,需直接调 clang-format 自查。
2026-09-13 22:11:09 +08:00
Lu Fan(SonnyCalcr)
ac40675ab9
Merge pull request #368 from fanlusky/fix/quanpin-autocorrect-jianpin-compose
...
fix(engine): 全拼纠错支持纠正头部+简拼尾巴的组合 (follow-up #366/#367)
2026-09-13 16:40:49 +08:00
fanlusky
49d0bf58b5
fix(engine): 全拼纠错支持"纠正头部 + 简拼尾巴"的组合
...
问题:autocorrect_cut_kbest 只有在整个输入都能切成完整音节时才成功。
所以 hauzh(hau 打错 + zh 简拼)、hauz 这类"需纠正的头部 + 不完整音节
尾巴"整体切分失败,既不纠错也查不出词;而 huazh(hua 本就合法,zh 走
简拼)和 hauzhong(hua+zhong 都是完整音节)都正常。
改动:resolve_series_query 在整串纠错失败时,尝试剥掉结尾的简拼声母
(不完整音节:是合法拼音前缀但本身不是完整音节,如 zh/z/h),对头部
纠错,成功后把纠正结果与尾巴拼成 hua'zh 交给既有的简拼查询路径处理。
优先尝试更短的尾巴,让纠错尽量解释更多输入。复用同一套代价分层逻辑,
更贵的头部读音(如 hau→hai)仍排在便宜的(hau→hua)之后。
头部纠错排除邻键(neighbor)类型:邻键表最大、误报面最广,而本路径已经
放宽了"整串must是合法切分"的约束、去信任一个推测出来的简拼边界,两者
叠加会把漏字形状的输入变成噪声(shng → sun + g → 笋干,而 shng 在关闭
deletion 时本应不纠错)。换位/漏字/多字是结构性错拼,置信度足够,可组合。
效果:hauzh→hua'zh(化妆)、hauz→hua'z、gauzh→gua'zh(挂着)、
hauzhong→hua'zhong(华中)均可纠;huazh/nihaoa 等正常输入不受影响,
关闭纠错时也不组合;shng 关闭 deletion 时不再被组合成 sun'g。
验证:imetest 全绿(新增组合段位断言、关闭开关守卫、shng 不组合守卫);
server 集成测试全绿(QuanpinDictionaryAutocorrectDeletionTypo 等,0 失败);
eval(seed 42,300 样本)mixed 与 deletion 两模型逐档与基线一致;
scripts/format.sh --check 通过。
2026-09-13 16:25:32 +08:00
Jia Sui
94abc08e7c
feat(engine): ü 系拼写别名归一,非标准拼法候选带轻标记
...
nue/lue 是词库标准键 nve/lve 的混拼别名,此前按原样查库必然落空。
把别名归一收敛到 resolve_segments 唯一咽喉点(jqxy 系 jv→ju 既有行为
一并收敛),查询、调频、造词键全部落到标准拼法上;别名改写同步进
pinyin_segmentation_,mark_autocorrect_candidates 据此给非标准拼法的
候选写 corrected_from,前端以词尾 * 轻标记提示(对标搜狗纠错星标),
提交、调频、置顶、造词不受影响。
契约翻转:fold_autocorrect_letters 不再把 v 折叠成 u——旧契约
「别名永不标记」按产品决策废弃,别名即标记;nu/nv、lu/lv 是真实
不同音节,归一按段精确匹配 [nl]ue,绝不互串,nu'e 手动分隔符不受
影响。composition 层显示折叠保留(职责是分隔符重建的等价分类),
注释改写说明与字典侧标记契约的分工。
2026-09-13 15:56:29 +08:00
fanlusky
c064cc641a
fix(engine): 全拼纠错候选按编辑代价分层——便宜读音整组领先
...
问题:merge_alternative_segmentations 把首选读音与所有备选读音的候选
混在一起、纯按词典词频排序(注释自陈"跨切分词频不可直接比较",实现
却照排)。于是输入 gau 时,高频的 改/该(gai,邻键,权重13)盖过了
低频的 挂(gua,换位,权重10),尽管 gua 的编辑代价更低、更贴近"交换
字母顺序"的用户意图。
改动:
- AutocorrectCut 增加 edge_count/weight,report k-best 的编辑代价;
same_cost_as() 判定同代价。
- resolve_series_query 按代价把备选拆成两层:与首选同代价的进入词频
竞争(alternative_corrected_cuts,保留 uanli→{quan,cuan,...} 那类
等权消歧),严格更贵的落入 costlier_corrected_cuts。
- query_exact 将更贵读音排在整个首选代价层之后追加(仍查询、去重、
标注 corrected_from),词频不再能把更贵的纠错顶到便宜的前面。
权衡(重要):离线 eval(seed 42,300 样本)显示这是"编辑代价优先"
对"词频优先"的取舍——mixed 模型 R@1/R@3 各档下降约 8~17 个点,
deletion 模型小幅下降 1~3 点。原因是注错样本的原词分布跟随词频,
词频优先在整体召回上更强;本改动以召回换取"最便宜纠错优先"的确定性
排序,是产品取向的选择,经维护者确认后合入。
验证:imetest 全绿(新增 gau 代价分层用例、元音尾守卫用例);
scripts/format.sh --check 通过。
2026-09-13 15:06:55 +08:00
fanlusky
34c69fbe11
fix(engine): 全拼纠错门控——完整音节+单个元音尾不再当简拼,放行纠错
...
looks_like_syllable_with_jianpin_tail 把「若干完整音节 + 至多 1 个
结尾字母」一律判为简拼意图并压掉纠错。但该守卫只看尾巴长度,未区分
尾字母能否作简拼首字母:结尾是辅音(zheg = zhe + g)确实是简拼,
结尾是元音则不可能是简拼首字母,更可能是换位打错。
结果就是 gau 被判成 ga + 简拼 u 而不纠错,gauzai 因尾部剩 4 字母
逃过守卫却能纠成 gua'zai,行为不一致。
改为:结尾恰好 1 个字母时,仅当它是合理简拼首字母(辅音)才判为
简拼;裸元音结尾放行到纠错。gau -> gua、hau -> hua 现在可纠,
zheg / zher / keneng 等辅音尾仍不纠,多字母尾(sahng/shabg)不受影响。
验证:imetest 全绿(新增 gau/hau 守卫用例与 gau->gua 纠错用例);
eval(seed 42,300 样本)deletion 模型与基线逐档一致,mixed 模型
25%/50%/100% 档 R@1/R@3 各升约 1~1.7 个点、10% 档持平,读音还原不变。
2026-09-13 14:31:28 +08:00
fanlusky
3ee2ecdba2
fix(engine): 修复全拼纠错 k-best 的去重、权重与热路径开销
...
代码审查发现的问题:
- finalize() 用 std::unique 按序列去重,但列表按 (edge_count,
weight, arrival) 排序,相同序列的重复项并不相邻,会漏去重并
占用 k-best 槽位。改为按序列哈希用 unordered_set 全表去重。
- 每个假设都拼接完整 key 字符串(O(length^2 * k) 分配)仅用于
去重。改用滚动哈希 seq_hash,去掉逐节点字符串物化。
- 合并纠错边时按 min 取所有表(含被禁用表)的权重,会让被关闭
的表泄漏更低的 tie-break 代价。改为查询时仅按启用类型的子集
计算权重(min_enabled_correction_weight)。
- resolve_series_query 在 series-cache 命中前也重跑 k=9 束搜索。
按输入元组缓存 SeriesQueryResolution(纯输入函数,与词典内容
无关,无需失效),退格/重打前缀不再重算。
- mark_autocorrect_candidates 用 any_of 匹配任意备选读音,若某
备选折叠后恰好等于原始字母,会给用户拼对的候选误标
corrected_from。改为仅匹配与原始字母不同的读音。
验证:imetest 全绿;eval_quanpin_autocorrect(mixed/deletion,
seed 42,300 样本)R@1/R@3/读音还原与基线逐档一致,p50 略降。
2026-09-13 14:09:18 +08:00
Jia Sui
6673155c7b
fix(engine): 双拼置顶权重静默失效——直切完整拼读检查并去除二次归一化
...
update_weight_by_pinyin_and_word 判定输入已是 canonical 全拼只看切分拼回
原串,而 correction 模式的 greedy 后备能把 qbtmuo 切成 q'b't'mu'o 这类恰好
拼回原串的碎片,归一化被跳过后 resize 截出错误 key;build_quanpin_sql_for_
updating_word 又对调用方已归一化的键再过一次双拼归一化,qin'tian'shuo 被
重切成 qi'n'ti'an'sang'shuo。两层叠加使三音节置顶 UPDATE 匹配 0 行仍返回
OK。直切增加完整拼读检查(与 delete 路径语义一致),SQL 构建器信任调用方
传入的 canonical 键。钉住用例 ThreeSyllables 已在前一提交启用。
2026-09-13 11:04:34 +08:00
Jia Sui
3c2f3ae374
feat(engine): 全拼纠错补齐漏字/多字,k-best 切分按词频消解歧义
...
此前纠错只有交换(sahng -> shang)与 QWERTY 邻键(shabg -> shang)两类,
生成器在离线阶段就把「一个错串对应多个音节」的变体整个丢掉,运行时也只
取代价最低的一条切分。三类输入因此没有出路:漏字的(shng)、多敲一键的
(shangg、sjhang)、漏声母连基础切分都切不出来的(uanli 想打 quan'li)。
本次按专利 CN 101133411 B 的思路重排整条链路:错串可能对应的读音全部
保留到运行时,先由 k-best 切分带出平行读音,再让词典词频决定谁排前面。
- 纠错类型从两类扩到四类:新增漏字(shng -> shang)与多字(shangg 双打、
sjhang 邻键 -> shang)。两个新位跟随既有 autocorrect_transposition /
autocorrect_neighbor 开关联动(任一打开即生效),定义保持独立,留待
将来的独立配置键。查询与 preedit 重建共用同一映射,两侧纠正空间一致。
- 生成器从两表扩到四表并保留歧义变体(ahan -> {shan, zhan});同一条
(错串, 正确) 对被两条规则产出时在两张表各留一份,保证每个类型开关
单独有效。正确音节收敛进 kCorrectSyllables 单表、行内改存 16-bit
下标,一万七千多行不再各自重复音节字符串,--check 拦截过期表头。
- 切分层新增 autocorrect_cut_kbest:按位置自左向右做 top-k 假设传播,
排序键是(纠错边数,边权之和,生成序)——最少纠错优先的契约不变,
权重只在同边数内打破平局,引擎绝不用一条额外纠错换更低权重。边权取
专利 [0052] 允许的估常量(交换 10 / 漏字 11 / 多字 12 / 邻键 13),
标定路径是挖掘用户日志。漏字/多字边不等长,切分段改按 raw 长度推进
位置,preedit 分隔跟着原始字母的区间走。
- 词典层固定 k=9,为离线评测定值:漏声母键 uan 有 10-15 个等权漏字
目标,k=3 按表序截断够不到 quan'li 的正确读音;k=9 提升 R@1/R@3
且 p95 不变。主切分仍是查询键,其余读音交给
merge_alternative_segmentations 与主读法在词频下竞争;备选读音在
缓存命中路径同样发布,corrected_from 标记对全部纠错读音生效;纠错
读音排在音形别名读音之前。结尾单插入(shangg = shang + g)天然是
简拼尾形状,仍被简拼形守卫拦下——多字纠错的可达面在中间/开头插入
与多音节输入。
- 纠错门放宽:!segments.empty() 从门里去掉。BFS 工作在原始字母上,
基础切分为空的丢声母输入恰是最需要纠错的一批,jian -> ian 现在能
走到纠错候选并按词频合并到首位。简拼形守卫保留:合法音节加至多一个
尾字母(zheg)是用户意图,任何纠错表都不得改写。
- preedit 补两道防线:zheg 在显式三位掩码下仍走简拼守卫,不被漏字表
重排为 zheng;只有 BFS 切分的读音与 scheme 别名层完全一致时才按
BFS 位置重画分隔符,否则 sahng 会被切成 sa'hng 而候选实际来自
别名读音 shang。
新增独立评测工具 eval_quanpin_autocorrect:从真实 msime.db 采样高频
词条,按交换/邻键/漏字/多字模型注错,测 R@1/R@3、读音还原率与温态
p50/p95,固定种子可复现。它是评测工具不是测试,永远不进 CI,作为
纠错阈值与门逻辑改动的零回归标尺;注错规则与生成器逐键一致,改任一
侧须同一提交同步另一侧。
表格测试改为按 (错串, 正确) 对断言唯一并加覆盖门(合计 >= 4700 条),
新增歧义保留、漏字/多字变体、k-best 排序与权重定序、preedit 守卫等
用例;engine/tests 的 README 与 AGENTS.md 同步写清 imetest 与评测
工具两个产物的边界。
2026-09-13 11:04:34 +08:00
Jia Sui
f28b01faf0
test(engine): 修复 imetest 构建树
2026-09-13 11:04:34 +08:00
fanlusky
cbd95f74e3
fix(engine): 消除 /W4 下的编译警告
...
一方代码在 /W4 下已零警告。剩余警告全部来自上游副本
(googlepinyinime-rev/、vendor/opencc/ 及其捆绑的 marisa),
按 AGENTS.md 保留上游原格式,有意不改。
- data_path.h、test_pinyin.cpp: _wgetenv 改 _wdupenv_s,返回值
用 optional 包一层,由 unique_ptr 负责 free
- quanpin_dictionary.cpp: 去掉条件表达式内的赋值。外层查询原本
只用其真假,reset_cache_if_database_changed() 可能使其失效,
故保留重取
- build_sql_for_checking_word: 删掉从未使用的 jp 形参
- 未使用的 catch 形参、int/size_type 符号不匹配
- wav_writer.h: 形参 sample_rate 遮蔽同名命名空间常量,改名
requested_sample_rate;默认值改为直接引用该常量,去掉魔数
- pinyin_file_io.h: _wfopen/_wopen 改 _wfsopen/_wsopen_s
最后一项没有用微软推荐的 _wfopen_s: 它打开的文件不可共享,而这是
词库文件,Server、TSF DLL 和设置进程会并发读同一份。_SH_DENYNO
正是被弃用调用原本的共享语义,因此只消除警告,不改变行为。
_wsopen_s 的 pmode 传 0,唯一调用方 userdict.cpp 用的是 O_WRONLY,
不带 _O_CREAT。
2026-09-13 02:22:53 +08:00
fanlusky
426bb9d544
feat(server): WebView2 候选窗口支持滚轮翻页,并加上开关
...
PR #351 只给 D2D 候选窗口加了滚轮翻页,WebView2 后端还没有。
网页端只上报一个以 WHEEL_DELTA 为单位的原始 delta,notch 累加仍然
交给 CandidateWheel::ConsumeWheelDelta,两个宿主最终都汇聚到
WM_PAGE_CANDIDATE,翻页逻辑一份不重复。Chromium 的 wheelDeltaY 本身
就是 ±120/格,正是原生累加器期望的单位,deltaY 作标准回退。
同时把滚轮翻页做成可配置的 general.paging_mouse_wheel,默认关闭:
候选宿主是 NOACTIVATE 且从不获得焦点,滚轮只有在 Windows「当我悬停
在非活动窗口上方时滚动它们」开启时才会送达,无法无条件承诺,所以设为
opt-in,设置页里也写明了这个前提。开关只在 WM_PAGE_CANDIDATE 一处
判断;D2D 那边在 WM_MOUSEWHEEL 顶部提前返回 false,关闭时消息照旧
交给 DefWindowProc,与功能加入前一致。
2026-09-13 01:02:02 +08:00
fanlusky
a8c10a63a6
feat(settings): add a restart-server button under the UI backend setting
...
界面渲染 only takes effect at Server startup, so the setting already told the
user to restart the IME process but offered no way to do it outside the
Ctrl+Shift+Alt+R developer shortcut. Add a 重启 button in the same section.
Settings runs in its own process, so the request travels the Aux pipe — the
supported cross-integrity control path the config notifications already use —
and the Server exits with the Watchdog restart code. The shortcut and the
button now share RestartServerProcess().
2026-09-12 16:41:13 +08:00
fanlusky
e835e09c20
fix(engine): give whole-sentence fallback candidates a canonical pinyin
...
The Google-pinyin whole-sentence candidate is inserted at index 0 in both
quanpin and shuangpin, so it is what Space commits. All four construction
sites built it without a canonical quanpin reading, so when it finished a
word-creation session update_creating_word_progress() had no reading to
join and can_store stayed false: the prefix plus the sentence went to the
host app but were never learned.
Carry the converted quanpin segmentation on those candidates, and extract
the early-return storage condition into a testable predicate.
2026-09-12 15:53:54 +08:00
fanlusky
a7779d2b05
refactor(engine): 将 engine 从 submodule 内嵌为仓内一等组件
...
`vendor/MetasequoiaImeEngine` submodule 改为 `engine/` 目录,导入的是
product-lock.json 原本钉住的提交 c810d201,逐文件核对过 blob,因此这是一次
搬迁而不是引擎升级。引擎自己的嵌套 submodule(googlepinyinime-rev、utfcpp、
miniaudio)按各自 gitlink 一并展开,来源与提交见 engine/UPSTREAM.md。
Windows 专门化裁掉了不属于本产品的部分:whisper.cpp 与本地推理 provider、
macOS 示例宿主、整个 dictionary/ 建库流水线、上游 CI 与 release-please 配置。
词库仍按 product-lock.json 在构建时取发布产物。
引擎不再进 product-lock.json:没有 gitlink 就没有可比对的东西,锁里写一个上游
commit 等于宣称这棵树仍等于上游,而第一个 Windows 专门化改动落地时这句话就假了。
verify-contracts / verify-checkout 随之移除,verify-published 保留。
CI 与工具链:引擎源码并入根 scripts/format.sh(删掉引擎自己的那份,避免两套排除
列表分叉);引擎 voice.yml 里的 silero_vad.onnx 摘要校验移到本仓 lock job;
engine-bump-triage 工作流与其测试删除;gitsubmodule 的 dependabot 频率由 daily
改为 monthly。
README、README.en、AGENTS.md、CONTRIBUTING、docs/product-release.md 及各子目录
文档同步说明新的位置与边界。
2026-09-12 03:40:48 +08:00