45 Commits
Author SHA1 Message Date
Lu Fan(SonnyCalcr) a60e2948db fix(engine): 整句联想按 P(读音|字) 压住多音字的生僻读音 (#1123)
tai'zhuan'qian'le 出「大赚钱了」:大 也读 tai,词库里这一行权重 4719,占 tai 的
0.47%,已有的 reading_prior(看 P(字|拼音),1% 免罚线)只扣 0.33,翻不过语言模型
对「大赚」约 1.6 的偏好。换个角度看,大 读 tai 只占它全部权重的 0.14%——这才是
「这个字几乎不这么读」,语言模型只认汉字,这一维要由读音补上。

词格跨度查询对单字行加一项 P(读音|字) 惩罚:占比低于 10% 的部分按 log10 扣分,
上限 2.5。长 chang、重 chong、行 hang 这类常见第二读音不扣;大 tai/dai、会 kuai、
都 du、给 ji 扣 1.5~2。只罚单字,词本身就是读音的证据(会计、成都、大夫)。多音字
各读音的权重和在第一次整句联想时扫一遍单字表载入,全拼与双拼(含直接辅助码)共用。

自建 111 句多音字评测从 79 提到 82,无回归;5% 档翻不过「大赚钱了」,20% 与 10%
结果相同,取保守的一边。test_engine_smoke 加固定权重的断言,句子评测集补两条。
2026-10-20 08:47:27 +08:00
Lu Fan(SonnyCalcr) 6e15526e75 perf(server,ui,engine): 降低 Server 常驻内存并加快候选去重 (#1078)
- ui: 同一线程上的窗口共用一个 D3D11 设备。候选窗、悬浮工具栏、托盘菜单原先各建一个,
  每多一个硬件设备约多占 10 MB 私有内存;设备被移除时下一次取用会换新的。
- engine: U 模式拆字表改为单块文本池加 16 字节定长条目,常驻内存从约 12.6 MB 降到约 3.9 MB,
  查询结果不变。
- server: 未开启语音输入时,提示音播放器推迟到第一次播放才打开 miniaudio 引擎,
  不再常驻 WASAPI 播放设备和音频线程;开启时行为不变。
- engine: append_unique_words 由逐条线性比对改为哈希集合去重,顺序与结果不变。
2026-10-19 00:55:46 +08:00
Lu Fan(SonnyCalcr) 58a27f3edb fix(engine,server): 候选末页满页且无法再展开时置灰下一页箭头 (#1070)
下一页箭头原先在当前页满时一律可点,为首字母候选分批展开留口子;
但只有单个首字母且首字母候选恰好截在 24 条时才能展开,其余情况
末页满页的右箭头点了没反应也不会变灰。

引擎新增只判断不查库的 can_expand_initial_candidates,与实际展开
共用前置条件;Server 发布候选页快照时,满页只有在还能展开时才算
有下一页。

Refs #1069
2026-10-18 23:11:22 +08:00
Jia Suiandfanlusky d270395816 fix(engine): 简拼尾组合路径放开静态邻键纠错并拒生成式远键 (#1009)
* fix(engine): 简拼尾组合路径放开静态邻键纠错并拒生成式远键

整串 k-best 解释不了时回退到「head 纠错 + 简拼尾组合」,该路径原本把邻键位整段屏蔽,导致邻键手误的 head 纠不回来(shabgz -> shang'z)。

改为三条规则:head 用完整掩码(静态邻键进来);head 最优切分带 has_generated 时拒(k-best 静态优先,等价无静态读法,挡 huanzg -> huang'g);存在自洽明文读法时跳过组合(挡最短尾拆 sh/zh 后静态邻键改写正确输入,xish -> xia'h 类 39 起,及 huanzh -> huang'h)。

* fix(engine): 纠错用例取词库路径改用 path_to_utf8,非 UTF-8 ACP 下不再抛异常

新增的 head+简拼尾用例用 (directory / msime.db).string() 取路径。MSVC 的 path::string() 走本机 ACP,而测试临时目录名含中文(metasequoia-session-词库-…),在 ACP 表示不了它的机器(CI runner 的英文版 ACP 1252)上会抛 ERROR_NO_UNICODE_TRANSLATION,也就是 CI 日志里的 No mapping for the Unicode character exists in the target multi-byte code page.,input_session 因此失败。文件其余取路径处一律走 metasequoia::path_to_utf8,改齐。

本地 ACP 936 能表示该中文,故本地未复现;本地沙箱另限制子进程建临时目录,端到端只能由 CI 验证。

* fix(engine): 简拼尾组合的 head 也过简拼形状门,挡住 xibd -> xin'd

规则 3 只认「完整音节 head + 简拼尾」,漏掉「完整音节 + 两个简拼声母」:
xibd(xi + b + d)在 0xe/0xf 下最短尾 d 留下 head xib,静态邻键 b->n 把它
读成 xin,组合出 xin'd 顶掉明文;tamd、zhemd 同理。这正是旧邻键掩码要挡的
那类噪声。

改为让 head 过整串同一道 looks_like_jianpin_spelling 门:整串不纠的
"shag"(sha + g),后面多接一个简拼尾也不该纠。shabgz、hauzh 等正例不受影响。
顺带合并两遍 tail 扫描,去掉恒等于 autocorrect_types 的 head_types。

xibd/tamd/zhemd 补进 test_input_session 回归表(改动前 xibd 实测为 xin'd),
test_pinyin 用 xibd 替换被删的 shng 负例。

---------

Co-authored-by: fanlusky <1730976608@qq.com>
2026-10-17 00:05:17 +08:00
HowGeGeandfanlusky 1671c3a467 fix(engine): recognize complete umlaut aliases in quanpin (#994)
* fix(engine): recognize complete umlaut aliases in quanpin

* refactor(engine): 移除九键输入

九键会话只有引擎公共 Session 门面在用,Server 与 TSF 都没有接入。删掉
NineKeySession、Session 的九键入口与 SessionSnapshot::nine_key_spellings,
以及对应测试和注释。

* fix(engine,server): ü 别名共用一条归一规则,入库与纠错不再产出查不到的键

- 新增 quanpin::is_jqxy_v_alias / canonical_umlaut_spelling,替换 canonical_syllable
  与 to_google_spelling 里各自手抄的别名表,normalize_umlaut_aliases 也改用它。
- 纠错表生成器与运行时生成式空间不再从 jqxy+v 别名派生纠错键(nvan -> jvan 之类),
  手误只纠到 u 的标准写法;重新生成 autocorrect_table.h。
- 个人词库新写入的拼音词、设置页导入的全拼按词库写法入库(xvan -> xuan、lue -> lve),
  否则查询归一后永远查不到;被替换的旧词保留原键,旧行仍可删除。
- 双拼音节集统一排除 jqxy+v 别名,round-trip 检查不再特判。

---------

Co-authored-by: fanlusky <1730976608@qq.com>
2026-10-16 20:48:33 +08:00
Jia Suiandfanlusky 60c2b26698 fix(engine,server): 全拼纠错收窄「多字」纠错并让开关真正关闭纠错 (#889)
* fix(engine,server): 全拼多字纠错收窄为仅 QWERTY 邻键插入

多字(插入)纠错只保留「误插 QWERTY 邻键」:生成器 insertion_variants 不再生成重复相邻键(双击),生成式空间不再生成任意字母插入(远键插入)。autocorrect_table.h 随生成器重新生成。

* fix(engine): 全拼纠错开关关闭时真正不再改写切分

纠错开关关掉后真正不纠错:把手误拼写别名层从常开改为跟掩码走(ü 归一 jv->ju / nue->nve 仍常开);两个早于掩码可知的切分入口 QuanpinScheme::build_request 与 QuanpinDictionary::get_or_compute_segments 显式带上掩码,切分缓存键随之隔离。

* fix(engine,server): 纠错开关关闭时辅助码、写回与排序上下文也按字面切分

关掉全拼纠错后,只有方案的 build_request 和查询自身切分不再跑手误别名层,
其余几条路径仍用默认(带别名)的切分,和查询落在不同的键上:

- 辅助码路径自己切底串作为显式切分交给词典,haoyonga 仍被切成 hao'yong'ga;
- 调频、造词、删词、建 SQL、校验去掉分隔符后重切,开关关掉时选中 hao'yong'a
  的词会去更新 hao'yong'ga,调频静默失败;
- 排序 / 固定位置的上下文 key(引擎与 server 两处)仍按别名切,laing 与 liang
  的固定位置互相串。

改动:
- cut_pinyin_by_mode 的 include_correction_aliases 改为必填,每个调用点显式决定;
  双拼词典、设置页词库校验和离线工具传 true,保持原行为。
- 新增 quanpin::correction_aliases_enabled,方案、词典、辅助码与排序 key 共用
  (只看换位、邻键两位),修正只带删除 / 插入位的掩码在方案与词典两层判断不一致。
- 词典按最近一次 query 的掩码记录别名开关,写回路径统一走 cut_for_write。
- 会话改为在换方案、改开关时 bind_quanpin_scheme 一次,不再每次 refresh 下发。
- 方案只存 bool;jv -> ju 放回纠错表,去掉合并表。
- 在 CI 实际执行的 test_input_session 中补回归用例(写回、辅助码、固定位置、
  方案掩码),每条都确认回退修复后会失败。

---------

Co-authored-by: fanlusky <1730976608@qq.com>
2026-10-12 20:41:23 +08:00
Jia Suiandfanlusky fc90cb1f2c fix(engine): 纯简拼读法否决权,纠错不再劫持 nbn 等简拼候选 (#881)
* fix(engine): 纯简拼读法否决权,纠错不再劫持 nbn 等简拼候选

纯简拼形输入(切分全是单字母声母)若词库 jp 列有该输入读法,纠错不得接管:
nbn 是「能不能」、nqn 是「女青年」、bqng 是「并且能够」,纠错却把它们改写成
字母更近的错串候选(难/帮),顶掉真实命中的简拼词。

输入形状分不干净——sahng 也全是合法声母却必须保留乱序纠错;唯一分得开的是
词库有没有这个简拼读法,故判定落在同时持有 DB 与纠错掩码的查询层。

- 新增 quanpin::has_pure_jianpin_reading(jp 列 LIMIT 1 查询)
- QuanpinDictionary::effective_autocorrect_types 统一 query_exact 与
  insert_word_to_series_cache 的有效掩码,避免 series 缓存键分歧
- 非纯简拼(shng=[sh,n,g]、shbng、sahng/shangg/sjhang)纠错不变

* fix(engine): 纯简拼整词改为排在纠错前面,不再整个关掉纠错

原方案在词库有纯简拼读法时把纠错掩码置 0,漏了元音的真手误(bng 想打 bang)
也随之纠不了,而且只有词库层知道纠错被关,预编辑与选词消耗仍按原掩码判断。

改为不动掩码:纠错照常进行,纠错分支里字面结果中的纯简拼整词
(literal_jianpin_pinyin_match:两个及以上单字母声母盖住整串,逐个是正读首字母,
a/e/o 不算)和混合简拼一样放进领衔组。纠错读法排在后面,仍可选、仍带纠错标记。
判定直接看已查回的字面结果,不新增 SQL;掩码不变,缓存键也不变,
因此删掉 has_pure_jianpin_reading 与 effective_autocorrect_types。

测试补上预编辑(n'b'n)、选前缀单字只消耗自己的声母、bng 仍保留「帮」。

---------

Co-authored-by: fanlusky <1730976608@qq.com>
2026-10-12 17:23:30 +08:00
Jia Sui d495ea2ad0 fix(engine,server,installer): octagram 加成收敛为单一注入,校正量纲与参数,修复〔万象〕同音乱码 2026-10-12 13:12:21 +08:00
fanlusky b434240489 feat(engine): 模糊读音按词频与原读音混排,整句词格收模糊读音
开模糊音时模糊读音的候选不再整组垫在末尾:同组内按「权重 × 0.2」与敲出读音的词库行
归并,min 出「民 明 名 命 敏…」而不是先翻完 min 的生僻字。词格的跨度查询同时收模糊变体,
每个模糊音节扣 1.0(log10),lin'yi'bian 解出「另一边」;整句 canonical 记真实读音,
造词不写错音。切分与纠错的裁决打分不带模糊读音,避免字面读法借模糊分压过纠错。
双拼同步。eval_quanpin_sentence 新增 fuzzy=<规则位> 参数。
2026-10-12 03:44:25 +08:00
fanlusky cf07b77df6 feat(engine): 等音节数字面切分按整句打分裁决,预编辑跟随首选
最少音节切分平局时固定取较短首音节,切分又在查词之前定死,yibananzhao 只会切成
yi'ba'nan'zhao 出「一把难找」,「一般按照」「这个方案很好」这类 n/ng 边界歧义的整句没有机会。

- arbitrate_literal_segmentations:合法输入的等音节数字面切分各解一遍词格,路径分最高的领衔,
  同分仍取原切分;输方分差在 3.0 以内时它的整句占第 2 位,更远不出。任一切分在词库里有整词时
  不介入(单词级仍按词频),整句来源全关时与原来一致,用户调过频的读音不让出领衔
- 合法输入纠错的原读法改为领衔的字面切分:纠错胜出时第 2 位是得分最高的字面读法
- 首选是另一种等音节数字面切分时预编辑跟着它分(yi'ban'an'zhao、fang'an)
- 胜出切分的前缀词替换原切分前缀区间扫出的同名行,选「一般」按 yi'ban 消耗
- 词格跨度查询与最佳路径加缓存,随 reset_cache 清空;普通输入每键 p90 也随之下降
- 新增 eval_quanpin_sentence 与句子级评测集 tests/data/quanpin_sentence_cases.tsv
2026-10-12 03:05:44 +08:00
Lu Fan(SonnyCalcr) 649747f8cd feat(server): 神经网络整句效果优先模型改为按需下载 (#838)
效果优先模型(sentence-model-desktop.safetensors,约 25 MB)不再随安装包分发,
改为和八股文模型一样在设置页下载后才能使用。

- engine:SentenceAssociationOptions 新增 neural_desktop_model 路径,留空即模型缺席、
  静默不重排;词典不再从资源目录固定加载效果档。load_file 按 UTF-8 打开路径,
  数据目录在非 ASCII 用户目录下也能读到模型
- server:新增 settings/neural_model,下载到 <数据目录>\models\neural-desktop\,
  下载与导入都按 neural-model/lock.json 的字节数和 SHA256 校验后原子落位;
  会话层只在开关打开时解析模型路径
- ui-html:设置页增加下载 / 导入 / 删除 / 浏览器下载,模型未就绪时开关置灰
- installer:不再打包效果档模型;旧版装进数据目录根下的那份在覆盖安装与卸载时清掉

原有配置项 sentence_neural_desktop 保持不变,用户的开关状态升级后保留。
2026-10-11 18:08:33 +08:00
chr331 36ab259c43 fix(engine): 保留原样混拼候选优先级 2026-10-11 13:56:14 +08:00
Ouchihao 29c8917e8c fix(engine): preserve leading-initial mixed jianpin 2026-10-10 14:46:13 +08:00
fanlusky e72820ef46 perf(engine): 全拼前缀不再解注定被丢弃的 Google 整句
query_series 对每个前缀走 query_single_path,词库查不到时会解一条 Google
兜底整句,随后又因只对完整输入出整句而被删掉,还打乱解码器的增量状态。前缀
改走只查词库的 query_prefix_path:非空结果照原样写入 cache_,查不到的记进
单独的 prefix_miss_cache_,不往 cache_ 写空结果,这串之后作为完整输入时仍
补得上兜底整句。

逐键对照新旧引擎 3960 个快照(三档整句设置、退格、缓存冷热、先长串后前缀)
的全部候选词、来源与读音,完全一致。
2026-10-05 13:02:46 +08:00
Lu Fan(SonnyCalcr) 8a524aa498 feat(engine): 全拼纠错扩展到多字/漏字,贵档读法按整句分领衔 (#623)
* feat(engine): 合法输入纠错扩展到多字、漏字,纠错读音按原始区间消耗字母

jiuzheeyang 切成 jiu'zhe'e'yang 全合法,多打的 e 原本纠不回「就这样」;多字纠错的
读音又与原始字母不等长,buuhui 选「不会」后会剩下一个 i。

- legal_input_correction_cuts(原 legal_input_transposition_cuts)认换位、多字、漏字
  三类静态表读法,不认邻键与生成式空间;末尾音节按漏字读出的(nia -> nian)视为
  还没打完,跳过;k-best 先取 9 条再过滤,长句里纠正不被别处的读法挤掉
- 改变字母数的纠正代价 3.0(换位 2.0);字面切分本身是真词时不认改变字母数的读法,
  少一个音节的句子在语言模型里天然分高(邪恶 -> 些、答案 -> 但)
- 新增 correction_cut_for_reading / corrected_reading_raw_length:选词按纠错切分
  记录的原始区间消耗字母,预编辑也按它分隔(jiu'zhee'yang)
- AutocorrectCut 恢复 has_generated,供合法输入路径排除生成式读法

* fix(engine): 被裁掉的合法输入纠错读法不再误标原读法的候选

合法输入上的纠错读法登记进了全局纠错标记表,即使裁决时被丢掉也照样参与按读音字母
的标记匹配:xiee 按多一个 e 读成 xie 被丢掉后,邪恶后面的前缀单字 写/些/血 读音
也是 xie,全被标成纠错。

改为不登记,只在 arbitrate_legal_corrections 里给真正用上的那条纠错读法的整读音
候选打标,标记随结果进缓存。

* feat(engine): 结构性贵档纠错读法可按整句分领衔

shiideya 的换位读法 shi + ide->die(权重 10,是爹呀)比多字读法 shii->shi(权重 12,
是的呀)便宜一档。#592 只在同档内做上下文消解,贵档读法一律排在最后,「是的呀」
连首页都进不了。

- 上下文消解把前 2 条贵档读法也解码,不要求词库有整词;总分 = 路径分 - 手误代价,
  代价按纠错表权重换算(权重 x 0.2:换位 2.0、漏字 2.2、多字 2.4)
- 只收由结构性手误(换位、漏字、多字)构成的贵档读法:邻键只比漏字贵 0.4 分,
  常用词的语言模型优势会把 zhowei 的「周围」挤成「作为」
- AutocorrectCutSegment 记录每段的纠正权重,用来区分纠正类型
- 预编辑跟随领衔读法分隔也覆盖切不成合法音节的输入(shii'de'ya)
- eval_quanpin_autocorrect 新增 --word-lattice,量产品默认开词格时的排序

评测(--word-lattice,300 样本,seed 42)各档 R@1/R@3 不低于改动前:mixed R@1
79.7/76.7/78.3/70.3 -> 82.3/81.7/81.3/78.0,insertion 83.3/74.7/72.7/52.0 ->
91.3/89.3/90.0/83.7,deletion 持平或上升,ambiguous 持平。

* fix(engine): 纠错上下文消解在任一整句来源开启时生效

上下文消解只认 word_lattice 开关。只开万象重排、不显示 Trigram 整句时(词格在内部
照样解码),整段被跳过,shiideya 退回静态排序,「是爹呀」又领衔。

- 新增 SentenceAssociationOptions::any_sentence_source,门控改看它;全关时仍是纯
  静态排序,#592 的「关联关闭 = 静态路径」不变
- 贵档胜者没有整词也没有整句行时(重排模型未载入),由它的最长前缀词领衔,不让
  主切的行顶回首位
2026-10-05 01:41:37 +08:00
Lu Fan(SonnyCalcr) adde5c188c feat(engine): 合法输入的换位手误按整句打分纠错 (#622)
ziazheliya、jioa、liazheli、nia 能整串切成合法音节(zi'a'zhe'li'ya、ji'o'a),
纠错入口按「切不成合法音节」设闸,原本完全不进纠错。

- legal_input_transposition_cuts 列出合法输入上的换位读法:纠正段须盖住字面
  切分里被挤出的非首位零声母音节(排除 dao'lu 读成「大楼」这类跨边界重切),
  另有罕见音节小表(lia->lai、dia->dai),只用于多音节输入
- arbitrate_legal_corrections 按噪声信道整句打分:词格最佳路径分 + 每处换位
  -2.0,不看整句候选显示开关;纠错领衔时原读法固定第 2 位,原读法领衔时纠错
  在 3.0 分差内占第 2 位,否则不出
- 字面整词保住首位:候选里有读音字母与输入相同的词库词时,改写字母的读法
  (换位纠错,以及已有的辅音复制别名 yongan -> 勇敢)最多排第 2
- 纠错领衔时预编辑按领衔读法分隔(zia'zhe'li'ya)
2026-10-04 23:54:18 +08:00
fanlusky a219e499f6 fix(engine): 全拼纠错上下文接管让位于用户在主切读音上的选择
加载 sc.lm 时词格路径分里的词库权重只剩 dictionary_tiebreak(≤0.07)和
reading_prior,调频改的权重翻不过 1.0 的接管边际:banzhng 的用户每次都选
班长,办证系照样每次领衔,调频在这组词上被架空。

接管前加一道判据:主切键在用户日志里有 upsert,且主切整键首位权重不低于
胜出切分首位(即用户已把主切读音选到前面),就不接管、走按权重排序的静态
合并。出货词频本身仍交给上下文裁决;日志只在边际达标后查一次。

测试用手写 ARPA 作 sc.lm,让 LM 稳定偏向办证,覆盖无用户记录照常接管、
用户选过班长不再接管、用户记录未越过胜者照常接管三种情形。
2026-10-04 14:35:50 +08:00
Jia Sui bbebd34d22 docs(engine): 补上下文重排切分与用例打分的前提说明
R2 的分层判据是 canonical_pinyin == winner_key。原注释只说明了「整句与词格行
同键」,没解释为什么整键档**必然**是 winner_key。而 query_single_cut_keyed 在精确
键查空时会退到前缀区间扫描,那类行的 canonical 是别的读音(gun'qi 捞出 gun'qiu),
放宽判据就会静默把同档整词沉到末尾——正是本任务在修的那类回归。补上这条不变量:
decode_pool 由 query_exact_segmentations_keyed_flat 只收精确键构造,词库里没有整词的
切分进不了池,这条路在胜出切分上不可达。

测试侧补上「fixture 无 sc.lm ⇒ 启发式打分 ⇒ 用例确定」这个前提(同组 ranking fixture
已有,新用例依赖同一前提却没写)。
2026-10-04 03:45:53 +08:00
Jia Sui e1331ff1dc fix(engine): 恢复上下文重排的「整键在前、前缀在后」分层
胜出切分接管后用 query_series(raw_input, winner_key, winner_segments) 取它的结果,
但该函数的 count 从整键递减到 1,返回的不只是整键行,还有首音节的全部单字;而
rest(班长、搬账这些同键位整词)在这之后才append。

结果是办证/辩证/整句/班/办/半/般…/班长——胜出切分的单字插到同键位整词之前,把它们
整组挤出首页,PR 描述里「同键位纠错读法整组下移」并不成立。

按 canonical_pinyin == winner_key 把前缀行摘出来,恢复 merge_alternative_segmentations
的既有分层(merged_full 在前、前缀作为 remaining 追加):整键块、rest、前缀块。分类
键选 canonical_pinyin 而非 pinyin——后者承载用户键入码(可能是简拼);整句与词格行
同样落在整键块内,因为 append_ime_fallback 用完整 segmentation、词格 select_distinct
用 path->key 填 canonical_pinyin。

补判别性用例:原 fixture 缺 tbl_1_b,前缀这条路根本没被走到。已实测——撤掉本修复
时该用例失败(Same-tier whole words must not be pushed behind the winner cut's
single-character prefixes.),装回后通过。
2026-10-04 03:45:53 +08:00
Jia Sui 044352d49a fix(engine): 上下文重排的预过滤基准改用正读整键首位权重
1/100 预过滤的基准取 result.front(),注释声称与merge_alternative_segmentations
的 primary_top_weight 同源,实际不是:merge 取的是 primary_full(query_single_path
的整键结果),无整键行时为 0;而这里的 result 是 query_series 的完整输出。

正读在词库里没有整词时,result.front() 是一个前缀单字——真实词库里单字是 1e6+
的语料计数——于是 5000*100 < 1e6,正常备选在解码之前就被筛掉。而这恰恰是重排价值
最高的场景:正读最弱、词格只能用单字拼出路径时,正是上下文裁决该出手的时候。

改为复用 merge 的同一调用(query_single_path + 同一实参),走缓存不额外查库,
两处门槛从此不会各自漂移。

该缺陷无法用行为测试判别:正读无整词时 merge 的merged_full 只剩备选整键行,
备选本就居首,重排发生与否结果相同;构造分歧需要「词格胜者≠ 权重排序首位」,
而启发式 ln(w)+3.0×音节数 下同音节数时权重恒占优。故作为一致性修复保留。
2026-10-04 03:45:53 +08:00
Jia Sui fc3564e4d9 feat(engine): 生成式纠错空间兜静态表长尾
静态纠错表四类形状(相邻换位/漏字/QWERTY 邻键替换/限重复邻键插入)均为
穷举式生成,表外单编辑手误(非相邻键替换、任意字母插入)结构性无人管——
这是覆盖反馈的机制根源。新增生成式索引:对全部合法音节枚举表外形状
(48,216 对,首次纠错查询时一次性构建),并入纠错索引,k-best/贵档垫底/
上下文消解全链路复用;换位/漏字静态已穷尽,不枚举。

表内零回归为结构性保证:含生成边的切分在 k-best 排序中整体垫底
(has_generated 先序键),最优切分为纯静态时全部生成切分被丢弃——丢弃
规则在 autocorrect_cut_kbest 单点实现,主解析与 head+简拼尾组合路径同享;
生成读法只在静态空间完全无法解释输入时出场。混表加权仲裁依赖训练权重,
留给方向阶段 3。

评测:eval 新增 outside 注错档(表外形状,与静态表构造性不相交),覆盖
增益量化 R@1 47.7/44.3/36.3/22.7(四率;静态-only 恒为 0);表内四档
R@1/R@3 与基线逐位一致;种子聚合不回归。fixture 四用例含 head 组合路径
判别(shatngzh)。README 补 outside 档与资源目录说明。
2026-10-04 01:27:15 +08:00
Jia Sui 49be145bdf feat(engine): 全拼纠错同档歧义按词格上下文消解
同档纠错读法(zhng→{zhang,zheng} 这类 k-best 等权目标)此前只按静态词频
消解、不读上下文,是纠错误报的主要来源。现在整句词格开启时直接解出主切与
同档备选的最佳路径分,边际达标(打分器自身对数单位 1.0)时胜出切分接管
领衔候选,静态词频降级为解码前置过滤;词格不可用或边际不足时与现状逐位
一致(关联关闭、单音节、纠错开关关三条降级路径)。

七项护栏全部保留:贵档绝对垫底、精确键领先位、每来源至多一行、合并池去重、
纠错标记边界、输入门未动,1/100 门槛升格为解码前置过滤。单音节纠错无词格
可搭,维持静态路径(已知限制,fixture 测试钉住)。

评测:eval 新增 --seeds-file 固定精度套件(15 条,豁免逐键一致契约),
四档注错模型 R@1/R@3 与重采基线逐位一致,种子聚合 R@1 73.3%、R@3 80.0%;
机制生效由 test_input_session 隔离 fixture 端到端断言(lattice-on 序列
[办证,辩证,班长,搬账])。README 补资源目录要求与种子表维护纪律。
2026-10-04 01:27:15 +08:00
Jia Sui cc738f19ca feat(engine): 词格接入万象搭配加成与 n-best 重排
SentenceAssociationOptions 新增四个字段(模型路径、加性权重、重排开关、重排权重),
随 QueryRequest 下发到全拼与双拼词典:

- 加性(接入点 A):collocation_scorer 在 Viterbi 逐边叠加字级搭配分,与三元模型
  同域竞争,离线评测 top1 89.4% -> 95.6%(w=0.25)
- 重排(接入点 B):make_octagram_reranker 在解出的 n-best 上按整句搭配分重排,
  以独立来源 CandidateSource::Collocation 出行(徽标〔万象〕),落库/学习判定与
  神经整句同类
- 模型路径由宿主按模型包解析后传入,UTF-8 经 path_from_utf8 显式转换;模型缺席
  时词格静默降级,与其余整句来源同范式

同时修正 test_pinyin 里一处 9 月底起与实现矛盾的断言:重排器同意词格排序时,
行保持词格来源(Generated)而非记到重排名下——以 word_lattice.h 的单重排器契约
与 merge 实现为准。原断言所在的 imetest 不进 CI,因此一直没被发现。
2026-10-02 20:15:39 +08:00
fanlusky e365a8d1b5 perf(engine): 大模型改为只读映射并按需载入,缩短每键查询路径
- sc.lm 改用 kenlm 的 LAZY 载入:Windows 上默认的 POPULATE_OR_READ 会退成 READ,
  把 33 MB 整份拷进私有堆且永不释放;现在是 MapViewOfFile 只读映射,可被系统回收
- dict_japanese.dat 在 Windows 上同样改为只读映射,与 POSIX 分支一致
- 神经整句模型改为开关首次生效时才载入(LazySentenceModel),默认关闭的桌面模型
  不再占用约 24 MB
- 新增 reset_sentence_cache():重排结果与上文只影响词格整句,只清 series 一层,
  逐前缀查库与切分缓存保持热;set_rescoring_context 也改用它
- CircularBuffer 新增 find():命中时返回指针而不复制整份候选列表,并改为真正的 LRU;
  全拼、双拼每键的命中路径改用它
- Google 解码器去掉每次调用前的 im_reset_search,沿用上游的公共前缀增量搜索
- 测试:出货 sc.lm 能映射载入并正常打分;增量搜索与从头搜结果逐步一致
2026-09-30 19:58:18 +08:00
fanlusky 738435b002 fix(engine): 全拼长句不再为更短的前缀补 Google 整句
前缀查不到词时 query_single_path 会补一条 Google 整句,长句候选后面于是缀上一串越来越短的〔Unigram〕子串。整句联想只对完整输入出一句,前缀只取词库里的词,查不到就交给更短的前缀。
2026-09-29 02:22:57 +08:00
fanlusky 1e372c9c48 fix(engine): 读词库的连接等写锁而不是直接失败
msime.db 的三条读连接(全拼词典、双拼词典、quanpin_query 的 SqliteDb)都没设
busy timeout。设置页或学习写入提交的那几毫秒里,落在这段时间的查询直接拿到
SQLITE_BUSY,这一页候选就是空的。统一设 250 ms(与 english.db 读连接一致):
写锁只占一次提交,这点等待足够;大批量导入时最多卡一下,而不是出空页。

这也是把学习写入挪出按键线程的前提:之后读写会真正并发。
2026-09-27 16:09:28 +08:00
fanlusky fc6f6469f7 perf(engine): 上屏不再清空候选缓存,按键不再重复重建混合候选
- set_rescoring_context:每次选词都会换上文并清空 cache_、series_cache_ 和
  segmentation_cache_,下一个词的前几键因此都是冷查询。上文只经神经重排
  进入候选,没有生效的神经模型时与缓存无关,此时不清;之后打开神经重排
  会经 set_sentence_association 清缓存。全拼、双拼同改。
- set_quanpin_autocorrect_types:Server 每键重新应用配置,值没变也会调用
  update_mixed_candidates 复制整份候选列表。值不变时直接返回;构造时已把
  初值同步给引擎。
2026-09-27 15:03:17 +08:00
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
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
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
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 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
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
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 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 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
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 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