Skip to content

Prefer 才 核 煙 in HKVariantsRev#1274

Open
frankslin wants to merge 3 commits into
BYVoid:masterfrom
frankslin:upstream-master2
Open

Prefer 才 核 煙 in HKVariantsRev#1274
frankslin wants to merge 3 commits into
BYVoid:masterfrom
frankslin:upstream-master2

Conversation

@frankslin

Copy link
Copy Markdown
Collaborator

Fixes #378

"hk2t": "這樣纔行",
"hk2t": "這樣才行",
"hk2s": "这样才行",
"tw2t": "這樣纔行",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danny0838 @skyuns 能否评估下 tw2t 是否也应做同样处理?

@danny0838

danny0838 commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

類似的問題在 #504 (comment) 就提過,除了反向轉換的優先權問題以外,還有單向轉換的問題,提出新架構時最好能一起處理。

比較接近原生架構的方式可能是該串說的,加入一個 HKVariantsRev.txt 作為比自動產生的 HKVariantsRev.ocd2 更優先的詞典。

至於單向轉換,除了該串說的加入 HKVariantsUni.txt 以外,也可以考慮 HKVariantsRev.txt 加入 X => X 的方式覆蓋。但這做法實際是變成 X => X Y Z ...,並不是很完美,在某些情況下可能有副作用。

再不然,改為手動維護 HKVariantsRev.txt 並配合程式自動檢測也可以。

如果一定要用 # @xxx 標註的寫法,目前的寫法也不建議,應該直接用 RichDict 讀取 entries 綁定的 comment block 內容做關聯(可以只讀末幾行到空行為止,即只讀以前的 attached block),swap 可以考慮改成 RichDict 專屬方法,或保留 Dict.swap 作為忽略標註的版本。

