Skip to content

feat: 新增 Fcitx5/libime 拼音词库支持(二进制 libimebin + libimetxt 修复) - #426

Merged
studyzy merged 4 commits into
studyzy:masterfrom
Nahuimi:feat/libime
Oct 7, 2026
Merged

studyzy merged 4 commits into
studyzy:masterfrom
Nahuimi:feat/libime

Conversation

@Nahuimi

@Nahuimi Nahuimi commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

一、摘要

本 PR 让 imewlconverter 原生支持 Fcitx5 / libime 拼音引擎的词库,不再需要 Linux 或 libime_pinyindict 工具链:

  1. 新增格式 libimebin(导入 + 导出):读写 libime 的二进制拼音词库(*.dict,即 Fcitx5 的 sc.dict / 用户词库格式)。输出的文件可被 Fcitx5 与 libime_pinyindict 直接加载,已做字节级对齐验证。
  2. 修复 libimetxt(文本拼音词库)的四个缺陷:其中「汉字 / 拼音两列读反」会让该格式自身往返必然错位;另修复 BOM 污染首条词面、缺失词面转义、未按 libime 规则限制列数。
  3. 修复拼音编码生成对特殊字符的处理(影响所有拼音输出格式,不限于 libime):BMP 之外汉字(CJK 扩展 B 及以后,UTF-16 代理对)、私用区(PUA)生僻字会被当成「标点」清空拼音并与后续字错位;含 · 等无声母符号的词条拼音整体前移。

二、新增 libimebin:二进制格式要点

2.1 文件容器

offset 0:  be32 magic   = 0x000FC613      (libime pinyinBinaryFormatMagic)
offset 4:  be32 version
             = 0x2 → 其后是 zstd 帧,解压得到 DATrie<float> 序列化数据(当前版本)
             = 0x1 → 其后直接是 DATrie<float> 序列化数据(旧版,未压缩)

要点:

  • magic 与 version 都是大端 32 位(libime 用 marshall(),其实现按大端写)。
  • zstd 压缩参数:libime 用 ZSTD_initCStream(level 0)(等价于 libzstd 默认级别)并开启帧校验和(ZSTD_c_checksumFlag = 1)。本实现用 ZstdSharp.Port 的流式 API 并同样置位 checksum 标志。
  • 导入时必须校验 zstd 校验和:libime 读取后在流尾 peek() 触发 footer 解析来验证 CRC,校验失败会抛 Failed to load dict data。本实现的 DecompressionStream(checkEndOfStream: true) 对应这一行为。
  • 一个 .dict 文件 = 一棵 trie = 一个词库段(libime 的 SystemDict / UserDict 是不同文件,不在单个文件内分段)。
  • 默认文件名 sc(与 libime 系统词库 sc.dict 一致),扩展名 .dict,IsBinary = true。

2.2 DATrie<float> 序列化布局

zstd 解压后的字节流(即 libime DATrie::save() 的输出):

顺序 内容 类型/端序
1 tailLength be32
2 size(节点数,ninfo/array 长度) be32
3 tail 尾链字节 tailLength × u8(其中 float 值小端,见下)
4 array:size × { base, check } 两个 be32
5 bheadF、bheadC、bheadO 各 be32
6 ninfo:size × { sibling, child } 各 u8
7 block:(size >> 8) × { prev, next, num, reject, trial, ehead } be32, be32, be16, be16, be32, be32

关键易错点(本实现踩过)——端序不统一:

  • 上述所有结构体字段都走 libime 的 marshall(),即大端。

  • 但尾链中保存的词值 float32 是小端。libime utils_p.h 明确注释:

    These are only used by DATrie, and it's le32 due to historic reason. Marshall* still use be.

    对应 storeDWord / loadDWord(htole32 / le32toh)。写成大端会导致 libime 读出的词频是垃圾值。

  • 其他隐含约定:

    • block 的数量恒为 size >> 8(ninfo 与 array 同长,每 256 个节点一个 block)。
    • 导出前必须执行 shrink_tail()(尾链压缩/重排),libime save() 同样调用;否则尾链偏移语义不一致。
    • 哨兵值用 float quiet NaN 的位模式:CEDAR_NO_VALUE = 0x7FC00001、CEDAR_NO_PATH = 0x7FC00002。

