Twinity-1-Demo / app.py
JacobLinCool's picture
Twinity-1 demo with OpenCC side-by-side comparison
3651a61 verified
Raw
History Blame Contribute Delete
7.66 kB
"""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("&", "&amp;").replace("<", "&lt;")
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)