Spaces:
Running
Running
File size: 7,662 Bytes
3651a61 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | """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)
|