最好是先改善維護腳本的基礎架構(#1264),再引進新標記會比較好。

@frankslin

Copy link
Copy Markdown
Collaborator Author

Refactoring 是为需求服务的;refactoring 本身不是项目的需求。

如果未来出现更多需要指定反向优先级的条目,那么完全可以在后续重构中统一纳入新的框架;但当前只有 3 个案例时,为此引入新的词表结构,或者等待更大的重构完成后再解决,都属于过度设计。

从维护角度来看,我更关心的是:

  • 当前是否存在真实问题需要解决
  • 当前方案是否正确解决了这个问题
  • 为解决这个问题引入了多少额外复杂度

这个 PR 解决的是一个真实存在的问题,而新增的复杂度仅限于少量标记及对应的生成逻辑。

相比之下,无论是新增 HKVariantsUni、单独维护 HKVariantsRev,还是先进行更大规模的词典框架重构,都会引入额外的文件、规则和维护成本,而这些成本目前并没有相应的需求支撑。

另外,我认为「单向转换的优先级」与「自动生成反向词典时的优先级」本质上是同一类问题:都是为现有映射增加额外属性,而不是引入新的词表内容。因此,用标记的方式描述这些特殊情况,本身也更接近长期的重构方向。

如果将来这类案例继续增多,再统一抽象也不迟;在此之前,我倾向于优先解决实际问题,而不是提前为尚未出现的需求设计新的结构。

@danny0838

danny0838 commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

這問題有和 @BYVoid 討論過嗎?

請爬爬之前的討論串,至少當初增加 HKVariantsRev.txt 也是 @BYVoid 說的。

雖然樂見有人願意積極維護,但目前看起來,這些更動似乎是你個人的主張,並沒有受到社群的支持。

而你的更動是否引進更少複雜度也非常有待商榷。引進一套由程式解析的註解本身就是很大的架構更動,註解的格式、解析程式的寫法是否有足夠通用、可維護,都有很多討論空間,本身也需要相關文件規範註解的語法,現在只是你偷懶沒寫,並不是沒有需求。而且你的更動也可能和 #506 衝突,在 #506 未處理完成前也不建議急著引進。

反而改為手動維護 HKVariantsRev 在我看來才是整體更動最小的,畢竟 reverse/swap 的腳本已經寫好,程式自動生成 reverse 再手動改幾個例外,然後刪除原來的自動生成程式碼,視需要比照 phrase rev 擴充雙向檢測腳本,就完成了。

而作為一開始寫出 Dict/RichDict 架構的本人而言,不在支援註解解析的 RichDict 的基礎上分析註解,而是在 Dict 上寫,也是很 ugly 的程式寫法,這都會欠下大量技術債,日後維護可能又必須重新更動註解語法與程式架構,反而使 OpenCC 整體變得很不穩定。

@skyuns

skyuns commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

"這樣才行"這句本身就有歧義:

  1. "這樣纔行" : 如此 才可以(行,二聲),例如: 格式要這樣才行 (纔)
  2. "這樣才行" : 這種 才華與品行(行,四聲),例如: "這樣才行兼備之人"
    現代繁體用語來說,應該是第一種用法比較常用,第二種是偏向書面文雅用法。

參考: STPhrases 裡面,將第一種排在前面,"才行 纔行 才行"
s2tw,因為TWVariants裡面有"纔 才",不管中間層轉成繁體 "才" 或 "纔",最後都用"才"。
所以,在s2tw沒有問題。

再討論tw2t,參考 OpenCC 設計思想:

OpenCC 默認的簡體到繁體轉換模式為 s2t。此模式的目標不是轉換為任何一個特定國家或地區實際使用的繁體中文,而是轉換為一種內部使用的中間詞彙標準層(intermediate lexical canonical form),稱為 OpenCC 標準繁體。

採用中間標準層後,可將流程拆分為:
地區標準簡體 → OpenCC 標準繁體 → 地區標準繁體
s2tw ≈ s2t + t2tw

上面寫的是轉為"地區標準繁體"的流程,反過來說 tw2s 應該是 ≈ tw2t -> t2s
我不確定保留 tw2t 結果的用途為何, 以我自己來說, t 應該是程式內部使用的中間層,
只是轉換之中的過程,最終還是要繼續轉換成地區標準.
所以 tw2t (經過 TWVariants 逆轉) 之後,流程繼續走到 TSCharacters 裡面有 "纔 才"
那麼在中間層,無論是 "這樣纔行" 或是 "這樣才行",都不影響最終轉為簡體 "这样才行"

我自己處理的方式是,最終結果對了,中間層就沒有太深究,
因為要認真研究中間層的話,詞典要新增很多詞,再加上許多判斷邏輯,最終地區結果卻相同,要評估是否需要這麼做。

@frankslin

Copy link
Copy Markdown
Collaborator Author

@skyuns 确实如你所说,「hk2t」「tw2t」模式是顺带输出「OpenCC 标准繁体」一中间状态,大部分用户实际上不会用「t」这类模式。

从用户角度来看,与类似转换模式输入输出不完全对称,只要合理,充其量只是个美学层面的问题。
不过近期有用户反馈 hk2t/tw2t 模式中「核心」会转换成「覈心」,danny0838 早在 2020 年就指出「天才」变「天纔」的问题。这些直接影响输出质量的问题,优先级更高一些。多年下来,问题主要就集中在几个字上。

修复思路大体有两种:

  1. 在 {HK,TW}VariantsRevPhrases 中增加大量词组例外进行覆盖;
  2. 调整 {HK,TW}VariantsRev 中相关字映射。

之前在 #1247 其实已经按第一种方式解决,不过仅为了解决这个问题就给 HK/TW 各增加了一千行左右的新映射规则。
而调整 HKVariantsRev 的优先级,只要 3 个字的反向输出,上述规则中的 700 多行就可以精简掉。

从维护成本角度来看,好处是不言而喻的;代价是非词组的字转换行为可能改变。

TW 我也考虑采用类似的做法修改,现在主要是看除了 HK reverse 这三个字之外,TW 还有没有其他需要改的地方。

@skyuns

skyuns commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

前幾天,我看到TWVariantsRevPhrases,突然多了很多行,我就測試確認了一下這個檔案每一條內容,在程式不改的情況下,轉換回簡體都正確。
就像上面說的,我知道過程不一定是對的,例如: "參加",在原本的逆轉過程會變成"蔘加",但結果會變成"参加"就好了。
雖然TWVariantsRevPhrases 已經加入了"參加 參加",但我也沒有打算去使用這個檔案來讓轉換過程正確,因為增加的內容應該會讓轉換效率變慢,但不會讓結果更正確。

要是想讓過程變得更正確,除了花費時間不斷擴充TWVariantsRevPhrases之外,改寫TWVariantsRev 是一個方式。
目前在專案裡面,是沒有看到TWVariantsRev。從相關文件來看,是用程式從TWVariants來反轉自動產生的。
這個PR#1274,應該是讓反轉程式去偵測註解,來達到自動避免"才"轉成"纔"的目的。

+# @reverse-prefer: 才
+才	才
-纔	才
+纔	才 纔

如果不想要用註解且變動最少的情況,我有兩個想法,不知道是否可行?

第一個想法,尾巴加純數字標記:
在原本的"纔 才"之後,加" 0": 也就一個空白一個0。反轉程式增加偵測判斷,最後一個欄位如果是0,就不反轉這整行。
原本s2tw最終因為第一候選還是"才",所以沒變。而tw2s或tw2t因為有0,反轉程式不會產生包含"才 纔"的TWVariantsRev,轉換過程中的中間字 t 也就不會轉成"纔"。
假設有人考證後說"材"是標準繁體字,那麼"材"是要反轉的優先目標,則維護時可以加上"材 才 1",代表這是要讓反轉程式優先產生"才 材"。
不過,我不確定這裡的第二欄位,在其他相容的程式或輸入法中,有沒有作為其他候選字顯示的用途?

-纔	才
+纔	才 0
(+材	才 1)

參考說明註解:

# TWVariantsRev is auto-generated from this file.
# Suffix " 0": Do NOT reverse this entry into TWVariantsRev.
# Suffix " 1": Set as the top-priority target in TWVariantsRev.

第二個想法,自對應過濾:
目前PR有加的其中一行"才 才"。反轉程式可依此增加偵測判斷,當Key=Value (自對應),則"Value"就不反轉為Key,包含原本的"纔 才"。
為什麼這樣思考,因為既然這個檔案是"Variants",要讓轉換有效的前提就是不同字,如果文字相同,那就沒有轉換必要,反轉行不保留這行,結果也會相同。
只是,這個方式僅限目前的情況,也就是不想反轉的"才 核 煙"這三個字,正好是Key=Value。
目前可能受影響的逆轉換,如:"只 只 衹",修改之後產生的VariantsRev,會沒有這行,但中間的過程與結果應該都相同吧。

+才	才
纔	才

參考說明註解:

# TWVariantsRev is auto-generated from this file.
# Identity Mapping Rule:
# If "Key == Value" exists (e.g., "才\t才"), OMIT all entries pointing to this value (including "纔\t才") from TWVariantsRev to prevent unwanted reverse conversions.

以上兩個簡單的想法,是以"盡量簡單化修改"為目標,希望以不產生新檔案、不解析註解的方式,來達到轉換過程的中間字 t,能夠盡量還原它的字形。
這麼做,只需要修改逆轉的程式就好。原本的s2tw與tw2s讀取到的Variants與VariantsRev檔案架構都沒變。

以上兩個想法僅供參考,我本身不是專業寫程式的,只是希望能提供不同的程式開發思考路徑。
若有思慮不周或不妥的地方,還請大家多多包涵。

@danny0838

danny0838 commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

我覺得最簡單的就是 *VariantsRev 改回人工維護。

一般而言,地區異體字表數量遠小於詞彙表,如果 *PhrasesRev 都可以人工維護,*VariantsRev 沒道理不行。(按:TWPhrases 有 687 行,HkVariants 才 69 行)

至於是否要用程式自動檢測雙向轉換,可以再議,我認為是可以,但應該考慮增加像 # @no-reverse-conv 之類的標記讓檢查程式略過特定行或特定字詞的反向檢查,否則不好解決特定字詞只想單向轉換的情況。

特殊標記只用於略過檢查也比用於生成更輕量,且相容性高。例如 Python, npm 都有 linter 支援檢測程式碼語法,但可藉由特殊註解略過一部分的檢測。

@frankslin

frankslin commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator Author

可以看到,对于 OpenCC 这样的项目,新增一个新词表绝不仅仅是简单地多加一两个 txt 文件,而是贯穿整个构建链的。不算下游的程序,至少会涉及:

  • CMake
  • Bazel
  • node-gyp
  • 词典生成脚本
  • CI 测试
  • 文档
  • Windows / Mac / Linux 三大平台,binary / node / python 各自的产物发布

对于「才 核 煙」这三个反向转换要改这件事本身大概没有太多争议,讨论的焦点其实是「谁愿意承担这个改动的多少代价」问题。

方案 词表改动 构建系统改动 代码改动 新语法 下游兼容性 长期维护
RevPhrases override(现已实施) +2000 行左右(已加入) 无影响 高,需长期维护大量覆盖规则,未来修改词表会面临一致性的问题
手工维护 Rev 新增 Rev 词表 CMake + Bazel + node-gyp + 生成流程 中影响 中,需处理 Rev 与源词表一致性问题
Rev overlay 新增 Overlay 词表,并与自动生成的 Rev 协作 CMake + Bazel + node-gyp + 生成流程 复杂 大影响 中,需维护 overlay 与生成逻辑的关系
Uni + Rev 新增 Uni 与 Rev 词表,并处理协作关系 CMake + Bazel + node-gyp + 生成流程 复杂 大影响 高,词表职责和优先级关系更复杂,并非一目了然
reverse-prefer 原字表增加少量标记,未来根据需要补充 RevPhrases 局部修改现有反转脚本 少量注释标记 小影响 低,信息集中;下游不跟随修改时仅损失反向优先级信息
RichDict 元数据 原字表增加较复杂的元数据标记,未来根据需要补充 RevPhrases 现有脚本大幅重构 较多 中影响 中,能力更强但解析逻辑和语法规范成本更高
数字标记 原字/词表增加新字段,未来根据需要补充 RevPhrases 修改反转脚本 不兼容字段 大影响 中,下游需跟随解析,否则可能产生错误候选
自映射过滤(以往讨论中否决过) 利用现有字/词条,未来根据需要补充 RevPhrases 修改反转脚本 小影响 低,但只能处理自映射场景,不能表达一般优先级

在项目长期实践中其实「一致性」问题已经显现,踩过很多次坑,比如「抬/擡」映射删掉后多年仍然转换异常,原因就是词表没有跟随字表一起改。所以我说的这些维护成本不是抽象的概念,它是真实存在的。

新增词表方案的在项目内外的改动面并不小(特别是 build process 方面),之前有人尝试失败就是实例。兼容三个平台三个生态的 build process 的修改本来就比改脚本困难得多。danny0838 手工做一次 s2hkp 和 hk2sp 的过程,也就应该明白这里的痛点在哪里了。其实 build process 修改的风险远不在代码量上,而是在测试覆盖最薄弱上。这类修改一定要保证 Windows binary,Node package,Python wheels 各种调用方式全部没问题。

reverse-prefer 的争议点在于引入注释中的 optional metadata 语义;RichDict 走的是同一条路线,只是在 metadata 这个方向上继续演化了一步。这两者在各项目中的逻辑修改,改动面反而比 build process 要小得多。这两者路线兼容,具体的选择完全可以日后再议。

综上:对于 OpenCC 这种多构建系统、多语言绑定、多发布渠道的项目而言,新增词表的主要成本不在词表本身,而在于由此带来的构建链和一致性维护成本;而 metadata 类方案的主要成本则在于引入新的语义表达。两类方案解决的是同一个问题,只是把复杂度放在了不同的位置。

@danny0838

danny0838 commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

你的評價並不公正。

如果你所謂的下游是指直接拿 OpenCC dictionary 原始檔去用的專案,它們現在就已經是自行寫 reverse 腳本去生成相應的反向詞典。如果 VariantsRev 改為人工維護,下游未跟進調整建構程序,那和你的 @reverse-prefer 方案一樣,只是遺失反向優先序的資訊。

然而,若下游要跟進調整,前者只要改為使用上游反向詞典並取消自動生成即可;而後者則必須跟著實做同樣複雜的自訂註解處理邏輯。因此你的方案對下游的兼容性較差

我自己就是下游的其中之一,我的專案 sts-lib 的原始碼大家也都看得到,何者對下游影響大,我非常清楚。

況且,果真你的方案那麼好,何以當初要把 TWPhrasesRev 改為人工維護,而不是使用相同做法?箇中理由你應該比我更清楚。

@frankslin

Copy link
Copy Markdown
Collaborator Author

我觉得你有一个根本性的前提弄反了。

OpenCC 首先是一个独立维护的项目,而不是为了兼容某个下游实现而存在。本项目需要优先考虑的是:

  • OpenCC 自身算法和词典的正确性;
  • OpenCC 自身的可维护性;
  • OpenCC 自身的长期演化能力。

然后才是在此基础上尽量兼顾特定的下游项目。

如果某方案能够显著降低项目内部复杂度、减少长期维护成本,那么即使会给部分下游带来迁移成本,也是完全合理的。否则按照这个逻辑,只要有人依赖当前版本的实现,任何内部结构都不能调整了。

事实上,OpenCC 从来没有承诺过「词表格式永久不变」、「词典生成流程永久不变」、「某个词表一定会或不会自动生成」、「某个字词一定要怎么转」、「某些标准一定要照搬」,这些都不应被视为对外兼容性承诺。各个项目可以依赖实现细节,但一旦选择依赖实现细节,就需要自行承担实现细节发生变化的风险和代价。至于某个下游产品如何适配,是各位工程决策的问题。

更何况这点 reverse.py 脚本的改动根本就是微不足道的。如果你需要参考一种同步上游词表数据的方式,根本不用改自己的脚本,照这个项目抄作业就可以了。

另外,我也注意到近期讨论中经常出现另一种情况:从过去的讨论、某次 issue 或某个人的观点中,归纳出一些所谓的「项目原则」,然后再以这些原则作为既定前提来约束当前设计。

你当然可以主张某种设计方向,也完全可以提出自己的理念;但这并不等同于该理念就是「项目原则」。项目原则应当来自项目实际行为、明确文档以及维护决策,而不是你对历史讨论的再诠释。

对于当前问题而言,我们仍然应该比较的是各方案的实际成本与收益,而不是先假定某种原则已经存在,再由该原则推导出某种唯一允许的实现方式。

@danny0838

Copy link
Copy Markdown
Contributor

請回顧這個討論串,一開始是誰先提到下游的?不正是你嗎?

如果下游相容性不重要,你一開始就不用提。既然提了,我便指出你的分析錯誤:你的方案對下游更不友好。

先不說你所謂可以抄的作業是你幾個小時前才在你維護的項目寫出來的腳本,即使可以這樣做,那也意味著下游必須引進一整套 build 依賴,或引進對 GiitHub workflow 的依賴,不見得每個下游都適合且能夠這樣做。相較之下,Rev 詞典改為人工維護的方案,下游只要像以往一樣 copy 詞典檔即可。

你也沒有回答我之前的問題:為什麼當初要把 TWPhrasesRev 改為人工維護?難道當時開發者都是蠢蛋,選擇一項不利長期維護性和演化能力的方案嗎?還是其實就是因為改為人工維護 Rev 詞典恰恰才是更好的方案?

無論如何,我都認為這種涉及重大架構更動的事,應該要經過充分的需求評估及社群討論,尤其至少專案的建立者及管理者 @BYVoid 的看法。

豆腐塊的處理在缺乏充分溝通下急就章上路,並且引進大量詞典更動,也讓大量既有 PR 需要 rebase 及解決大量衝突,光是這點就對 OpenCC 本身的長期維護性和演化能力造成很大影響。而我當時提的「方案4」(建立及維護一個生僻字代換表作為 schema)則不會涉及既有詞典檔案的變動,卻沒有被評估,未來不曉得還要花多少力氣處理衍生困擾。我實在不樂見類似情形重演。

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.

程序自動生成的 HKVariantsRev.txt 不能處理「一香港繁對多 OpenCC 繁」

3 participants