Commit Graph
56 Commits
Author SHA1 Message Date
fanlusky 57dd0fc2f3 fix(engine,server): 整句来源标签只挂在整句联想候选上
候选窗按 CandidateSource 挂〔Trigram〕/〔Unigram〕,但 Generated/Fallback 同时被
原样上屏兜底、英文模式原文、日期、Unicode、假名、译文义项等合成候选借用,导致
输入 E 等特殊模式前缀时原样候选被误标为〔Unigram〕。

WordItem 新增 sentence_association 标记,只由 Google 解码器整句(全拼、双拼)与
kenlm 词格整句置位;Server 挂标签时同时要求来源与标记。source 本身不变,落库与
学习策略不受影响。
2026-09-27 08:25:51 +08:00
Lu Fan(SonnyCalcr) 4375d80414 feat: 支持导入自定义辅助码,内置“加加”更名为“拼音加加” (#477)
* feat(engine): 支持 helpcodes/custom 下的自定义辅助码

custom 目录里的每个 .txt 都是一套方案,方案名为 custom/<文件名>。
文件开头的 "# name:" / "# name_en:" 注释给出中英文显示名,缺省用文件名;
兼容 BOM、CRLF 和全角冒号。方案名不能跳出 custom 目录。

* feat(engine): WebView 契约新增 openHelpcodeDirectory

设置页请求打开自定义辅助码目录,仅允许 settings 页发送。同步 ui-html/webview2/shared。

* feat(server): 设置页列出自定义辅助码并可打开其目录

配置快照带上 helpcode.custom_schemas 与 helpcode.custom_directory,每次都会重新扫描;
方案文件被删除或改名后回退到 lantian。两个设置宿主都会处理 openHelpcodeDirectory。

* feat(ui-html): 辅助码设置显示拼音加加与自定义方案

内置方案“加加”改名为“拼音加加”。自定义方案追加在内置方案之后;新增说明区,
提供“打开目录”和“刷新方案”两个按钮,刷新完成后弹出提示。

* feat(installer): 升级与迁移数据目录时保留自定义辅助码

升级清理时只清掉 helpcodes 里的内置表,保留 custom 子目录;更换数据目录时一并迁移;
安装时建出 helpcodes\custom。
2026-09-26 20:21:55 +08:00
fanlusky b3b3b3303c feat(windows): 焦点切换到另一个输入框时提示当前中英文状态
新增开关 general.caret_state_indicator_on_focus(默认关闭,需同时开启光标
状态提示)。

- DLL 把以下情况视为“换了输入框”:焦点窗口变化(Win32 编辑框、对话框、
  其他应用各有自己的窗口);焦点经过一段没有可编辑文档的间隙后落到可
  编辑文档上(Chromium、WPF 等单窗口宿主在字段之间就是先失焦再聚焦);
  焦点会话真正结束后重新开始。Chromium 打字时在两个可编辑文档之间直接
  替换文档管理器,不会触发。从 Win+. 面板返回不计入;线程焦点在真正的
  应用切换后返回计入。一次切换的多次回调由定时器合并为一次提示;组字
  过程中、以及没有光标的控件都保持静默。每次判断在 TSF 诊断日志里记录
  判定标志,不含任何文本
- 协议复用 IMESwitch:wch 改为 FanyImeCaretStateTrigger(UserToggle /
  CapsLockEdge / FocusEntered),CapsLockEdge 保持首发时的 VK_CAPITAL
  取值,Server 对未知值按 UserToggle 处理
- Server 在开关打开时显示当前的有效模式(Caps Lock 开启时为“英”);
  配置项同步写入两份模板和两个设置窗口宿主的读写映射
- 设置页在光标状态提示卡片中增加“切换输入框时提示”开关
2026-09-26 17:09:34 +08:00
fanlusky 597d087580 fix(server): 光标状态提示仅由用户快捷键触发并收紧协议边界
- DLL 只在 Shift/Ctrl 切换、Shift+Space、Ctrl+.、Caps Lock 与
  Ctrl+Shift+F 处显式请求提示;Server 下发的模式同步、宿主转换模式
  同步和激活初始化不再弹出提示,移除 compartment 回调里的推断逻辑
- 恢复文档切换时候选窗的 OnKillThreadFocus,改用 COM 身份比较
- 能力位改名为 CaretStateIndicator,DLL 仅在 Server 确认后发送切换
  事件;Server 端这些事件只驱动提示,不再改写工具栏或状态快照
- 标点提示的中英模式由 DLL 随 PuncSwitch 携带
- UILess 宿主不显示提示,会话转为 UILess 时隐藏已显示的提示
- Ctrl+Shift+F 的锚点就地读取,不再覆盖 Global::Point,且只采信
  协商了该能力的客户端
- 撤回对所有包默认 point 的修改;提示窗口创建失败不再阻断 Server
  启动;提示内容改为结构化 CaretStateBadge
- Caps Lock 在同一按键重复进入 TestKeyDown 时只处理一次
2026-09-26 13:58:22 +08:00
fanlusky dc7cfa0354 Merge branch 'develop' into feat/issue-229-ime-status-indicator 2026-09-26 13:58:03 +08:00
fanlusky e7f7addd4f feat(engine): 神经网络整句联想与词格 n-best 重排
- 新增 engine/neural:移植 chinese-ime-lm 的字符级 Transformer 推理,在词格解出的最多 12 条
  n-best 里重排挑选整句,desktop / keyboard 两档模型各出一条首选,后台 worker 异步打分
- 词格支持 n-best 输出与重排(quanpin/lattice_rerank),全拼、双拼共用
- 新增 SentenceAssociationOptions,经 QueryRequest 传给各 provider;四个来源默认全关
- 资源契约登记两个 safetensors 与 NOTICE;模型二进制不进仓库,由
  scripts/fetch-neural-model.ps1 按 neural-model/lock.json 下载并校验 SHA256
- 新增 neural_sentence 测试
2026-09-24 14:03:39 +08:00
Jia Sui 5b0be4e71d Merge branch 'develop' into feat/caret-driven-resegmentation 2026-09-24 08:45:54 +08:00
Jia Sui 54c41d34b2 fix(engine): reset_cache 连带失效 caret 前缀候选缓存 2026-09-23 21:32:59 +08:00
fanlusky 7b7b19f449 fix(wubi): 前缀提示不挡混合拼音、候选去重,修复顶字/自动上屏后快打丢字母
- 只有精确码行才算五笔表已回答,前缀提示不再阻止混合拼音回退;拼音无结果时保留提示
- 五笔前缀查询按词去重,保留精确码行
- HideCandidate 抑制改为推送成功记账、Hide 消费(带 1s 时限),不再被下一个按键撤销;
  四码唯一自动上屏路径同样记账
2026-09-23 15:41:04 +08:00
AaGMix a855a9c7f4 feat(windows): 发送光标状态切换事件 2026-09-22 21:20:07 +08:00
Jia Sui 1cfba58eba feat(engine): 光标驱动的拼音前缀解码与候选重算
caret 从纯显示状态升级为查询管线输入:set_caret 量化到最后一个完整
音节单元边界(只消费 segment_raw_boundaries,#187 契约),前缀候选
独立解码、preedit 保持全串拼音风格;纯移动光标同样重算候选。
IInputScheme::set_raw_input 提升为接口方法以支持不动现场组合的
独立前缀查询。runtime_isolation 两处按 R3/R4 新语义改写(caret 在
音节中间 floor 到空前缀、候选为空是预期状态);新增 caret 前缀
会话单测(engine/tests + server CI 目标,隔离词库夹具,覆盖
全拼 floor、双拼贪心配对、五笔无单元模型、boundary=0 空候选)。
2026-09-22 14:56:22 +08:00
Jia Sui c6632e8d85 feat(engine): 五笔前缀逐键提示与调频接线
- 码表查询从精确匹配改为前缀匹配:精确等长行优先(保住一级/二级简码),
  前缀行按权重降序跟随,LIMIT 50 截断(引擎层不读 server 配置的分层边界)
- WubiCandidateProvider 注入 RuntimePaths:接通 update_weight / delete_by_key
  到 user_dictionary journal;新增 bump_wubi_weight(同码组 MAX+1、主库更新
  与 journal upsert 同事务,行缺失不造行)
- create_word / cache_dynamic_candidate 补「为何 no-op」注释(后续任务范畴)
- 两个 toml 的 wubi_schema 注释与实现对齐:当前仅支持 wubi86
- server 测试:前缀语义、LIMIT 截断、上界构造、调频+journal 回放、删词墓碑
2026-09-22 09:23:25 +08:00
Jia Sui 536d0a95ef feat(windows): 五笔四码后继续输入不丢字(顶字)
四码(不论是否唯一)之后继续敲字母时,先上屏该码的首选候选,再把这个字母
保留为下一次组合的开头。根因是两侧对「组合里有几个字母」认知不一致:TSF 的
本地按键缓冲跟着敲键涨(raw 预编辑样式下 preedit 就是它),服务端的五笔 raw
被引擎裁在四码;空格提交时组合文本被整段替换成候选并清掉本地缓冲,多敲的
字母随之丢失。

- 新增 worker 消息 CommitCandidateAndContinue(27),payload "<consumed>\t<text>":
  TSF 按服务端给出的消费数裁**自己**的组合缓冲,而不是套用服务端按旧视图算好
  的剩余串——快打时用户已多敲的字母因此不会被覆盖
- TSF 处理序列全部复用既有原语:_InsertTextToComposition + _HandleCompleteCommitFirst
  真提交,_StartComposition + AddVirtualKey + _HandleCompositionInputWorker(NO_REQUEST_ID)
  重建余下字母的组合;无组合时退化为直写
- 服务端顶字不看自动上屏开关:开关只决定唯一码要不要多敲一键才上屏,
  不决定丢不丢输入;自动上屏那条路也改用同一消息,顺带收掉快打竞态
- 引擎新增 wubi_four_code_is_complete(完整四码、码表作答、不要求唯一;
  空码不算——没有候选可提交时不触发顶字)

已知限制:光标在组合中间时的改码行为维持现状;空码继续敲字母仍会被忽略。
2026-09-22 09:23:25 +08:00
Jia Sui 5d1a9a6cc2 feat(server): 五笔四码唯一自动上屏
五笔敲满四码且码表只答一个候选时不再需要空格:第四个字母落下即上屏,下一个
字母直接开新组合。行为无条件生效,不设配置开关:业界默认开启(微软五笔、
搜狗五笔、极点、Rime 主流五笔方案),唯一候选没有第二选择,确认空格是纯冗余;
而一条开关要牵动配置五处同步,换不来可形成预期的行为差异,不值。

- engine 只报告事实(wubi_unique_four_code:完整四码、码表作答、候选恰好一个),
  是否上屏由 Server 决定;判定收在 input_key_policy.h 的纯函数里,随 portable
  目标进 ASan/UBSan;组词(creating_word)激活时组合保持打开,唯一四码不会把
  正在拼的词截断上屏
- 提交复用 ProcessSelectionKey,与空格上屏同一套记账;但只能走 worker 管道推送
  CommitCandidate:字母键在默认 raw 预编辑样式下不读请求-回复管道,回一帧 Normal
  既不会上屏,还会在 TSF 侧留下永不释放的回复帧,而 Server 已按 Normal 清掉组合
- 上屏前按当前组合同步重建候选页:候选页异步发布,提交路径读的 ui.items /
  page_words 否则可能还停在第三码那一拍
- 非唯一四码不做顶字上屏:一个按键只能回一条管道消息,真顶字需要扩 IPC 协议
2026-09-22 09:23:24 +08:00
fanlusky 837af70b7e fix(engine): 全拼云查询带上手动分词而非去边界的裸串
手动把 qi'e'huan 分成三段,云端却返回二段的「切换」。因为全拼云查询的 query_text
取自 normalized_input(已去掉撇号的裸串),云端拿不到音节边界只能自行贪心断句。改用
保留手动撇号的 normalized_segmentation 构造 query_text,与双拼分支一致;也让
to_google_spelling 能逐音节把 nve/lve 换成云端认的 nue/lue 写法。
2026-09-22 01:13:11 +08:00
fanlusky 99a8a355c0 fix(engine): 云/AI 联想候选上屏后按手动分词落库
选中云或 AI 联想候选上屏后,再次输入同样的拼音却不出现该词。根因有二:

- create_word_from_canonical_pinyin 已按 canonical 键里的撇号校验过分段,却又调用
  do_validate 二次校验;do_validate 会先去掉撇号、再用贪心 correction 重新断句,把
  qi'e'huan 切成 qie'huan(音节数 3→2),与字数不符而静默拒绝入库。移除这次多余且
  有害的二次校验,分段以调用方给定的 canonical 键为准(do_validate 本身保持不变,
  造词/调频/删除三条旧路径不受影响)。
- 上屏时 AI/云候选自身不带 canonical_pinyin,落库用的是去掉音节边界的
  committed_pinyin,同样会被重新贪心断句。改用会话里带撇号的 normalized_segmentation
  作 canonical 键,经 create_word_from_canonical_pinyin 落库。
2026-09-22 01:13:11 +08:00
Jia Sui 566ff8b832 feat(engine): 新增加加辅助码(拼音加加),第六套辅助码方案
按笔顺拆前两个部件、各取读音首字母(妈 = 女 n + 马 m = nm),部件用加加俗称读音(亻=d、
扌=t、纟=j、忄=s、攵=f、夂=z、刂=l、彳=s);138 个独体/部首字取「本字声母 + 起笔」。码表
7968 条,覆盖《通用规范汉字表》7846 字,比小鹤少 422 个扩展区生僻字——缺码会让含这些字的
词组拿不到辅助码,是已知取舍。

编码对齐拼音加加 5.x 安装包内的 fzm.bin(双拼键位已换算为声母):6487 字取到两码,6349 字
逐字一致,138 字保留本表规则,覆盖外的 800 字按同一规则重建。来源与非官方性质写在 NOTICE.md
与随包的 THIRD_PARTY_NOTICES.txt。

- engine/contracts/assets 注册 helpcode_jiajia,assets.h 重新生成(5 → 6)
- 设置页两个下拉显示名「加加」,两份配置模板注释同步
- 两侧测试补支持列表;README / README.en.md / server/AGENTS.md 改「六套」

默认值不变(双拼 lantian、全拼 ziranma)。
2026-09-20 22:00:34 +08:00
fanlusky 94df9a7917 fix(stats): 修复统计功能审查发现的 8 个问题
- Server 退出时唤醒统计管道监听线程,避免从未有客户端连接时 join 永久阻塞
- DLL 发送批次时对 ERROR_FILE_NOT_FOUND 重试,覆盖服务端重建管道的窗口
- 新增 StatisticsEnabledChanged(26) 推送开关,DLL 侧在采集入口直接短路:
  关闭时不读 range、不分类、不开管道
- stats_meta 的 schema_version 写入正确的键名(此前键与值都是 "1")
- dropped_count 仅在实际发送的批次中消费,空批次不再吞掉丢弃计数
- _CaptureCompositionStats 改为遍历 composition range 的克隆,避免移动宿主的活动区间
- FanyImeStatsEvent 显式声明 reserved 尾部,不再把未初始化的填充字节写入管道
- 透传统计去重标记在抑制一次后即消费,避免同一 tick 内的连击被漏计
2026-09-20 20:34:50 +08:00
Jia Sui ffcd1aee28 feat: 输入统计(DLL 采集、Server 聚合存储与设置页展示)
- windows: 三上屏出口 + OnTestKeyDown 直通字符采集,修复半角数字恒零
- engine: 统计 Web 消息契约(statsRequest/statsResponse)
- server: 统计管道接收、聚合、SQLite 存储、保留策略与每日自动清理
- ui-html: 设置页「统计」模块(卡片、热图、明细、开关、清理)
- docs: PRIVACY / README 改为内置可选统计
2026-09-20 19:08:16 +08:00
fanlusky 1ea01d5e0a fix(quanpin): 备选切分保护位只拉首页外的候选,不覆盖调频名次
保护位原来对任何排在 index 1 之后的备选切分都生效,于是调频刚把 吉安 排到
自然第 5 位,它就因为成了 ji'an 组里权重最高的词、过了 1/100 门槛而被重新
钉回第 2 位。用户选一次就跳到第二,之后再怎么调也只能停在第二——固定槽位把
调频写进权重的结果整个覆盖掉了。

改成只对自然排序已经掉出首页(默认 page_size 6)的备选切分生效。西安 在出货
词库里的自然位置是 16,仍旧提升到第 2 位;调频过的 吉安 留在它自己挣来的位置
上,之后按 promote 一步步往上走。

夹具同步补候选:engine 与 server 的 xian 夹具原来只有三条主候选,备选切分自然
就在首页内,测不到提升这条路径。
2026-09-19 22:42:29 +08:00
fanlusky e32eeadec9 fix(engine): 调频比较集跨键合并,按权重而非显示顺序取基准
打 jian 时列表里混着另一种切分 ji'an 的词。吉安 在出货词库里权重是 1,压在同组的
积案(9420)/几案(5999) 底下,而 adjust_candidate_ranking 按音节数圈比较集,于是
吉安 只跟 ji'an 这一组比,可学权重封顶一万出头,永远追不上 见 的 3460998——用户
选多少次,这个候选都纹丝不动。

音节数这个尺度是 #400 加的,为的是挡住另一个 bug:显示列表里备选切分词会被保护位
顶到最前面,中点数学却假定列表按权重降序,于是拿 西鄂(w=6) 当基准把 写 写成 506。
根子在「显示顺序 != 权重顺序」,按音节数滤掉跨表行只是绕开了症状,代价是把每个候选
关进自己的组里。

改成在比较集上直接按权重降序排一次,不变式在源头恢复:任何行都不可能坐在高于自身
权重的位置上充当基准,西鄂 那条路被堵死;同时比较集不再按音节数切开,吉安 的基准
取到整个列表最重的 见,置顶一次即越过,promote 模式连选五次一路走到首位。写入仍只
落在 entry_key 的行上,#36 的护栏不变。

test_user_dictionary_journal 新增两条回归(跨键置顶、连续 promote 收敛到首位),
#400 的两条断言原样保留并仍然通过。
2026-09-19 22:14:57 +08:00
fanlusky 01c5bca325 fix(engine): 选中的整句候选落成用户词组
词格(CandidateSource::Generated)和 Google 解码器(CandidateSource::Fallback)
猜出来的整句在词库里没有对应的行,而调频走的是
update_weight_by_pinyin_and_word——它只改 SQLite 里已经存在的行,找不到行就什么
也不做。于是用户选中这种整句多少次,下次输入同样的拼音,它仍然是猜出来的,仍然
排在词库里那条同音短语后面,看起来就是"永远提不上来"。

选中即落库才是对的处理:存成用户词组之后,下次它以 UserDatabase 候选出现,按
quanpin::generated_sentence_insert_position 的约定排在两条整句之前,之后再选还能
走正常调频。

落库不受 frequency_adjustment_ 约束(那是候选词频策略,不是造词),也不跳过
index == 0——首位整句同样没有落库,下次仍要靠猜。长度上限对标
quanpin::WordLatticeOptions::max_phrase_syllables,再长的整句只是这一次输入的
产物,落库除了撑大用户词库没有别的作用。
2026-09-19 13:48:17 +08:00
fanlusky b4728fdb3b test(engine): 九键删词只要求词库那一行消失
test_nine_key_session 的删词断言从词格整句合并(#430)之后就是红的:删掉
tbl_2_n 里的「你好」之后,重开 session 仍能看到「你好」,但它的 source 是
Generated——整句合成用还活着的单字「你」和「好」拼出来的,同一批候选里还有
「米好」。tbl_2_n 已经空了,删词本身没问题。

删词的契约是词库里那一行没了,不是这几个字从此打不出来(真要那样,删过一次
词就再也输入不了这个词)。断言相应收窄成 Database/UserDatabase 来源的候选。
2026-09-19 12:29:09 +08:00
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