2.3 键(key)的编码

每个词条的 trie 键 = 拼音字节串 + '!' + 汉字 UTF-8:

<音节1:2B><音节2:2B>...<音节n:2B> '!'  <汉字 UTF-8 字节>
  • 分隔符 !(0x21)即 libime 的 pinyinHanziSep。

  • 每个音节固定 2 字节 =(声母, 韵母),取值直接复用 libime PinyinEncoder 的枚举数值(枚举值就是 ASCII 码,因此可读性很好):

    枚举 起始值 说明
    PinyinInitial B = 65 B P M F D T N L G K H J Q X ZH CH SH R Z C S Y W Zero,Zero = 88 表示零声母
    PinyinFinal A = 65 A AI AN ANG AO E EI EN ENG ER O ONG OU I IA IE IAO IU IAN IN IANG ING IONG U UA UO UAI UI UAN UN UANG V VE UE NG Zero Letter_A..Letter_Z

    例:ni → (N=71, I=78);hao → (H=75, AO=69);xiao → (X=78, IAO=81);zhong → (ZH=79, ONG=76);e → (Zero=88, E=70);lv → (L=72, V=96);lve → (L=72, VE=97)。

  • 音节表覆盖 450 条:422 个规范音节(取自 libime pinyindata) + 26 个单字母音节(A–Z → (Zero, Letter_x),用于 X光 这类词)+ 2 个 VE_UE 模糊别名(lue/nue → lve/nve),与 encodeFullPinyinWithFlags(pinyin, VE_UE) 的行为一致。
    即:文本格式里的 lue/nue 与二进制里是同一个键,两种格式可以互转而不丢信息。

  • 大写字母音节(X'guang)是合法输入,已包含在表中。

  • 无法编码的音节(生僻表外拼写)→ 该词条计入 ErrorCount 并跳过;若全部词条都无法导出,则抛异常,避免写出空文件或损坏文件。

  • 键本身是字节串,不涉及端序问题。

2.4 值(value)

float32,语义是 libime 的「权重 / 代价」(对应文本格式的第 3 列)。本 PR 与 libimetxt 保持一致,直接用 WordEntry.Rank。导入时 float → int(clamp;非有限值 → 0)。

2.5 新增文件

文件 行数 职责
src/ImeWlConverter.Formats/LibIMEPinyin/CedarFloatTrie.cs 952 libime DATrie<float>(cedar 双数组 trie)的 C# 移植:Set(构建)、Save/Load(序列化)、Enumerate(遍历导出键值对)。含 shrink_tail、block/ninfo 维护与尾链分配
src/ImeWlConverter.Formats/LibIMEPinyin/LibimePinyinTable.cs 177 450 条音节 ↔ (声母, 韵母) 编码表;TryEncode / DecodeSyllable / DecodeFullPinyin
src/ImeWlConverter.Formats/LibIMEPinyin/LibimeDictFormat.cs 83 文件容器:magic/version 头、zstd(含校验和)、v1 兼容读取
src/ImeWlConverter.Formats/LibIMEPinyin/LibIMEPinyinExporter.cs 89 导出器,[FormatPlugin("libimebin", …, 501, IsBinary = true, FileExtension = ".dict", DefaultFileName = "sc")]
src/ImeWlConverter.Formats/LibIMEPinyin/LibIMEPinyinImporter.cs 78 导入器,遍历 trie → 解析键 → 还原词面/拼音/词频

配套工程改动:

  • ImeWlConverter.Formats.csproj:新增 NuGet 依赖 ZstdSharp.Port 0.8.8(仅 Formats 项目;纯托管实现,无原生依赖,跨平台)。
  • ImeWlConverter.Formats.csproj:新增 InternalsVisibleTo("ImeWlConverterCoreTest"),供测试直接验证内部 trie 与音节表。

2.6 为什么要自己移植 trie

.NET 没有可用的 cedar/CEDAR 双数组 trie 实现,而 zstd 有成熟的 ZstdSharp.Port。因此 trie 部分按 libime libime/core/datrie.cpp 移植了构建 + 序列化 + 遍历所需子集(未移植删除等无关操作),目标是字节级兼容而非仅语义兼容。


三、修复 libimetxt(文本拼音词库)

libime 文本格式(官方文档 pinyindictionary-text-format.md):

<hanzi> <full-pinyin> [weight]
  • 一行一条;' 分隔音节;允许大写(X'guang);权重可省略(省略时按 0 载入,但回写时一定补上)。
  • 解析后列数不是 2 或 3 的行整行忽略(含空行、# 开头行)。
  • 汉字字段含空白、引号、反斜杠、转义换行时用 " 包裹并按 fcitx 规则转义。

3.1 修复 1(严重):导入器把「汉字 / 拼音」两列读反

原实现:

var parts = line.Split(' ');
if (parts.Length < 3) yield break;
var pinyinParts = parts[0].Split('\'');   // ← 把第 0 列当拼音
var word = parts[1];                      // ← 把第 1 列当汉字

而 libime 的实际顺序是「汉字 拼音 [权重]」。因为导出器本来是正确的,所以该格式的自身往返(导出→导入)必然错位(词面与拼音互换),且 2 列行(合法,权重可省略)会被直接丢弃。

修复后:

var parts = LibimeTextEscaping.Tokenize(line);
if (parts.Count is not (2 or 3)) yield break;   // 与 libime loadTextImpl 一致
var word = parts[0];
var pinyinParts = parts[1].Split('\'');
var rank = parts.Count >= 3 && int.TryParse(parts[2], out var r) ? r : 0;

3.2 修复 2:导出带 BOM,污染首条词面

FileEncoding 原为 Encoding.UTF8(带 BOM),libime 会原样把 BOM 读成第一个词面的首字符(U+FEFF)。改为 new UTF8Encoding(false),与 saveText 一致。

3.3 修复 3:补齐 fcitx 词面转义(新增 LibimeTextEscaping.cs,158 行)

原先按空格 Split,词面含空格的词条会被拆成多列;含引号/反斜杠也无法还原。libime 通过 fcitx 的 escapeForValue / consumeMaybeEscapedValue 处理,本 PR 做了等价的 C# 移植:

  • 需要加引号的字符集:\f \r \t \v 空格 " \ \n

  • 转义表(escapeMap / unescapeMap):

    原字符 转义写法 说明
    \ \\
    " \"
    \n (LF) \n
    \f \f
    \r \r
    \t \t
    \v \v
  • 空白集合 FCITX_WHITESPACE = "\f\n\r\t\v "(注意不包含普通空格之外的其它 Unicode 空白)。

  • 解析:跳过前导空白 → 若以 " 开头则读到下一个未转义的 ";否则读到下一个空白。非法转义序列按原字符处理(丢弃反斜杠),与 fcitx 一致。

  • 引号未闭合时,fcitx 回退为「按未加引号处理」,本实现同样回退。

于是 a b 这类词面导出为 "a b" ni'hao 5,导入可完整还原。

3.4 修复 4:列数校验与 libime 对齐

只接受 2 列或 3 列,其余整行忽略(原为「≥3 列」才处理)。


四、修复:拼音编码生成对特殊字符的处理

这一节影响所有拼音输出格式(搜狗、微软、QQ、RIME…),不限于 libime。上游 a3cbd97 只修了 CJK 扩展 A 区(U+3400–U+4DBF),本 PR 继续修复其后的情形。

4.1 代理对导致的「拼音被清空 + 整体串位」

CodeGenerationPostProcessor 原先按 UTF-16 码元遍历 word 并与 Code.Segments 按下标配对。BMP 之外的汉字(CJK 扩展 B 及以后,如 𫚉 U+2B689、𩽾 U+29F7E、𬘭)在 UTF-16 中是代理对,于是:

  1. 代理对的两个码元各自被判为「非中文 → 标点/符号」而清空编码;
  2. 下标错位使后续字的拼音落到错误的字上并被清掉。

实际表现(用户反馈语料,34 条输入只导出 21 条):

# libime 输出(期望)
口𠱞子早鸟 kou'ran'zi'zao'niao 1
豹江𫚉娘   bao'jiang'hong'niang 1
𩽾𩾌       an'kang 1

# 修复前
口𠱞子早鸟 kou'''zao'niao 1
豹江𫚉娘   bao'jiang''       1
𩽾𩾌       (两段拼音全空 → 被当空编码整个丢弃)

修复:

  • CodeGenerationPostProcessor 改为按 Unicode 码点遍历(一个码点对应一个 segment);
  • IsCJK 补齐 BMP 之外的范围:CJK 扩展 B–F 与兼容补充(U+20000–U+2FA1F)、扩展 G–H(U+30000–U+323AF)、兼容表意文字(U+F900–U+FAFF)、带圈表意补充(U+1F200–U+1F2FF);
  • 落单代理项(畸形输入)走安全分支,不再调用会抛异常的 char.ConvertToUtf32;
  • PinyinCodeGenerator 同样按码点产出 segment,维持「一个码点一个 segment」的不变式,避免生成编码时再次错位。

4.2 私用区(PUA)生僻字被当标点清空

IME 词库会用私用区承载生僻字(本次语料里那些看起来像异体字的字形),这些码点此前不在 IsCJK 内:

# 期望 / 修复前
<U+E000>汗 po'han 1   →   'han 1
镆<U+E000> mo'ye 1    →   mo' 1

修复:IsCJK 增补私用区(U+E000–U+F8FF),并顺带补齐 CJK 部首补充+康熙部首(U+2E80–U+2FDF)、CJK 笔画(U+31C0–U+31EF)、带圈 CJK 与兼容区(U+3200–U+33FF)。

4.3 含「·」等无声母符号的词条拼音整体前移

libime 文本只写有音节的拼音——「芭芭拉·巴布科克」8 个字只有 7 个音节,· 没有音节。原先「字下标 == 段下标」严格 1:1 配对时,· 占用了第 4 个 segment,其后所有音节前移一位:

# 期望
芭芭拉·巴布科克 ba'ba'la'ba'bu'ke'ke 1
# 修复前
芭芭拉·巴布科克 ba'ba'la''bu'ke'ke 1

修复(在按码点对齐的基础上):

  • 先统计「非标点码点数」,仅当它恰好等于 segment 数时,判定为 libime 省略了标点拼音,标点不占用 segment;
  • 否则退回逐码点 1:1 配对——以兼容生成路径的约定(生成器为每个码点都产出 segment,标点对应空段)。

这样文本导入(省略标点)与编码生成(标点空段)两种来源都能正确对齐。


五、兼容性与行为变化

  • libimetxt 导入结果会变化:字段顺序修正后,此前「词面↔拼音互换」的错误结果被改正;若有人依赖旧行为,会看到修正后的数据。
  • libimetxt 导出不再写 BOM:首条词面不再带 U+FEFF。
  • 导入条目数可能变多:含非 BMP / PUA 字符的词条,拼音不再被清空,因而不再被「空编码」过滤器丢弃(本次语料 21 → 34 条)。
  • 新增依赖:ZstdSharp.Port 0.8.8(仅 ImeWlConverter.Formats,纯托管)。
  • 无 API 破坏性变更:格式通过既有 [FormatPlugin] + 源生成器自动注册,CLI/GUI 无需改动即出现在格式列表中。
  • README 格式速查表补充 libimetxt(此前遗漏)与 libimebin;--list-formats 集成断言同步补充 libimebin。

六、测试

新增/增补的测试:

测试文件 用例数 覆盖内容
LibIMEPinyinTest.cs(新增) 8 magic/version 与 zstd 帧头、v2 往返、v1 旧格式导入、音节编码与 libime 枚举一致(含 lue/nue)、多音节解码、非法 magic 报错、无拼音时抛异常
LibIMETextTest.cs(新增) 14 字段顺序、2 列(无权重)、无 BOM、fcitx 转义表、引号/转义/未闭合引号、非法转义、列数校验、34 行 libime 真实语料整表黄金用例、· 省略标点对齐、PUA 字符保持
CodeGenerationPostProcessorTest.cs(增补) 22 非 BMP 保留、PUA/部首/笔画保留、真标点仍清除、非 BMP 后接数字/英文/标点不串位、落单代理项不抛异常、libime 省略标点对齐、生成路径标点空段
PinyinCodeGenerationIntegrationTest.cs(增补) 18 非 BMP 一字一段、连续非 BMP 不错位

结果:

  • 全量单测 409 通过 / 3 跳过 / 0 失败。
  • 解决方案构建 0 警告 / 0 错误。
  • 反向验证:临时撤掉修复后,新增用例分别有 7 / 8 条失败,确认这些用例是有效守卫而非恒真断言。

交叉验证方法(重点)

  1. 二进制格式:与 libime 自身的 C++ 实现双向对拍——
    • 用 libime 的 datrie.cpp 与 PinyinEncoder 编译了一个 ref.exe build|dump 参考程序;
    • C# 导出 → C++ 读取、C++ 构建 → C# 读取,键与 float 值逐字节一致;
    • 端序细节(结构体大端 / 尾链 float 小端)正是通过这一步发现的。
  2. 文本转义:把 fcitx5 src/lib/fcitx-utils/stringutils.cpp 的 escapeForValue / consumeMaybeEscapedValue(含 escapeMap/unescapeMap)原样编译成参考程序,与 C# 实现做十六进制逐行 diff:
    • 边界语料 33 行 → 139 行输出;
    • 随机语料 3000 行(含空白、制表符、引号、反斜杠、中文)→ 20038 行输出;
    • 全部一致。
  3. 真实语料:用户提供的 libime 导出的 34 行(含非 BMP 汉字)与 21 行(PUA + ·)在 libimetxt → libimetxt 下逐行一致(唯一差异是 libime saveText 会补行尾换行,属正确行为)。

七、如何手工验证

# 1) 文本 → 二进制
imewlconverter -i libimetxt -o libimebin -O sc.dict data/dict_sc.txt

# 2) 用 libime 自带工具回读校验(Linux / 已装 libime 的环境)
libime_pinyindict -d sc.dict sc.roundtrip.txt

# 3) 二进制 → 文本
imewlconverter -i libimebin -o libimetxt -O out.txt sc.dict

# 4) 格式注册是否齐全
imewlconverter --list-formats | grep libime
#   libimetxt  LibIME 拼音词库(文本格式)
#   libimebin  LibIME 拼音词库(二进制)

八、已知限制 / 后续可做

  1. 只导出 v0x2(zstd):v0x1(未压缩)仅支持读取,用于兼容旧词库。
  2. 模糊音只实现 VE_UE:与 encodeFullPinyinWithFlags(..., VE_UE) 对齐(lue/nue → lve/nve)。libime 的其它 PinyinFuzzyFlag(zh/z、ch/c、sh/s、l/n、f/h、r/l、an/ang、en/eng、in/ing 等)未实现。
  3. 词频用 int:libime 的权重是 float,文本格式第 3 列也是浮点数。目前文本导入用 int.TryParse(如 0.5 会得 0),二进制导入 clamp 到 int,导出写 int。如需完整保真,建议后续把 WordEntry.Rank 扩展为 float(改动面较大,本 PR 未做)。
  4. 同键覆盖:同拼音同词面的重复词条,后写覆盖先写(与 libime DATrie::set 语义一致),不做合并。
  5. libimebin 的 Enumerate 为全量遍历,超大词库(百万级)导入时内存占用与原文本格式相当,未做流式优化。

九、变更文件总览

文件 变化
src/ImeWlConverter.Formats/LibIMEPinyin/CedarFloatTrie.cs 新增(952)
src/ImeWlConverter.Formats/LibIMEPinyin/LibimePinyinTable.cs 新增(177)
src/ImeWlConverter.Formats/LibIMEPinyin/LibimeDictFormat.cs 新增(83)
src/ImeWlConverter.Formats/LibIMEPinyin/LibIMEPinyinExporter.cs 新增(89)
src/ImeWlConverter.Formats/LibIMEPinyin/LibIMEPinyinImporter.cs 新增(78)
src/ImeWlConverter.Formats/LibIMEText/LibimeTextEscaping.cs 新增(158)
src/ImeWlConverter.Formats/LibIMEText/LibIMETextImporter.cs 修复字段顺序 / 列数 / 转义(+12/−6)
src/ImeWlConverter.Formats/LibIMEText/LibIMETextExporter.cs 去 BOM + 词面转义(+4/−2)
src/ImeWlConverter.Core/CodeGeneration/CodeGenerationPostProcessor.cs 按码点对齐 + CJK/PUA 范围 + 省略标点对齐
src/ImeWlConverter.Core/CodeGeneration/Generators/PinyinCodeGenerator.cs 按码点产段
src/ImeWlConverter.Formats/ImeWlConverter.Formats.csproj +ZstdSharp.Port、+InternalsVisibleTo
src/ImeWlConverterCoreTest/LibIMEPinyinTest.cs 新增测试(182)
src/ImeWlConverterCoreTest/LibIMETextTest.cs 新增测试(302)
src/ImeWlConverterCoreTest/GeneraterTest/CodeGenerationPostProcessorTest.cs 增补测试(+138)
src/ImeWlConverterCoreTest/GeneraterTest/PinyinCodeGenerationIntegrationTest.cs 增补测试(+28)
README.md 格式速查表补 libimetxt、libimebin
CHANGELOG.md 新增/修复条目
tests/integration/test-cases/4-regression/verify-list-formats.sh 断言补 libimebin

Nahuimi and others added 4 commits October 2, 2026 16:50
新增格式 libimebin(.dict,导入/导出)
- CedarFloatTrie: 移植 libime DATrie<float>(Cedar 双数组 Trie),修正迭代器
  Begin/Next 别名 bug,与 libime C++ 读取器逐字节比对一致
- LibimeDictFormat: 头部 magic 0x000fc613 + version;version 2 为 zstd 压缩
  (ZstdSharp.Port) + 校验和,version 1 为原始字节
- LibimePinyinTable: 448 音节 + lue/nue(lve/nve) 编码表,整串拼音编解码
- 产物可被 Fcitx5 与 libime_pinyindict 直接加载,双向验证字节级一致

修复 libimetxt(文本拼音词库)
- 导入器把两列读反: 原按「拼音 汉字」解析,而 libime 文本实为「汉字 拼音 [词频]」,
  导致词面与拼音互换;导出器本就正确,故此前自身往返必然错位
- 词频可省略(2 列),并严格按 loadTextImpl 只接受 2/3 列
- 导出改用无 BOM UTF-8,与 saveText 一致(原 BOM 会混入首条词面首字符)
- 补齐 fcitx 词面转义: 含空白/引号/反斜杠的词面按 escapeForValue 加引号转义,
  导入按 consumeMaybeEscapedValue 还原(此前这类词面被拆成多列或丢字符)

验证
- 与 fcitx5 stringutils.cpp 逐字移植编译的 C++ 参考实现对拍: 边界语料 139 行 +
  随机语料 20038 行,输出完全一致(上游 unescapeMap 带符号索引越界已规避)
- 全量单测 388 通过 / 3 跳过 / 0 失败;解决方案 0 警告 0 错误
- --list-formats 集成断言补充 libimebin,README 格式速查表补 libimetxt
问题
libimetxt 导出含 CJK 扩展 B 及以后汉字(U+20000+,如 𫚉𩽾𬘭,UTF-16 中为
代理对)的词条时,这些字的拼音被清成 '' 且后续字拼音整体前移,例如:
  口𠱞子早鸟 kou'ran'zi'zao'niao -> kou'''zao'niao
  豹江𫚉娘 bao'jiang'hong'niang -> bao'jiang''
  𩽾𩾌 an'kang -> 整条被按空编码丢弃
34 条输入因此只导出 21 条,与 libime 自身输出不一致。

根因
CodeGenerationPostProcessor 按 UTF-16 码元遍历 word 并与 Code.Segments 按
下标配对。代理对的两个码元各自被 IsPunctuationOrSymbol 判为「标点/符号」,
于是 (1) 该码元对应的 segment 被清空,(2) 下标错位使后续字的拼音落到错误的
字上并被清掉。a3cbd97 只把 CJK 扩展 A 区(U+3400-U+4DBF)补进了 IsCJK,
非 BMP 汉字仍落在判定之外。

修复
- CodeGenerationPostProcessor: 改为按 Unicode 码点遍历(一码点一段),
  IsCJK 覆盖扩展 B-F/兼容补充(U+20000-U+2FA1F)、扩展 G-H(U+30000-U+323AF)
  与兼容表意文字(U+F900-U+FAFF);落单代理项走安全分支,不走会抛异常的
  char.ConvertToUtf32
- PinyinCodeGenerator: 同样按码点产出 segment,保持「一码点一段」不变式,
  避免生成编码路径把代理对拆成两段而错位

验证
- 用户提供的 34 条 libime 语料 libimetxt->libimetxt: 34/34 导出,
  逐行与 libime 输出一致(仅末行换行差异,为 saveText 正常行为)
- 新增回归测试: 后处理器 8 条(非 BMP 保留、真标点仍清、非 BMP 后接
  数字/英文/标点不串位、落单代理项不抛异常)、拼音生成集成 2 条、
  libimetxt 整表黄金用例 1 条
- 反向验证: 撤掉修复后新增用例 7 条失败,确认为有效守卫
- 全量单测 398 通过 / 3 跳过 / 0 失败;解决方案 0 警告 0 错误
问题
libimetxt 导出两类词条仍不正确:
1. 私用区(PUA, U+E000-U+F8FF)承载的生僻字拼音被清成 '',
   如「<PUA>汗 po'han」->「<PUA>汗 'han」、「镆<PUA> mo'ye」->「mo'」
2. 姓名中含无声母符号「·」的词条拼音整体前移,
   如「芭芭拉·巴布科克 ba'ba'la'ba'bu'ke'ke」->「ba'ba'la''bu'ke'ke」

根因
1. IME 词库用私用区表示生僻字,这些码点不在 IsCJK 任何范围内,
   被 IsPunctuationOrSymbol 判为标点而清空
2. libime 文本只写有音节的拼音(「芭芭拉·巴布科克」8 个字仅 7 个音节),
   编码后处理此前按「字下标 == 段下标」1:1 配对,「·」占用了第 4 个
   segment,导致其后所有音节前移一位

修复
- IsCJK 补充私用区(U+E000-U+F8FF)、CJK 部首补充+康熙部首(U+2E80-U+2FDF)、
  CJK 笔画(U+31C0-U+31EF)、带圈 CJK+兼容区(U+3200-U+33FF)、
  带圈表意补充(U+1F200-U+1F2FF)
- 编码后处理对齐改为按「非标点码点数」判定: 仅当非标点码点数恰好等于
  segment 数时,视为 libime 省略标点拼音,标点不占用 segment;
  否则退回逐码点 1:1 配对(兼容生成路径为每个码点都产出 segment 的约定)

验证
- 用户 21 行语料(PUA 生僻字 + · 姓名)libimetxt->libimetxt: 21/21 逐行一致
- 新增回归测试: 后处理器 7 条(PUA/部首/笔画保留、libime 省略标点对齐、
  生成路径标点空段仍正确处理)、libimetxt 端到端 4 条
- 反向验证: 撤掉修复后新增用例 8 条失败,确认为有效守卫
- 全量单测 409 通过 / 3 跳过 / 0 失败;解决方案 0 警告 0 错误
# Conflicts:
#	tests/integration/test-cases/4-regression/verify-list-formats.sh
@studyzy
studyzy merged commit fabdab3 into studyzy:master Oct 7, 2026
@Nahuimi
Nahuimi deleted the feat/libime branch October 8, 2026 01:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants