183 Commits
Author SHA1 Message Date
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
fanlusky 352c7e2b12 perf(engine): 混排未开启时不再每键复制候选列表
update_mixed_candidates 无条件把候选按值传进 CandidateQueries::mixed,Server
自己混排、candidates() 也只在混排开启时才读这份副本。未开启时直接清空返回;
混排开关、方案与固定位置每次变化都会重新调用本函数,不会读到旧列表。
2026-10-05 13:02:46 +08:00
Lu Fan(SonnyCalcr) 1da1c1bcd6 fix(engine): 辅助码串上的 Ctrl+方向与 Ctrl+Backspace 按单元编辑 (#627)
双拼串里带句中辅助码(反引号段 / 大写段)或开着直接辅助码时,segment_raw_boundaries
直接返回空,Server 只能退回逐字符移动和删除。

现在按请求带着的辅助码段布局切单元:每个音节和挂在它后面的辅码段各算一个单元,
Ctrl+Backspace 可以只删辅码,Ctrl+方向也能停在音节与辅码之间。布局对不上原串时仍
不给边界、退回逐字符。直接辅助码没解析出辅码时请求原样放行,沿用普通双拼切分。

光标前缀重算与按段编辑共用同一套边界,带辅助码的串也随之按光标前缀出候选。
2026-10-05 04:19:23 +08:00
Lu Fan(SonnyCalcr) b391fd616b feat: T 模式支持输入指定的日期和时间 (#626)
Shift+T 后面除了 rq / sj / xq 唤醒词,还可以直接写数字:

- YYYYMMDD、YYYY/M/D、M/D(年份取今年)出那一天的日期格式,星期按那一天算
- YYYY/M 出年月格式(新增 date:ym_* 五种)
- h:mm、h:mm:ss 出时间格式,写了秒才给带秒的格式
- YYYYMMDDhh[:mm[:ss]] 出日期加时间的格式

指定的日期时间沿用唤醒词的格式 ID,学到的顺序、置顶、固定位置跟着走。

输入形状放在 engine/contracts/date_time_input.h,TSF、Server、引擎共用:
数字、/ 和 : 只在还能接成某种形状时才进输入串,接不上的数字仍是选词键,
/ 和 : 仍是标点,Shift+数字始终选词。小键盘数字同样可以输入。

Refs #531
2026-10-05 03:42:12 +08:00
Lu Fan(SonnyCalcr) ae353d142c feat(server): 语言栏右键菜单新增输入方案二级菜单 (#625)
在托盘语言栏菜单第二项加入「输入方案」,悬停展开全拼/双拼/五笔二级菜单,
当前方案打勾,点击即切换(与设置页走同一条 SetConfiguredInputScheme 路径)。

- D2D:TrayMenuPresenter 用 Popup 实现子菜单,展开时宿主窗口向有空间的一侧加宽,收起时还原
- WebView2:页面通过 inputSchemeSubmenu 请求宿主加宽窗口,再由 PlaceInputSchemeSubmenu 摆放;
  changeInputScheme 切换方案
- 契约:messages.json 新增 changeInputScheme / inputSchemeSubmenu,并重新生成 ui-html/webview2/shared
2026-10-05 02:44:59 +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
Lu Fan(SonnyCalcr) 5208753ce8 feat: 句中辅助码新增大写字母触发方式 (#620)
* feat(engine): add uppercase trigger for shuangpin mid-sentence helpcode

With the uppercase trigger on, an uppercase letter right after a complete
two-key syllable opens a mid-sentence helpcode block by itself, as if the
backtick had been typed before it: ulpbXihfa is ulpb`xihfa, and a following
uppercase letter is still the second code (ulpbXYih is ulpb`xYih). The raw
input keeps the typed letters; the block rules live in
contracts/mid_sentence_helpcode.h so TSF, Server and engine parse alike,
including the semicolon final, which no longer counts uppercase blocks.

The scheme now hands the parsed layout (source index and decorations) to the
request, so selection advance and preedit no longer re-parse the raw input.
Adds the MidSentenceHelpcodeUppercaseChanged worker opcode (31).

* feat(windows): count uppercase helpcode blocks before backtick and semicolon

With MidSentenceHelpcodeUppercaseChanged on, TSF uses the shared block rules
when deciding whether '`' opens a block and whether ';' is the ing final, in
both the synchronous and the deferred key classification.

* feat(server): wire the mid-sentence helpcode uppercase trigger

New helpcode.shuangpin_mid_sentence_helpcode_uppercase (default off) in both
config templates, both settings hosts and the settings page as a third
trigger checkbox. It takes effect only with mid-sentence helpcode on and
direct helpcode off; the Server applies it to the semicolon final rule and
broadcasts MidSentenceHelpcodeUppercaseChanged to TSF.
2026-10-04 21:00:41 +08:00
Lu Fan(SonnyCalcr) 22fb1e2569 Merge pull request #619 from fanlusky/feat/shuangpin-direct-helpcode
feat: 双拼直接辅助码(万象式,无需引导键)
2026-10-04 19:32:10 +08:00
luojiyin 7e2d9990a5 perf(engine): use AVX2 for vocabulary projection
Pack active hidden rows before the tied vocabulary projection.

Reuse the existing int8 AVX2 kernel when the CPU supports it.
Keep the original scalar path for other CPUs and float weights.
The projection and complete scoring both run faster with locked models.
2026-10-04 16:16:30 +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 7e1ae10da2 docs(engine): 更正生成式空间可达性论断,删除对用户不可见的错误结论 2026-10-04 05:21:45 +08:00
Jia Sui 3a0d95e143 test(engine,server): 对齐生成式纠错空间的类型位可达性断言 2026-10-04 04:25:25 +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 7d83aed411 docs(engine): 说明 wake 谓词依赖的跨表不变量
wake_ 的谓词按模型判空槽、不比键,成立的前提是 pending_ 与 active_keys_ 不可能
同时持有同一个模型:待办是在 worker 取走的那一刻才从表里 erase 的,在那之前同一
模型的键只挂在 active_keys_ 上。

这个不变量此前没有文字说明。谁哪天在 order_for 里改成「在算的也照样排一份」
(在 42aeda08 之前看着像无害的加固),谓词会把正在算的那批重排一遍——正是该字段
引入前要消灭的重复计算。纯注释,行为不变。
2026-10-04 03:45:53 +08:00
Jia Sui 6d8f7b6e2b refactor(engine): 移除 AutocorrectCut::has_generated 死字段
该字段在 autocorrect_cut_kbest 重建结果时赋值,作为公开结构体字段暴露在头
文件并配了契约注释,但 engine/、server/、windows/ 及全部测试都没有读取点。

静态优先的过滤实际发生在 autocorrect_cut_kbest 内部的 SearchHypothesis 上
(排序比较器 + 末尾按 front().has_generated 决定是否整批 erase),在 cut 重建
之前就完成了。头文件注释把契约归因到这个字段,读者会以为调用方依赖它。

删掉字段与赋值,契约说明改归真正的机制。SearchHypothesis::has_generated 是
活代码,保留。
2026-10-04 03:45:53 +08:00
Jia Sui 7a8b441509 fix(engine): RescoreWorker 不再丢弃更新的重排请求
active 分支命中时(同一批正在算)原先会顺手清掉该模型的 pending_ 待办,
理由是「排在它后面的旧待办没人要了」。这个前提不成立:

- 模型 M 正在算 K1,用户继续打字把 K2 排进 pending_
- K1 算完写 done_ 并回调重查;若重查赶在工作线程擦 active_keys_ 之前,
  且 done_ 刚被 kMaxResults 重置清空过,done_.find 未命中就落进 active 分支
- K2 被静默抹掉,而本次调用拿到 nullopt、也不会有回调

失败模式是那次重排永久缺失,不是延迟。active 分支只表达「这批正在算,
别重复排」,两件事拆开;pending_ 本就按模型只留一格、下次 order_for 直接覆盖。
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
luo jiyin 9a22905fb7 perf(engine): add blocked AVX2 int8 linear kernel (#608)
Reuse each widened weight chunk across up to four activation rows.

Use a scalar path when the CPU lacks AVX2 or FMA.
The blocked kernel reduces scoring time for both locked models.
Keep a focused test for row blocks, input tails, and optional bias.
2026-10-04 00:06:38 +08:00
fanlusky af34e23d07 feat(server): 语法模型支持浏览器下载与本地导入,新增网络代理设置
应用内下载 octagram 模型较慢时,设置页每个模型行提供「浏览器下载」直链,
下载后用「导入」选中 .gram 文件:后台复制进模型目录,与应用内下载走同一套
格式校验与原子落位,原文件不动。导入与下载共用单槽。

帮助页新增网络代理(跟随系统 / 直连 / 自定义 HTTP 代理),新增 [network]
配置段。云输入、模型下载、AI 联想、翻译、语音识别与润色、凭证测试统一经
NetworkProxy 选择代理,改动即时生效。WinHTTP 的「跟随系统」由 DEFAULT_PROXY
改为 AUTOMATIC_PROXY,真正读取用户的系统代理与 PAC。

契约新增 collocationModelImport 消息;旧设置宿主补上 openExternalUrl。
2026-10-03 22:49:07 +08:00
luojiyin 895d637300 perf(engine): reuse neural forward buffers
Reuse temporary vectors across Transformer blocks within one forward call.

The keyboard model uses 76 allocations per scoring call, down from 135.
The desktop model uses 76 allocations per scoring call, down from 157.
Scores match the original implementation for both models.
2026-10-03 21:35:12 +08:00
fanlusky 05d202ce5b feat(server): 日期时间候选支持调频、置顶、固定位置,并加「📅日期」格式入口
日期时间候选(Shift+T 模式与 rq / sj / xq 混输)按格式 ID 调频:日期文本每天都变,
所以给每种格式一个稳定 ID,学到的顺序记在用户库新表 date_time_format_order,固定位置
借用 fixed_candidate_positions。T 模式与混输共用一份顺序,混输取排第一的格式。右键
置顶、固定位置、取消固定对日期候选生效。

混输时在第三位放一个「📅日期」入口(时间为「🕒时间」),选中后不上屏,候选框整页换成
全部格式,与 Ctrl+Enter 译文页同一套子页机制。新增开关 utility.date_time_menu,默认
打开,出厂模板、工作配置、两个设置宿主与设置页同步更新。UILess 宿主不显示入口。
2026-10-03 20:44:22 +08:00
luo jiyin 136d440517 Merge branch 'develop' into perf/neural-skip-padding 2026-10-03 17:05:48 +08:00
luojiyin 1b5e7a71e4 refactor(engine): name compact logit slots
Use slot for compact logits and row for Transformer positions.
2026-10-03 16:40:00 +08:00
luojiyin 1acc3d6745 perf(engine): skip unused neural scoring positions
Skip vocabulary projection for positions without targets.

Remove the final unused Transformer position from each candidate row.

Add coverage for empty candidates in mixed batches. Refs #593.
2026-10-03 16:40:00 +08:00
fanlusky b8ceeef7f9 fix(engine): 重查时不再重复排正在打分的同一批候选
回调触发的重查会问到刚被后台取走、还没算完的那一批:结果表里没有,待办位也空着,
于是同一批又排了一份。用户连续打字时每个回调都可能白算一次;benchmark 里这份重复
任务会在 clear() 之后写回结果,让下一轮误报 neural_already_visible 或把等待计入
async_ms。

按模型记下正在算的键,同键请求直接返回并丢掉该模型更旧的待办。
2026-10-03 16:35:48 +08:00
luojiyin a7087a0cdf fix(engine): label benchmark session caches precisely 2026-10-03 10:23:53 +08:00
luojiyin 68b791c771 perf(neural): add async rerank end-to-end benchmark 2026-10-03 10:09:08 +08:00
fanlusky 233ac9ef33 feat(engine,server,windows,ui-html,installer): 句中辅助码触发键可多选,新增分号
设置页「句中辅助码触发键」可勾选反引号和分号,至少保留一个。

- 分号只在 AcceptsMarkerAt 成立(这一节是偶数键)时才当触发键;微软/搜狗/紫光双拼的
  ing 韵母只跟在奇数键之后,两者不抢同一个位置,判断时分号先让给 ing 韵母
- 无论按哪个键,TSF 按键缓冲、Server raw 与引擎输入串里都记成反引号,引擎解析不变
- 新配置项 helpcode.shuangpin_mid_sentence_helpcode_backtick / _semicolon 同步写入出厂
  模板、工作配置与两个设置宿主
- opcode 28 保持「反引号是否触发」、载荷单个 "0"/"1"(TSF 会丢弃更长的帧);分号走新增
  的 opcode 29,旧 DLL 忽略它
2026-10-02 23:55:46 +08:00
Jia Sui 72c1d7ddb0 fix(engine): darts-clone 登记为 vendored 副本,脱离格式门禁
0976c307 随octagram 读取端入库的 darts-clone 0.32h 不在 format.sh 的排除名单里,
而它整份文件都不符合仓里的 .clang-format,本分支一旦推上去 CI 的
Check formatting 就会报 1826 处违规。

按精确路径排除 darts.h,不按目录:同目录的 octagram_gram.{h,cpp} 是我们自己的
代码,必须继续受门禁管,目录级排除会连带把它们一起豁免掉。

与上游 v0.32h(87b71afd)比对后确认这份副本并非原样入库,而是带 6 处本地改动
——open() 删掉 256 单元头部校验与两段式读、save() 删掉 offset 参数、
commonLongestPrefixSearch 整体删除、DoubleArrayBuilder 三个常量改为
static const std::size_t、一处注释拼写。这些此前没有任何地方记录,是它掉出
名单却一直没被发现的原因;逐条写进 UPSTREAM.md,升级 darts-clone 时才能重放。
前两条与 mmap 装载自洽(octagram_gram 走 set_array,不经过 open/save)。

两份 AGENTS.md 里硬编码的「那三个上游副本」清单同步为四个。
2026-10-02 22:00:36 +08:00
Jia Sui 01c45c8d90 fix(server,ui-html,engine): octagram 激活中的模型包也可删除,引擎映射支持动态释放 2026-10-02 20:15:41 +08:00
Jia Sui 03c0a4246d feat(server,ui-html,engine,installer): 接入社区多 octagram 模型,分模型下载与单选激活 2026-10-02 20:15:40 +08:00
Jia Sui 8089841e78 fix(engine): octagram 头按外部头暴露,darts.h 收窄告警不再刷屏 2026-10-02 20:15:40 +08:00
Jia Sui e1a664a194 fix(server): 万象下载器改用格式锁,撤下无依据的评测宣称
锁定值与上游实际资产不符(字节数 409719852 vs 409412652,摘要 ae43724a… vs 873cbbb3…),下载在字节锁处失败。上游 LTS 会被原地重传且无不可变资产 URL,摘要硬锁意味着每次重训都要改代码发签名安装包,否则全用户下载失败,这个成本消不掉。

改为锁格式:删字节数常量,总长取响应 Content-Length 并在收完后比对一次(截断检测);新增 ValidateFormat 让引擎读取端自己开一次(魔数 + 双数组边界,不按文件自带长度做堆分配)。摘要不再参与放行,只判断 reviewed 并写入旁挂 .sha256(状态查询要频繁调用,不能每次重算 400MB 哈希)。真实上游字节按 open() 判据复核精确贴边通过:array_size=102353152,收尾正好在 409412652。

kReviewedSha256 保留旧值,摘要不符即 reviewed=false,设置页明示「上游已更新,未经评测」,NOTICE 记录实际摘要与字节数并附差异说明。契约无需重生成:sentence_collocation_model_status 是两个宿主各自拼的 ad-hoc JSON,不在 schema.json 内。

撤下无法验证的宣称:设置页删掉「评测 top1 +6 个百分点」(仓里没有评测工具、没有 docs/research,该数字无法复现),改为描述机制;清掉两处对从未进仓的文档与工具的引用(docs/research、tests/src/gram_reader、eval_lm_compare);修正 octagram_gram.h 里被本次改动失效的「摘要校验在下载侧做」。配置只改注释、未增删键,五件套映射不适用。
2026-10-02 20:15:39 +08:00
Jia Sui 2925fabf4a feat(contracts,ui-html): 设置页新增万象模型下载与开关
- 契约新增客户端消息 collocationModelDownload / collocationModelStatusRequest
  (bindings 经 generate.py + sync-contracts.py 再生)
- 设置页整句区新增:模型状态卡片(未下载/下载中 x%/就绪/失败原因)、下载按钮、
  「万象搭配加成整句」「万象搭配重排整句」两个开关
- 下载中页面每 2 秒发 collocationModelStatusRequest 拉新快照,进度与按钮态随
  configSnapshot 的 association.sentence_collocation_model_status 应用
2026-10-02 20:15:39 +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
Jia Sui 0976c3072c engine(ngram): 万象语法模型(octagram .gram)读取端转为产品件
从评测工具的移植版升级为引擎库成员:装载从整文件 ReadFile 换成 Win32 mmap(RSS
只随命中页增长),新增进程内按路径共享的 shared_gram_db(照
ngram::shared_language_model 范式,查询全只读、多会话并发安全)。eval_lm_compare
改用同一份实现,评测与产品不再有两套读取器;darts.h(BSD)随目录入库。
2026-10-02 20:15:39 +08:00
Jia Sui c87684272b engine(quanpin): 词格加可选的字级搭配打分接缝
WordLatticeOptions 新增 collocation_scorer/collocation_weight(默认关闭,产品路径
零行为变化):为 octagram(.gram)这类与词表零耦合的字级搭配语法模型预留加性
打分入口。假设额外携带路径尾部文本(至多 8 个码点),句尾词以 is_rear 标记,
供 .gram 的「词+\$」rear 项参与打分。
2026-10-02 20:15:39 +08:00
fanlusky 45c473158a feat(engine,server,windows,ui-html): 双拼预编辑可显示转换后的全拼
外观里行内预编辑与候选窗预编辑各加一个「双拼显示转换后的全拼」开关,默认关闭。
开启后在双拼方案下:行内原始按键显示不带分词符号的全拼,拼音分词显示带分词符号的
全拼;候选窗显示带分词符号的全拼。

- 引擎新增 get_shuangpin_preedit_forms,同时给出原串切分与全拼切分,不受
  shuangpin_preedit_mode 影响。
- 原始按键样式下 TSF 不等 Server 回包,开关开启且为双拼时 Server 下发的实际样式
  改为 pinyin,回包里再按用户样式去掉分词符号;换方案时重新下发。
- 转换后字母数与按键不再对应,Server 在预编辑末尾附光标映射表
  (engine/contracts/preedit_caret_map.h),DLL 用它定位光标和处理方向键,
  无表或长度不符时回落到原有字母映射。
2026-10-02 16:14:50 +08:00
fanlusky c280634c2a fix(engine,server): 词库切到 WAL,避免调频写入阻塞按键线程
选词后的调频写入在词库写线程上提交 msime_user.db / msime.db。两个库都是
默认的 rollback-journal 模式,提交时持独占锁并 fsync,按键线程此时读同一个
库(引擎查询、apply_fixed_positions)就得等完整个提交。磁盘忙或杀软扫描
-journal 时单键可达 0.7~1.1 s,后续按键排队;排队期间按空格/数字会超过 TSF
300 ms 的上屏回复等待,组合被当作传输失败丢掉,表现为输入消失。

- 用户日志库每次建连接时切到 WAL;msime.db 在每次创建输入会话时切到 WAL
  (安装升级会换上非 WAL 的新文件)。WAL 下读取看到最近一次提交的快照,
  不再等写锁。安装器已保留 msime_user.db-wal/-shm、替换词库前删除旧的
  msime.db-wal/-shm。
- ensure_schema 的 PRAGMA user_version=3 本身是写操作,版本已一致时跳过,
  建连接不再被其他写者阻塞。
- 新增回归测试:两个库都被写事务锁住时,读取须在 1 s 内返回提交前的内容。

本地用真实词库加锁测量:被锁时首键 769 ms -> 0.5 ms,
apply_fixed_positions 810 ms -> 0.4 ms。
2026-10-02 14:57:31 +08:00
fanlusky 31241d22ed fix(engine,server,windows): 句中辅助码支持光标在句中插入,上屏按不带约束的排序调频
光标用箭头或 Ctrl+箭头移回句中后敲反引号,TSF、Server、引擎三处都要求光标在串尾,
反引号被当成标点,光标前的预编辑连同「·」一起上屏。改为只看光标前的部分:光标前是
完整的两键音节就收,落在音节中间或已有段的前面不收。形状规则新增
FanyImeMidSentenceHelpcode::AcceptsMarkerAt,三处共用;引擎的光标插入路径同时接受
句中的第一码和大写第二码。

句中辅助码筛过的列表里,要的词往往已经排第一,选第一个不调频,调了也是在筛过的列表
里排位,下次不敲辅助码时排序不变。现在带句中辅助码上屏词库候选时,改用去掉约束后的
候选作排位参照,选的是第一个也照样调;空格、数字键、鼠标点选都走 ProcessSelectionKey,
引擎自己的上屏路径同样处理。
2026-10-02 12:43:16 +08:00
fanlusky cec3118324 feat(engine): 新增日期时间混输候选来源
CandidateSource 末尾追加 DateTime,标记全拼 / 双拼里由 rq / sj / xq 等唤醒词混进普通候选的
日期、时间、星期;Shift+T 模式里的同一批文本仍记 Generated。快捷短语槽位计数不把它算作普通候选。
2026-10-02 03:39:13 +08:00
fanlusky fae80660c1 feat(engine,server,windows,ui-html): 双拼句中辅助码
在完整音节后敲反引号,接着的字母筛这个音节上的字:第一码大小写均可,
紧跟的大写字母是第二码,之后可以接着打,一个输入里可以有多段。开关
helpcode.shuangpin_mid_sentence_helpcode 默认关闭。

约束在整句解码时生效,而不是解完再筛:
- Google 解码器在 add_char_qwerty 里送 extend_mtrx_nd 之前按约束过滤词条,
  锁住的字带着上下文重新整句解码;约束变化时整段重解,不污染增量搜索。
- kenlm 词格建图时按约束筛边,受约束的跨度不截断地查词库;神经重排只在
  词格 n-best 里挑,自动满足约束。
- 普通词条、云和 AI 候选按同一规则收尾校验;满足不了就不出整句。

反引号段在请求里换成手动分隔符,原串留给宿主回读与预编辑显示;选词推进
时剩余音节上的约束随原串保留。输入形状规则放在 contracts/mid_sentence_helpcode.h,
TSF、Server、引擎共用,新增 Worker opcode MidSentenceHelpcodeChanged = 28。
googlepinyinime-rev 已与上游解绑,AGENTS 与 UPSTREAM.md 同步说明。
2026-10-02 02:20:37 +08:00
fanlusky 8004bc9250 fix(engine): imetest 直接链接正式引擎库,修正与实现不符的重排来源用例
engine/tests 手抄了一份引擎源文件清单,2c3546f8 新增 local_database.cpp
时只登记到 engine/CMakeLists.txt,imetest 自此链接失败;清单还漏了
shuangpin_custom_profile.cpp,也没有正式构建给 googlepinyinime 加的
/FI pinyin_file_io.h 与 /utf-8。改为 add_subdirectory 引擎根目录、链接
MetasequoiaIme::Engine,引擎源只在一处登记;vcpkg 依赖对齐根清单,
删掉无人引用的 includes/main.h。

test_pinyin.cpp 的「重排器同意」用例期望来源记为 NeuralDesktop,但
e7f7addd 引入的实现(word_lattice.h 的契约)是把被认可的三元首选提前、
来源仍为 Generated。以实现为准改测试。imetest 现在完整通过。
2026-10-02 00:52:12 +08:00
fanlusky e502201fd9 fix(engine): 全拼别名改写后的 preedit 保留合法切分与手动分隔符
#565 让合法键入在被别名改写后退回无分隔的原始字母,多打一个字母就会丢掉
已显示的全部分隔符(hao'yong -> haoyonga)。现在按键入字母自身的合法读音
重新切分(hao'yong'a、dong'an)。BFS 无法解释时的退路改为原样显示键入,
不再吞掉用户手打的分隔符(haoyonga' 不再显示为 haoyonga)。

测试拆成两组:走简拼守卫的合法拼写,与走退路的输入。dongua、zhonguo 不经
过守卫,归入退路组,此前它们在修复前也能通过,不构成守卫分支的回归用例。
2026-10-01 22:57:51 +08:00
Lu Fan(SonnyCalcr) 9529ac2d10 Merge pull request #565 from metasequoiaime/fix/quanpin-alias-preedit
fix(engine): 全拼合法键入被别名改写后 preedit 不再照搬改写串
2026-10-01 22:48:46 +08:00
Jia Sui efdea2c14f fix(engine): 全拼合法键入被别名改写后 preedit 不再照搬改写串 2026-10-01 15:09:32 +08:00