"""Twinity-1 Demo — 中國大陸中文 → 臺灣正體中文,與 OpenCC 並排比較。 Space 啟動時從 model repo 下載權重與字典,之後全部在 CPU 上跑(單執行緒最快)。 """ from __future__ import annotations import difflib import os import sys import time import gradio as gr import opencc import torch from huggingface_hub import snapshot_download MODEL_REPO = os.environ.get("TWINITY_REPO", "JacobLinCool/Twinity-1") torch.set_num_threads(1) # 實測單執行緒最快(小矩陣,執行緒同步成本壓倒運算) _path = snapshot_download(MODEL_REPO) os.environ["TWLAT_HOME"] = f"{_path}/data" sys.path.insert(0, _path) import twlat # noqa: E402 CONV = {p: twlat.Converter(ckpt=f"{_path}/twinity-1.pt", device="cpu", preset=p) for p in ("balanced", "accuracy", "taiwanize", "aggressive")} OPENCC_S2T = opencc.OpenCC("s2t") # 純字形:簡→繁 OPENCC_S2TWP = opencc.OpenCC("s2twp") # 字形+臺灣慣用詞(OpenCC 最強設定) EXAMPLES = [ # 語境相依:同一個詞在不同語境有不同答案(查表做不到) ["这个程序有bug,需要重新编译代码。", "balanced"], ["依法定程序办理相关申请,计划在下周完成。", "balanced"], # 跨詞界:商調|制度,不是 商|調製 ["现行公务人员指名商调制度值得检讨,十姑娘的香港法律顾问也表示关切。", "balanced"], # 標點:巢狀引號轉臺灣規範 ["他问:“老师,‘有条不紊’的‘紊’是什么意思?”", "balanced"], # 詞彙 + 中英數間距 ["发送电子信函之政策:将在事前或注册登录取得您的同意后,发送数据给您。", "balanced"], ["这个视频的信息量很大,博客里有更详细的说明。", "taiwanize"], # 保護段:程式碼與網址 byte-exact 不動 ["运行 `npm install --save-dev typescript` 后重启,参考 https://example.com/docs 。", "balanced"], # 已知弱點:這一句 OpenCC 比 Twinity-1 好(見下方說明) ["服务器和项目经理都需要升级。", "taiwanize"], ] CSS = """ .out {font-size:17px; line-height:2.0} .out mark {background:#d9f0e3; border-radius:3px; padding:1px 3px; box-shadow:inset 0 -2px 0 #2f6f4f} @media (prefers-color-scheme: dark){.out mark{background:#1d3b2c}} """ def _diff_html(base: str, other: str) -> str: """把 other 相對 base 的改動處標出來(用於凸顯系統間差異)。""" sm = difflib.SequenceMatcher(None, base, other, autojunk=False) out = [] for tag, i1, i2, j1, j2 in sm.get_opcodes(): seg = other[j1:j2].replace("&", "&").replace("<", "<") out.append(seg if tag == "equal" else f"{seg}") return f'
{"".join(out)}
' def run(text: str, preset: str): if not text.strip(): return "", "", "", "", "請輸入文字。" t0 = time.perf_counter() res = CONV[preset].explain(text) dt = (time.perf_counter() - t0) * 1000 cc_s2t = OPENCC_S2T.convert(text) cc_twp = OPENCC_S2TWP.convert(text) tw = res.text if res.decisions: rows = ["| 原文 | 改為 | 規則型別 | 勝算比 |", "| --- | --- | --- | --- |"] for d in res.decisions: odds = f"{2.718281828 ** d.utility:.0f}:1" if d.utility < 25 else ">1e10:1" rows.append(f"| `{d.source}` | **`{d.target}`** | {d.rule_type} | {odds} |") table = "\n".join(rows) else: table = "_沒有改動——輸入已符合臺灣正體慣例,或所有候選都未達決策門檻。_" n_diff = sum(1 for a, b in zip(cc_twp, tw) if a != b) + abs(len(cc_twp) - len(tw)) stat = (f"**{len(text)} 字** {len(res.decisions)} 處改動 {dt:.0f} ms(CPU 單執行緒)" f" 操作點 `{preset}` 與 OpenCC s2twp 相異 ≈{n_diff} 字") return (_diff_html(text, tw), _diff_html(text, cc_s2t), _diff_html(text, cc_twp), table, stat) with gr.Blocks(title="Twinity-1|臺灣正體中文轉換") as demo: gr.Markdown(""" # Twinity-1 **中國大陸中文 → 臺灣正體中文。8.86M 參數、100% 確定性、CPU 單執行緒 11,300 字/秒。** 不是查表,也不是叫 LLM 重寫。字典編譯成 conversion lattice 界定「哪裡可以改、可以改成什麼」, 模型只裁決「這個語境該不該改」,Viterbi 挑出全域最佳的編輯集合,最後**只替換必要的字元** ——編輯範圍以外的每個位元組原樣保留。 下面三欄並排:**綠底=相對輸入被改動的字**。OpenCC 的兩個設定是最強的查表基線 (`s2t` 純字形、`s2twp` 字形+臺灣詞彙)。 """) with gr.Row(): with gr.Column(scale=3): inp = gr.Textbox(label="輸入(簡體或大陸用語)", lines=4, placeholder="贴上简体或大陆用语的中文…") with gr.Column(scale=1): preset = gr.Radio(["balanced", "accuracy", "taiwanize", "aggressive"], value="balanced", label="操作點") btn = gr.Button("轉換", variant="primary") stat = gr.Markdown() with gr.Row(): with gr.Column(): gr.Markdown("### ✅ Twinity-1") o_tw = gr.HTML() with gr.Column(): gr.Markdown("### OpenCC `s2t`(純字形)") o_s2t = gr.HTML() with gr.Column(): gr.Markdown("### OpenCC `s2twp`(字形+詞彙)") o_twp = gr.HTML() gr.Markdown("### 逐項決策(Twinity-1 為什麼這樣改)") tbl = gr.Markdown() gr.Markdown(""" --- **這些範例實際會發生什麼**(以下都是實測,不是宣傳): | # | 看點 | 結果 | | --- | --- | --- | | 1・2 | 同一個「程序」:軟體語境 → **程式**;法律語境 → **保留程序** | Twinity-1 兩題都對;OpenCC 第 2 題誤改成「法定程式」 | | 3 | 「商調\|制度」跨詞界 | Twinity-1 保留;OpenCC 誤成「商調**製**度」 | | 4 | 巢狀引號 `“‘ ’”` → `「『 』」` | Twinity-1 轉換;OpenCC 不處理標點 | | 5 | 電子信函 → **電子郵件**、註冊登錄 → **註冊登入** | Twinity-1 兩者都改;OpenCC 只改後者 | | 6 | 視頻/信息/博客 → 影片/資訊/部落格 | 兩者相同(本例用 `taiwanize` 操作點) | | 7 | 程式碼與網址 | 兩者都 byte-exact 保留 | | 8 | **Twinity-1 的已知弱點** | 服務器/項目經理 **OpenCC 改對、Twinity-1 沒改** | 第 8 例是誠實的失敗案例:這類「固定對應」的陸式詞在我們的訓練語料裡缺少 「該改」的訊號(網爬語料原生大量出現且被視為正確),因此模型高信心保留。 查表法在這種固定映射上本來就強。Twinity-1 的優勢在**語境相依**的判斷(第 1–3 例), 整體評測上仍大幅領先(0.9712 vs 0.9176),但單句可以互有勝負。 """) gr.Examples(EXAMPLES, inputs=[inp, preset]) outs = [o_tw, o_s2t, o_twp, tbl, stat] btn.click(run, [inp, preset], outs) inp.submit(run, [inp, preset], outs) gr.Markdown(""" 評測(TWBench-Neutral,1,496 題/8,438 個歧義位點,gold =臺灣正體語料原文): Twinity-1 **0.9712**、OpenCC 0.9176、規則層 zhtw-mcp 0.9263。 與 frontier LLM 基線無顯著差異(p = 0.34),但提供 LLM 給不了的確定性、$0 與 23 ms 延遲。 模型與說明:[JacobLinCool/Twinity-1](https://huggingface.co/JacobLinCool/Twinity-1) """) if __name__ == "__main__": demo.launch(css=CSS)