Spaces:
Running
Running
| """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"<mark>{seg}</mark>") | |
| return f'<div class="out">{"".join(out)}</div>' | |
| 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) | |