Spaces:
Sleeping
統計穩定性警示、system prompt 補強、關於面板、多項 bug 修正
Browse files統計正確性:
- cohens_d()/eta_squared() 分母趨近於零時回傳 None 而非失真數值
(新增 EFFECT_UNSTABLE_LABEL),修掉效果量算出天文數字的 bug
- 新增 ZERO_VARIANCE_WARNING:任一組標準差趨近於 0 但檢定仍算出
p 值的退化情況,加註「統計穩定性有限」警示,不隱藏顯著性判斷
- 新增 ZERO_PROPORTION_WARNING(門檻 50%):任一組半數以上樣本值
為 0、SD 未趨近於零但平均數可能被離群值主導的情況,獨立於上面
的 SD 判準另外偵測,兩者可疊加、不重複描述同一組資料
- 修正 docx 差異性分析表格欄位定位改用欄名查表(原本 len(vals)-4
位置推算已對錯欄)
- None/NaN 判斷全面改用 pd.isna(),修掉 docx 表格出現 "nan" 文字
- 群組標籤(如「人工翻譯」)修正為會實際套用到送給 LLM 的
difference_analysis 資料,不再只停留在畫面/圖表
- 差異性分析箱形圖:達顯著指標全部畫出(取消前 12 項上限),排序
改為先依組別、組內再依效果量絕對值
- 兩群比較用詞「較高/較低」、三群以上用詞「最高/最低」
System prompt(LLM1):
- 補上角色邊界、11 條嚴格禁令(原 5 條)、指標代碼對照表、
metadata 一致性檢查指示
- 新增 INDICATOR_REFERENCE_TABLE:從 INDEX_CATEGORIES 動態產生,
取代手動謄寫,避免與實際指標定義漂移
- 每次分析新增 06_system_prompt.txt 可重現性附件,保留當次實際
送出的完整 system prompt 原文
介面:
- 新增「關於」面板(系統版本、免責聲明、姊妹工具、開發紀錄;版權
/聯絡方式/引用格式因論文尚未發表,暫不公開真實姓名或 email)
- 情境 1、4 卡片標題拿掉「同一原文的」;情境選擇小標改為
「請選擇最符合您針對同一原文的重譯研究設計的比較情境」
- 「語料 Metadata 說明」全面改稱「語料說明」(含隱形橋接按鈕文字
同步)
- 隱私提醒文案調整(移除「先與計畫主持人確認」,改為「先行確認」)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- app_agent.py +468 -66
- assets/app.css +48 -5
- assets/icons/info-circle.svg +1 -0
|
@@ -152,6 +152,48 @@ LLM2_MODEL = "google/gemma-4-26b-a4b-it"
|
|
| 152 |
# 報告本身走,不用回頭 git blame 才查得到。
|
| 153 |
PROMPT_VERSION = "v1"
|
| 154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
ALPHA = 0.05
|
| 156 |
|
| 157 |
# 2026-08-22(業主提出「數據完整性檢查」的必要性,追查後確認是真實存在的
|
|
@@ -166,6 +208,42 @@ ALPHA = 0.05
|
|
| 166 |
# 這個常數判斷,不要漏掉、也不要各自硬寫字串。
|
| 167 |
INSUFFICIENT_DATA_LABEL = "資料不足"
|
| 168 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
# ── POS 集合(與 Prog02 相同)───────────────────────────────────────────────
|
| 170 |
Vset = ['VC','VH','VE','VJ','VA','V2','VK','VL','VD','VCL','VHC','VB','VF','VI','VAC','VG']
|
| 171 |
Nset = ['Na','Nb','Nc','Ncd','Nd','Nf','Nh','Nv']
|
|
@@ -182,7 +260,7 @@ Aset = ['A']
|
|
| 182 |
# ── 研究情境定義 ─────────────────────────────────────────────────────────────
|
| 183 |
SCENARIOS = {
|
| 184 |
"1": {
|
| 185 |
-
"title": "
|
| 186 |
"short": "人機對比",
|
| 187 |
"upload_prompt": (
|
| 188 |
"1. 檔案格式:僅接受 **.txt** 純文字檔。\n"
|
|
@@ -218,7 +296,7 @@ SCENARIOS = {
|
|
| 218 |
"scenario_module_key": "scenario_4",
|
| 219 |
},
|
| 220 |
"4": {
|
| 221 |
-
"title": "
|
| 222 |
"short": "歷時重譯",
|
| 223 |
"upload_prompt": (
|
| 224 |
"1. 檔案格式:僅接受 **.txt** 純文字檔。\n"
|
|
@@ -313,6 +391,28 @@ def _drop_hidden_indicators(df):
|
|
| 313 |
# 之後隱藏清單增減,這裡跟著自動變動,不用每次巡一遍程式碼改字面數字。
|
| 314 |
TOTAL_VISIBLE_INDICATORS = len(INDEX_NAME_MAP) - len(HIDDEN_INDICATOR_CODES)
|
| 315 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
# ── LLM 報告用情境模組(同 prog04)─────────────────────────────────────────
|
| 317 |
SCENARIO_MODULE_1 = """## 三、情境特定分析:歷時變化分析
|
| 318 |
|
|
@@ -346,7 +446,6 @@ SCENARIO_MODULE_2 = """## 三、情境特定分析:人機差異分析
|
|
| 346 |
- 人工譯本在哪些指標上顯著偏離機器譯本?
|
| 347 |
|
| 348 |
### 禁令補充
|
| 349 |
-
- 不得對機器翻譯系統的品質做整體評價。
|
| 350 |
- 不得預設「人工一定優於機器」或反之。"""
|
| 351 |
|
| 352 |
SCENARIO_MODULE_3 = """## 三、情境特定分析:改寫策略分析
|
|
@@ -371,10 +470,7 @@ SCENARIO_MODULE_4 = """## 三、情境特定分析:個人風格差異分析
|
|
| 371 |
- 在哪些指標上,譯者間差異達到統計顯著?
|
| 372 |
|
| 373 |
### 3.2 風格穩定性的檢視
|
| 374 |
-
- 譯者內變異 vs 譯者間變異的比較
|
| 375 |
-
|
| 376 |
-
### 禁令補充
|
| 377 |
-
- 不得將個人風格與「翻譯能力」掛鉤。"""
|
| 378 |
|
| 379 |
SCENARIO_MODULES = {
|
| 380 |
"scenario_1": SCENARIO_MODULE_1,
|
|
@@ -386,12 +482,39 @@ SCENARIO_MODULES = {
|
|
| 386 |
SYSTEM_PROMPT_BASE = """# 角色定義
|
| 387 |
你是一位翻譯研究的量化分析助理。你的工作是將統計數據轉化為結構化的觀察與假設,供研究者進一步驗證。
|
| 388 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
# 嚴格禁令
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
|
| 396 |
# 輸入資料結構
|
| 397 |
【研究情境】:{scenario_type}
|
|
@@ -415,23 +538,44 @@ SYSTEM_PROMPT_BASE = """# 角色定義
|
|
| 415 |
## 一、語料說明
|
| 416 |
用 2-4 句話摘要上方【譯本 metadata】提供的原文資訊、群組層與個體層背景(例如原文標題、各組別的翻譯類型/方向、個別譯本的譯者或系統名稱等,視實際提供的內容而定)。
|
| 417 |
若 metadata 顯示使用者略過未填,僅陳述「使用者未提供語料補充說明」,不得臆測或杜撰內容。這個區塊只做摘要陳述,不做任何統計分析或推論。
|
|
|
|
| 418 |
|
| 419 |
## 二、數據事實描述
|
| 420 |
本區塊須包含以下三類數據事實,缺一不可(量化分析結果 JSON 裡都有對應資料,不得因為篇幅省略):
|
| 421 |
-
1. 差異性分析:每項陳述除了指標代碼、p 值與效果量之外,**必須明確指出各組別中平均數
|
| 422 |
-
|
| 423 |
名稱結尾 _M 的平均數欄位)——只寫 p 值與效果量、不寫是哪一組偏高偏低,讀者無法理解差異的
|
| 424 |
方向與意義。
|
| 425 |
三組以上時是用 ANOVA 檢定,效果量(η²)本身不帶方向性(不像兩組時的 Cohen's d 還有正負號
|
| 426 |
-
可看出方向),更不能省略
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
【資料完整性】若 JSON 附有「資料完整性提醒」欄位(表示部分指標因組別樣本數不足、根本無法
|
| 432 |
進行統計檢定),這些指標一律只能陳述為「資料不足,無法檢定」,絕對不可以寫成「無顯著差異」,
|
| 433 |
也不可以把它們計入「達統計顯著」或「無明顯差異」的統計比例——這是兩種完全不同的結論,混為
|
| 434 |
一談會誤導讀者以為系統已經檢定過、確認沒有差異。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
2. 監督式學習(隨機森林):說明 feature_importance_top20 裡重要性最高的 3-5 項指標與其重要性數值。
|
| 436 |
同時陳述「隨機森林分類準確率」的數字,並原樣附上 JSON 裡「隨機森林準確率之樣本外預測意義」
|
| 437 |
這欄給的完整句子(那句話已經是可以直接使用的中文敘述,不必再改寫或省略)。
|
|
@@ -956,20 +1100,38 @@ def run_indicator_calculation(file_paths: list, output_dir: Path, resources: dic
|
|
| 956 |
# ============================================================================
|
| 957 |
|
| 958 |
def cohens_d(g1, g2):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 959 |
n1, n2 = len(g1), len(g2)
|
| 960 |
if n1 < 2 or n2 < 2:
|
| 961 |
-
return
|
| 962 |
var1, var2 = np.var(g1, ddof=1), np.var(g2, ddof=1)
|
| 963 |
pooled = np.sqrt(((n1-1)*var1 + (n2-1)*var2) / (n1+n2-2))
|
| 964 |
-
|
|
|
|
|
|
|
|
|
|
| 965 |
|
| 966 |
|
| 967 |
def eta_squared(group_data):
|
|
|
|
| 968 |
all_data = np.concatenate(group_data)
|
| 969 |
grand_mean = all_data.mean()
|
| 970 |
ss_between = sum(len(g) * (np.mean(g) - grand_mean)**2 for g in group_data)
|
| 971 |
ss_total = np.sum((all_data - grand_mean)**2)
|
| 972 |
-
|
|
|
|
|
|
|
|
|
|
| 973 |
|
| 974 |
|
| 975 |
def effect_label(val, method):
|
|
@@ -1102,11 +1264,33 @@ def run_statistical_analysis(csv_path: Path, output_dir: Path, group_labels: dic
|
|
| 1102 |
row["顯著"] = INSUFFICIENT_DATA_LABEL
|
| 1103 |
row["效果量"] = None
|
| 1104 |
row["效果大小"] = None
|
|
|
|
| 1105 |
else:
|
| 1106 |
es = cohens_d(gdata[0], gdata[1]) if n_groups == 2 else eta_squared(gdata)
|
| 1107 |
-
row["顯著"]
|
| 1108 |
-
|
| 1109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1110 |
diff_results.append(row)
|
| 1111 |
|
| 1112 |
diff_df = pd.DataFrame(diff_results)
|
|
@@ -1115,17 +1299,40 @@ def run_statistical_analysis(csv_path: Path, output_dir: Path, group_labels: dic
|
|
| 1115 |
diff_path = output_dir / "02_difference_analysis.csv"
|
| 1116 |
diff_df.to_csv(str(diff_path), index=False, encoding='utf-8-sig')
|
| 1117 |
|
| 1118 |
-
# boxplot(
|
| 1119 |
# 2026-08-14(業主要求):原本取前 12 項是照指標在資料表裡的固定欄位
|
| 1120 |
# 順序(基礎統計→...→詞性標記)往下數,不是「最顯著/效果量最大」的
|
| 1121 |
-
# 12 項
|
| 1122 |
-
#
|
|
|
|
|
|
|
|
|
|
| 1123 |
box_path = None
|
| 1124 |
if len(sig_df) > 0:
|
| 1125 |
-
sig_df_sorted = sig_df.
|
| 1126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1127 |
)
|
| 1128 |
-
sig_cols = sig_df_sorted['指標'].tolist()
|
| 1129 |
n_row = max(1, (len(sig_cols) + 2) // 3)
|
| 1130 |
fig, axes = plt.subplots(n_row, 3, figsize=(14, 4*n_row))
|
| 1131 |
axes = np.array(axes).flatten()
|
|
@@ -1146,15 +1353,23 @@ def run_statistical_analysis(csv_path: Path, output_dir: Path, group_labels: dic
|
|
| 1146 |
# 「有沒有差」。
|
| 1147 |
row = diff_df.loc[diff_df['指標']==col]
|
| 1148 |
pv_str = f"p={row['p值'].values[0]:.4f}" if len(row)>0 else ""
|
| 1149 |
-
|
| 1150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1151 |
ax.set_title(f"{col}\n({pv_str}{es_str})", fontsize=8)
|
| 1152 |
for j in range(len(sig_cols), len(axes)):
|
| 1153 |
axes[j].set_visible(False)
|
| 1154 |
# 2026-08-14(業主要求):原本「Significant Variables by Type1」直接
|
| 1155 |
-
# 洩漏內部欄位名稱 Type1,使用者看不懂在比較什麼
|
| 1156 |
-
#
|
| 1157 |
-
|
|
|
|
| 1158 |
plt.tight_layout()
|
| 1159 |
box_path = output_dir / "02_significant_boxplots.png"
|
| 1160 |
plt.savefig(str(box_path), dpi=150, bbox_inches='tight')
|
|
@@ -1325,7 +1540,9 @@ def run_statistical_analysis(csv_path: Path, output_dir: Path, group_labels: dic
|
|
| 1325 |
f"{row['平均數']:.4f}", f"{row['中位數']:.4f}",
|
| 1326 |
f"{row['標準差']:.4f}", f"{row['最小值']:.4f}",
|
| 1327 |
f"{row['最大值']:.4f}",
|
| 1328 |
-
|
|
|
|
|
|
|
| 1329 |
for j, val in enumerate(vals):
|
| 1330 |
cell = tbl.rows[i].cells[j]
|
| 1331 |
cell.text = val
|
|
@@ -1352,7 +1569,7 @@ def run_statistical_analysis(csv_path: Path, output_dir: Path, group_labels: dic
|
|
| 1352 |
group_sub_headers = []
|
| 1353 |
for g in groups:
|
| 1354 |
group_sub_headers.extend([f"{g}\nN", f"{g}\nM", f"{g}\nSD"])
|
| 1355 |
-
diff_headers = ["指標"] + group_sub_headers + ["方法","統計量","p值","顯著","效果量","效果大小"]
|
| 1356 |
diff_tbl = doc.add_table(rows=1+len(diff_df), cols=len(diff_headers))
|
| 1357 |
diff_tbl.style = 'Light Shading Accent 1'
|
| 1358 |
for j, h in enumerate(diff_headers):
|
|
@@ -1367,13 +1584,30 @@ def run_statistical_analysis(csv_path: Path, output_dir: Path, group_labels: dic
|
|
| 1367 |
vals.extend([str(int(row[f"{g}_N"])),
|
| 1368 |
f"{row[f'{g}_M']:.4f}",
|
| 1369 |
f"{row[f'{g}_SD']:.4f}"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1370 |
vals.extend([str(row['方法']),
|
| 1371 |
-
f"{row['統計量']:.4f}" if row['統計量']
|
| 1372 |
-
f"{row['p值']:.4f}" if row['p值']
|
| 1373 |
str(row['顯著']),
|
| 1374 |
-
f"{row['效果量']:.4f}" if row['效果量']
|
| 1375 |
-
str(row['效果大小']) if row['效果大小']
|
|
|
|
| 1376 |
_is_untestable_row = row['顯著'] == INSUFFICIENT_DATA_LABEL
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1377 |
for j, val in enumerate(vals):
|
| 1378 |
cell = diff_tbl.rows[i].cells[j]
|
| 1379 |
cell.text = val
|
|
@@ -1385,11 +1619,16 @@ def run_statistical_analysis(csv_path: Path, output_dir: Path, group_labels: dic
|
|
| 1385 |
# 不能共用紅字——紅字在這份報告的視覺語言裡代表「這是
|
| 1386 |
# 值得注意的發現」,「資料不足」剛好相反,是「這項不能
|
| 1387 |
# 拿來當發現看」,用紅字反而會讓人誤以為它也是顯著結果。
|
| 1388 |
-
if row['顯著'] not in ("ns", INSUFFICIENT_DATA_LABEL) and j ==
|
| 1389 |
r.font.color.rgb = RGBColor(0xCC, 0x00, 0x00)
|
| 1390 |
elif _is_untestable_row:
|
| 1391 |
r.font.color.rgb = RGBColor(0x88, 0x88, 0x88)
|
| 1392 |
r.italic = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1393 |
doc.add_paragraph('')
|
| 1394 |
if box_path and box_path.exists():
|
| 1395 |
doc.add_heading('顯著指標箱形圖', level=2)
|
|
@@ -1451,14 +1690,22 @@ def run_statistical_analysis(csv_path: Path, output_dir: Path, group_labels: dic
|
|
| 1451 |
# SECTION 7: LLM Report Writing (adapted from prog04, uses LLM1)
|
| 1452 |
# ============================================================================
|
| 1453 |
|
| 1454 |
-
def load_analysis_data_for_llm(csv_path: Path, output_dir: Path, ml_summary: dict = None
|
|
|
|
| 1455 |
"""讀取分析資料組成 JSON 結構。
|
| 1456 |
ml_summary:{"accuracy": float, "do_eval": bool} 選填,來自
|
| 1457 |
run_statistical_analysis() 的回傳值。之前這裡完全沒有把隨機森林的
|
| 1458 |
準確率數字(accuracy)跟「這次是不是真的切了測試集」(do_eval)
|
| 1459 |
交給 LLM,LLM 因此連準確率是多少都不知道,等於沒有材料可以討論
|
| 1460 |
監督式學習結果——03_feature_importance.csv 只有各指標的重要性排名,
|
| 1461 |
-
沒有整體準確率這個數字。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1462 |
data = {}
|
| 1463 |
if ml_summary:
|
| 1464 |
# 2026-08-15(業主回報):原本這裡是英文鍵名 rf_accuracy/
|
|
@@ -1494,6 +1741,17 @@ def load_analysis_data_for_llm(csv_path: Path, output_dir: Path, ml_summary: dic
|
|
| 1494 |
p = output_dir / fname
|
| 1495 |
if p.exists():
|
| 1496 |
d = pd.read_csv(str(p))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1497 |
if key == "cluster_results":
|
| 1498 |
# 2026-08-15(業主要求):跟 rf_accuracy_is_meaningful 同一個
|
| 1499 |
# 教訓——原本這裡的欄名是 Type1,LLM 直接照抄,報告寫成
|
|
@@ -1532,6 +1790,27 @@ def load_analysis_data_for_llm(csv_path: Path, output_dir: Path, ml_summary: dic
|
|
| 1532 |
"或納入「達統計顯著」/「無明顯差異」的分類統計,兩者是完全不同的結論。"
|
| 1533 |
if len(untestable) > 0 else ""
|
| 1534 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1535 |
else:
|
| 1536 |
data[key] = []
|
| 1537 |
if "significant_count" not in data:
|
|
@@ -1569,13 +1848,16 @@ def _add_inline_markdown_runs(paragraph, text: str) -> None:
|
|
| 1569 |
|
| 1570 |
|
| 1571 |
def run_report_writing(scenario_key: str, user_metadata: str, user_focus: str,
|
| 1572 |
-
csv_path: Path, output_dir: Path, ml_summary: dict = None
|
|
|
|
| 1573 |
"""
|
| 1574 |
呼叫主 Agent (LLM1) 撰寫研究報告,儲存 docx + txt,回傳路徑 dict。
|
| 1575 |
ml_summary:見 load_analysis_data_for_llm() 的說明。
|
|
|
|
| 1576 |
"""
|
| 1577 |
output_dir.mkdir(parents=True, exist_ok=True)
|
| 1578 |
-
analysis_data = load_analysis_data_for_llm(csv_path, output_dir, ml_summary=ml_summary
|
|
|
|
| 1579 |
|
| 1580 |
scenario_info = SCENARIOS.get(scenario_key, SCENARIOS["1"])
|
| 1581 |
scenario_mod_key = scenario_info.get("scenario_module_key", "scenario_1")
|
|
@@ -1592,7 +1874,27 @@ def run_report_writing(scenario_key: str, user_metadata: str, user_focus: str,
|
|
| 1592 |
metadata = metadata_str,
|
| 1593 |
user_focus = user_focus if user_focus else "無",
|
| 1594 |
scenario_module = scenario_module,
|
|
|
|
| 1595 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1596 |
user_prompt = (
|
| 1597 |
f"請根據以下量化分析結果,按照 system prompt 中的輸出結構進行深度分析。\n\n"
|
| 1598 |
f"【量化分析資料(JSON)】:\n```json\n"
|
|
@@ -1629,6 +1931,26 @@ def run_report_writing(scenario_key: str, user_metadata: str, user_focus: str,
|
|
| 1629 |
style='Normal').alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 1630 |
doc.add_paragraph(f'報告時間:{now_tw().strftime("%Y-%m-%d %H:%M:%S")}(台灣時間)',
|
| 1631 |
style='Normal').alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1632 |
doc.add_page_break()
|
| 1633 |
# 2026-08-14(待辦 A1):區塊層級語法(標題/項目符號)沿用原本判斷,
|
| 1634 |
# 差別是段落文字改用 _add_inline_markdown_runs() 逐段解析 **粗體**/
|
|
@@ -1652,6 +1974,15 @@ def run_report_writing(scenario_key: str, user_metadata: str, user_focus: str,
|
|
| 1652 |
# txt 版之前完全沒寫,這裡補上、順便帶提示詞版本號,兩種格式的
|
| 1653 |
# 稽核資訊要一致,不能只有 docx 有。
|
| 1654 |
f.write(f"模型:主 Agent(LLM1){LLM1_MODEL}(提示詞版本 {PROMPT_VERSION})\n")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1655 |
# 2026-07-27:原本用 "="*60 當純文字分隔線,但緊接在上兩行文字正下方,
|
| 1656 |
# 符合 Markdown Setext 標題語法(文字正上方接一整行 = 會被解讀成 h1
|
| 1657 |
# 底線),導致這兩行 metadata 被渲染成巨大無樣式標題。改成前面留空行
|
|
@@ -1661,9 +1992,10 @@ def run_report_writing(scenario_key: str, user_metadata: str, user_focus: str,
|
|
| 1661 |
f.write(result_text)
|
| 1662 |
|
| 1663 |
return {
|
| 1664 |
-
"docx_path":
|
| 1665 |
-
"txt_path":
|
| 1666 |
-
"result_text":
|
|
|
|
| 1667 |
}
|
| 1668 |
|
| 1669 |
|
|
@@ -2130,8 +2462,9 @@ def run_computation_pipeline_streaming(state: dict):
|
|
| 2130 |
csv_path = csv_path,
|
| 2131 |
output_dir = output_dir,
|
| 2132 |
ml_summary = {"accuracy": anal.get("accuracy"), "do_eval": anal.get("do_eval")},
|
|
|
|
| 2133 |
)
|
| 2134 |
-
results_paths.extend([str(rpt["docx_path"]), str(rpt["txt_path"])])
|
| 2135 |
A["report"]["status"] = "done"
|
| 2136 |
A["report"]["line"] = "研究報告已產出"
|
| 2137 |
yield " [OK] 研究報告撰寫完成", False, state
|
|
@@ -3000,11 +3333,11 @@ def create_app():
|
|
| 3000 |
# 邏輯(on_scenario_pick)完全不用改。
|
| 3001 |
sc_btn_1 = gr.Button("1 人工翻譯 vs 機器翻譯", variant="secondary",
|
| 3002 |
elem_id="sc_btn_1", elem_classes=["sc-bridge-btn"])
|
| 3003 |
-
sc_btn_2 = gr.Button("2 全譯 vs 編譯
|
| 3004 |
elem_id="sc_btn_2", elem_classes=["sc-bridge-btn"])
|
| 3005 |
-
sc_btn_3 = gr.Button("3
|
| 3006 |
elem_id="sc_btn_3", elem_classes=["sc-bridge-btn"])
|
| 3007 |
-
sc_btn_4 = gr.Button("4 不同時期
|
| 3008 |
elem_id="sc_btn_4", elem_classes=["sc-bridge-btn"])
|
| 3009 |
|
| 3010 |
# Phase 8-E:手機漢堡開關+抽屜背景遮罩。跟 sc_btn_1..4 同一類坑:
|
|
@@ -3070,6 +3403,19 @@ def create_app():
|
|
| 3070 |
reset_confirm_btn = gr.Button("確定回首頁", elem_id="reset_confirm_btn",
|
| 3071 |
scale=1, variant="primary")
|
| 3072 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3073 |
# Phase 8-B:三欄式工作區——nav-col(窄,導覽/重置)+chat-col
|
| 3074 |
# (對話視窗+輸入框,獨立捲動)+canvas-col(步驟列+上傳/表單/
|
| 3075 |
# 執行紀錄,獨立捲動)。手機寬度見 CSS 的 media query 改上下堆疊。
|
|
@@ -3249,6 +3595,17 @@ def create_app():
|
|
| 3249 |
elem_classes=["result-nav-btn", "result-nav-btn--muted"],
|
| 3250 |
variant="secondary",
|
| 3251 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3252 |
# ↑ nav_items_scroll 到此結束(中段可捲動區)。
|
| 3253 |
|
| 3254 |
# 底部工具列(固定,不隨中段捲動):明暗切換/收合鈕原本在頂端
|
|
@@ -3348,7 +3705,7 @@ def create_app():
|
|
| 3348 |
# 標題+說明文字沿用 metadata_row 那則 Markdown 的寫法
|
| 3349 |
# (純 "### 標題\n說明文字",不套 elem_classes),維持
|
| 3350 |
# 同一批「Canvas 操作區塊」卡片的既有排版慣例。
|
| 3351 |
-
gr.Markdown("### 情境選擇\n請選擇最符合您研究設計的比較情境,選定後將進入下一步「文本上傳」。")
|
| 3352 |
gr.HTML(_build_scenario_cards_html("workspace_scenario_grid"))
|
| 3353 |
|
| 3354 |
# Phase 8(千卉實測後回頭調整):這張卡片改回正式使用
|
|
@@ -3389,7 +3746,7 @@ def create_app():
|
|
| 3389 |
|
| 3390 |
# ── 語料 Metadata 說明表單(上傳後、分析前)───────
|
| 3391 |
with gr.Column(visible=False, elem_id="metadata_zone", elem_classes=["metadata-zone"]) as metadata_row:
|
| 3392 |
-
gr.Markdown("### 📑 語料
|
| 3393 |
|
| 3394 |
gr.Markdown("**原文資訊**")
|
| 3395 |
with gr.Row():
|
|
@@ -3723,6 +4080,18 @@ def create_app():
|
|
| 3723 |
"無法進行統計檢定,不計入上述比例。"
|
| 3724 |
if untestable_n > 0 else ""
|
| 3725 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3726 |
# 2026-08-14(業主要求):標題句前面加一句「以[檢定方法]比較
|
| 3727 |
# [分組]文本」,方法與分組名稱都從 diff 本身反推——分組代碼
|
| 3728 |
# 是「{組別}_N」這組欄名的前綴(run_statistical_analysis 產出
|
|
@@ -3749,7 +4118,7 @@ def create_app():
|
|
| 3749 |
# 的做法一致。
|
| 3750 |
R[6] = gr.update(
|
| 3751 |
value=(f"### 差異性分析\n{prefix}共 **{len(diff)}** 個指標,"
|
| 3752 |
-
f"其中 **{len(sig)}** 個達顯著差異(p < {ALPHA})。{empty_note}{untestable_note}\n\n"
|
| 3753 |
f"下方表格中 N=樣本數、M=平均數、SD=標準差。"),
|
| 3754 |
visible=True,
|
| 3755 |
)
|
|
@@ -3821,6 +4190,13 @@ def create_app():
|
|
| 3821 |
try:
|
| 3822 |
with open(str(txt_files[0]), 'r', encoding='utf-8') as f:
|
| 3823 |
rpt_text = f.read()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3824 |
model_note = (f"<span style='color:var(--c-accent-strong);font-weight:600'>"
|
| 3825 |
f"本報告由主 Agent(LLM1)模型 {LLM1_MODEL} 生成"
|
| 3826 |
f"(提示詞版本 {PROMPT_VERSION})</span>")
|
|
@@ -3855,6 +4231,12 @@ def create_app():
|
|
| 3855 |
if not llm_dst.exists():
|
| 3856 |
shutil.copy(str(docx_files[0]), str(llm_dst))
|
| 3857 |
dl_rpt.append(str(llm_dst))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3858 |
if dl_rpt:
|
| 3859 |
R[19] = gr.update(value=dl_rpt, visible=True)
|
| 3860 |
|
|
@@ -4323,7 +4705,7 @@ def create_app():
|
|
| 4323 |
"---\n\n"
|
| 4324 |
"📢 **隱私提醒**:上傳的文本內容將傳送至第三方 AI 服務"
|
| 4325 |
"(OpenRouter 及其上游模型供應商)進行語言分析與報告撰寫。"
|
| 4326 |
-
"請確認語料不涉及尚未公開發表之機密內容,如有疑慮請先
|
| 4327 |
)
|
| 4328 |
yield (chat_history, log_history, state, gr.update(visible=True),
|
| 4329 |
_todo_update(state), gr.update(value=hint),
|
|
@@ -4422,6 +4804,9 @@ def create_app():
|
|
| 4422 |
|
| 4423 |
reset_btn.click(fn=lambda: gr.update(visible=True), outputs=[reset_confirm_row])
|
| 4424 |
reset_cancel_btn.click(fn=lambda: gr.update(visible=False), outputs=[reset_confirm_row])
|
|
|
|
|
|
|
|
|
|
| 4425 |
# 2026-08-12:reset_confirm_btn 的接線搬到檔案最後(見 create_app 結尾
|
| 4426 |
# 「重置接線」那段)。原因:它需要 cancels=[分析運算那兩條事件鏈],而
|
| 4427 |
# 那兩條要等 meta_confirm_btn/meta_cancel_btn 接完才存在。Gradio 的
|
|
@@ -4437,8 +4822,18 @@ def create_app():
|
|
| 4437 |
# 發言氣泡,看起來像 AI 自問自答):在流程真正開始處理之前,先補一則
|
| 4438 |
# 使用者訊息氣泡,把「使用者上傳了哪些檔案」呈現出來,對話才有一來
|
| 4439 |
# 一往的感覺。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4440 |
_uploaded_names = [Path(f.name if hasattr(f, "name") else str(f)).name for f in files]
|
| 4441 |
-
|
|
|
|
| 4442 |
|
| 4443 |
# 文本上傳(step 2:3-A 新增「研究選擇」為第 1 步後,這裡的步驟
|
| 4444 |
# 編號全部 +1):開始處理即關閉上傳彈窗,避免遮罩擋住操作。
|
|
@@ -4491,9 +4886,9 @@ def create_app():
|
|
| 4491 |
log_history = _log_append(log_history, "✅ 格式驗證通過")
|
| 4492 |
# 格式通過後:顯示 metadata 說明表單(語料說明=step 3),等使用者填寫
|
| 4493 |
state["state"] = "METADATA_INPUT"
|
| 4494 |
-
meta_hint = "請填寫語料
|
| 4495 |
chat_history = chat_history + [_assistant_msg(meta_hint)]
|
| 4496 |
-
log_history = _log_append(log_history, "📑 請填寫語料
|
| 4497 |
# 2026-07-31(Batch E 第 5、6 項):業主回報「語料說明」「分析
|
| 4498 |
# 運算」兩步驟畫面上還留著「文本上傳」的進度區塊(檔案接收 ✓/
|
| 4499 |
# 格式驗證 ✓),流程往下推進後這塊資訊已經沒用。清空的時機點
|
|
@@ -4583,7 +4978,7 @@ def create_app():
|
|
| 4583 |
(群組標籤/翻譯類型/翻譯方向/譯者背景/目標讀者)攤平排列,
|
| 4584 |
對應 meta_group_fields 三個 slot、各自 5 個 Textbox 的值。"""
|
| 4585 |
sc = state.get("scenario", "")
|
| 4586 |
-
L = ["## 語料
|
| 4587 |
"### 原文資訊",
|
| 4588 |
f"- 原文標題:{title or '(未填)'}",
|
| 4589 |
f"- 作者:{author or '(未填)'}",
|
|
@@ -4767,6 +5162,13 @@ def create_app():
|
|
| 4767 |
state["cleaned_files"] = []
|
| 4768 |
state["current_step"] = 1
|
| 4769 |
state["done_steps"] = [s for s in state.get("done_steps", []) if s != 1]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4770 |
return (
|
| 4771 |
gr.update(visible=True), # scenario_select_row
|
| 4772 |
gr.update(visible=False), # upload_row
|
|
@@ -4834,10 +5236,10 @@ def create_app():
|
|
| 4834 |
version, ngroups, method, group_field_values, indiv_df)
|
| 4835 |
state["group_labels"] = _extract_group_labels(state, group_field_values)
|
| 4836 |
state["individual_grouping"] = _extract_individual_grouping(_df_rows(indiv_df), method)
|
| 4837 |
-
log_history = _log_append(log_history, "✅ 已記錄語料
|
| 4838 |
# 2026-08-22:同上傳檔案那段,補一則使用者訊息氣泡,呈現「使用者
|
| 4839 |
# 填寫完語料說明並送出」這個動作,避免對話看起來像 AI 自問自答。
|
| 4840 |
-
chat_history = chat_history + [_user_msg("已
|
| 4841 |
# 在主對話視窗立即顯示「開始分析」訊息,避免長時間運算期間畫面看起來像沒有反應
|
| 4842 |
chat_history = chat_history + [_assistant_msg(_ANALYSIS_START_MSG)]
|
| 4843 |
yield (chat_history, log_history, state, gr.update(visible=False),
|
|
@@ -4881,8 +5283,8 @@ def create_app():
|
|
| 4881 |
yield (chat_history, log_history, state, gr.update(), _todo_update(state),
|
| 4882 |
gr.update(), gr.update(), *_no_tabs)
|
| 4883 |
return
|
| 4884 |
-
state["user_metadata"] = state.get("user_metadata", "") or "(使用者未提供語料
|
| 4885 |
-
log_history = _log_append(log_history, "⏭️ 已略過
|
| 4886 |
# 2026-08-22:同 on_metadata_confirm,補使用者訊息氣泡(略過版本)。
|
| 4887 |
chat_history = chat_history + [_user_msg("略過填寫,直接開始分析")]
|
| 4888 |
chat_history = chat_history + [_assistant_msg(_ANALYSIS_START_MSG)]
|
|
|
|
| 152 |
# 報告本身走,不用回頭 git blame 才查得到。
|
| 153 |
PROMPT_VERSION = "v1"
|
| 154 |
|
| 155 |
+
# 2026-08-26(「關於本系統」面板用):跟 PROMPT_VERSION 是兩件不同的事——
|
| 156 |
+
# PROMPT_VERSION 只追蹤 LLM 提示詞內容,這個追蹤的是整個系統(畫面、流程、
|
| 157 |
+
# 各項修正)的版本,給使用者回報問題時對照用。用日期字串(而非 v1.0 這種
|
| 158 |
+
# 語意化版號)是刻意的:日期比較不會忘記更新,且跟 使用手冊.md 文末的
|
| 159 |
+
# 「文件版本」日期同一套慣例。有實質功能異動時手動更新這個日期即可。
|
| 160 |
+
APP_VERSION = "2026-08-26"
|
| 161 |
+
|
| 162 |
+
# 2026-08-26(業主指定內容):導覽列「關於」按鈕彈出的說明面板。版權聲明/
|
| 163 |
+
# 聯絡方式/Bug 回報/引用格式這四項業主明確要求「論文發表前不公開姓名或
|
| 164 |
+
# 信箱」,這裡不放任何真實聯絡資訊、也不留 [方括號] 這種看起來像沒填完的
|
| 165 |
+
# 佔位符,改寫成正式的「將於論文發表後公開」——對使用者來說是完整、有意義
|
| 166 |
+
# 的句子,之後論文發表了直接把這幾行換成真實資訊即可。
|
| 167 |
+
ABOUT_CONTENT_MD = f"""## 關於本系統
|
| 168 |
+
|
| 169 |
+
**譯彩紛呈 II 分析引擎 SpectraEngine**(TransPrism: An Analytical Framework for Retranslation II)
|
| 170 |
+
|
| 171 |
+
系統版本:{APP_VERSION} 回報問題時請附上此版本號
|
| 172 |
+
|
| 173 |
+
---
|
| 174 |
+
|
| 175 |
+
### 免責聲明
|
| 176 |
+
本系統分析結果僅供研究參考,不構成正式學術判斷;上傳內容將傳送至第三方 AI 服務進行分析,請勿上傳尚未公開發表之機密內容。
|
| 177 |
+
|
| 178 |
+
### 姊妹工具
|
| 179 |
+
文本須先經由「[語料淬鍊(CorpusRefinery)](https://github.com/minchun-git/I_CorpusRefinery)」完成斷詞後才能上傳本系統分析。
|
| 180 |
+
|
| 181 |
+
### 開發紀錄
|
| 182 |
+
系統功能與版本異動,請參見導覽列「使用手冊」文末的文件版本紀錄。
|
| 183 |
+
|
| 184 |
+
### 版權聲明
|
| 185 |
+
© 2026|作者姓名/單位將於論文發表後公開
|
| 186 |
+
|
| 187 |
+
### 聯絡方式
|
| 188 |
+
將於論文發表後公開
|
| 189 |
+
|
| 190 |
+
### Bug 回報
|
| 191 |
+
聯絡窗口將於論文發表後公開
|
| 192 |
+
|
| 193 |
+
### 引用格式
|
| 194 |
+
建議引用格式將於論文發表後一併提供
|
| 195 |
+
"""
|
| 196 |
+
|
| 197 |
ALPHA = 0.05
|
| 198 |
|
| 199 |
# 2026-08-22(業主提出「數據完整性檢查」的必要性,追查後確認是真實存在的
|
|
|
|
| 208 |
# 這個常數判斷,不要漏掉、也不要各自硬寫字串。
|
| 209 |
INSUFFICIENT_DATA_LABEL = "資料不足"
|
| 210 |
|
| 211 |
+
# 2026-08-24(業主回報:報告上出現「Cohen's d=-9028313184615308.0」這種
|
| 212 |
+
# 誇張數字):跟上面 INSUFFICIENT_DATA_LABEL 是兩種不同性質的問題,不要混用
|
| 213 |
+
# ——INSUFFICIENT_DATA_LABEL 代表「p=NaN,檢定本身無法執行」;這個標籤代表
|
| 214 |
+
# 「p 值有效、檢定確實跑出顯著結果,但 Cohen's d/η² 的公式分母(組內變異)
|
| 215 |
+
# 趨近於零,除出來的數字在數值上不穩定、失真」。兩組數值幾乎沒有組內變異卻
|
| 216 |
+
# 彼此不同,是統計上最乾淨俐落的一種顯著差異,不該被當成「檢定失敗」處理,
|
| 217 |
+
# 顯著性判斷(p 值)仍然有效、正常參與統計比例;只有效果量這個數字本身
|
| 218 |
+
# 換成這個描述性標籤,不強行印出失真的天文數字。
|
| 219 |
+
EFFECT_UNSTABLE_LABEL = "效果量不穩定(組內變異趨近於零)"
|
| 220 |
+
|
| 221 |
+
# 2026-08-24(業主回報:AG_05 在 MT 組標準差為 0,Welch t 檢定仍算出
|
| 222 |
+
# p=.047 並被 LLM 列為顯著發現,但這個結果不可靠):這是跟上面兩種都不同
|
| 223 |
+
# 的第三種退化情況——Welch t 檢定公式是 t = (mean1-mean2) / sqrt(var1/n1 +
|
| 224 |
+
# var2/n2),只要「其中一組」變異數不是零,分母就不會退化成 0 或 NaN,
|
| 225 |
+
# 檢定照樣算得出一個數值上正常、但統計上不可靠的 p 值;INSUFFICIENT_DATA_
|
| 226 |
+
# LABEL(p=NaN)跟 EFFECT_UNSTABLE_LABEL(兩組都幾乎零變異、Cohen's d 分母
|
| 227 |
+
# 趨近於零)都攔不到這種「只有一組零變異」的情況。這裡不動指標計算方式
|
| 228 |
+
# 本身(AG_05 算出來是 0 就是事實),只在檢定這一層加警示標記——p 值與
|
| 229 |
+
# 效果量照常計算、正常參與顯著性統計,只是額外附註「這個結果的統計穩定性
|
| 230 |
+
# 存疑」,供人與 LLM 判讀時謹慎看待,不當成跟其他乾淨顯著結果同等份量
|
| 231 |
+
# 的證據。
|
| 232 |
+
ZERO_VARIANCE_WARNING = "⚠️ 其中一組標準差為 0,檢定結果之統計穩定性有限,建議謹慎解讀"
|
| 233 |
+
|
| 234 |
+
# 2026-08-28(業主追問「零膨脹」偵測):上面 ZERO_VARIANCE_WARNING 只抓「整組標準差
|
| 235 |
+
# 趨近於 0」——通常代表該組幾乎全部樣本值相同(多半是全部掛零)。但還有一種較不極端、
|
| 236 |
+
# SD 判準抓不到的退化情況:某組樣本值大量集中在 0,只靠少數幾筆非零值撐出「看起來
|
| 237 |
+
# 正常」的變異數,平均數與檢定結果其實被那幾筆離群值主導,不能代表該組的整體特徵。
|
| 238 |
+
# 這裡另開一條獨立判準:任一組零值比例達到 ZERO_PROPORTION_THRESHOLD(預設
|
| 239 |
+
# 50%,過半數樣本為 0)就額外標記。門檻定 50% ��不是更低(如 20%、30%)的理由:
|
| 240 |
+
# 許多語言指標(如成語、特定詞性頻率)本身就容易有不少檔案掛零,門檻太低會對
|
| 241 |
+
# 「只是資料偏稀疏但正常」的指標大量誤報、稀釋警示的可信度;50%(過半數)才是
|
| 242 |
+
# 「零值已經是這組的主流樣態」的合理起點。刻意只在該組「尚未被 SD 判準抓到」時
|
| 243 |
+
# 才檢查零值比例(見下方偵測邏輯),避免同一組資料被兩條規則重複描述。
|
| 244 |
+
ZERO_PROPORTION_THRESHOLD = 0.5
|
| 245 |
+
ZERO_PROPORTION_WARNING = "⚠️ 其中一組逾半數樣本值為 0,資料集中於零值,檢定結果建議謹慎解讀"
|
| 246 |
+
|
| 247 |
# ── POS 集合(與 Prog02 相同)───────────────────────────────────────────────
|
| 248 |
Vset = ['VC','VH','VE','VJ','VA','V2','VK','VL','VD','VCL','VHC','VB','VF','VI','VAC','VG']
|
| 249 |
Nset = ['Na','Nb','Nc','Ncd','Nd','Nf','Nh','Nv']
|
|
|
|
| 260 |
# ── 研究情境定義 ─────────────────────────────────────────────────────────────
|
| 261 |
SCENARIOS = {
|
| 262 |
"1": {
|
| 263 |
+
"title": "人工翻譯 vs 機器翻譯",
|
| 264 |
"short": "人機對比",
|
| 265 |
"upload_prompt": (
|
| 266 |
"1. 檔案格式:僅接受 **.txt** 純文字檔。\n"
|
|
|
|
| 296 |
"scenario_module_key": "scenario_4",
|
| 297 |
},
|
| 298 |
"4": {
|
| 299 |
+
"title": "不同時期人工譯本",
|
| 300 |
"short": "歷時重譯",
|
| 301 |
"upload_prompt": (
|
| 302 |
"1. 檔案格式:僅接受 **.txt** 純文字檔。\n"
|
|
|
|
| 391 |
# 之後隱藏清單增減,這裡跟著自動變動,不用每次巡一遍程式碼改字面數字。
|
| 392 |
TOTAL_VISIBLE_INDICATORS = len(INDEX_NAME_MAP) - len(HIDDEN_INDICATOR_CODES)
|
| 393 |
|
| 394 |
+
# 2026-08-24(業主提供 system_prompts_v1.3_0529.md 設計稿,要求補回原始設計
|
| 395 |
+
# 裡有、但現行實作缺少的內容):v1.3 在 system prompt 裡內嵌了一份完整的
|
| 396 |
+
# 指標代碼對照表,讓 LLM 有明確依據判斷每個代碼的意思,並規定「未列於
|
| 397 |
+
# 對照表的代碼視為未定義指標」(見嚴格禁令第 10 條)。這裡不手動謄寫一份
|
| 398 |
+
# 靜態文字(那樣容易跟 INDEX_CATEGORIES 各自走鐘、兩份對不起來),改成
|
| 399 |
+
# 直接從 INDEX_CATEGORIES 動態產生,且比照 HIDDEN_INDICATOR_CODES 的規則
|
| 400 |
+
# 排除隱藏指標——LLM 實際收到的 JSON 資料本來就已經濾掉這 13 項,對照表
|
| 401 |
+
# 若照樣列出它們,LLM 反而會誤以為資料裡漏了這些代碼、去猜測或杜撰。
|
| 402 |
+
def _build_indicator_reference_table() -> str:
|
| 403 |
+
lines = []
|
| 404 |
+
for cat, items in INDEX_CATEGORIES.items():
|
| 405 |
+
visible = {c: n for c, n in items.items() if c not in HIDDEN_INDICATOR_CODES}
|
| 406 |
+
if not visible:
|
| 407 |
+
continue
|
| 408 |
+
lines.append(f"## {cat}")
|
| 409 |
+
for code, name in visible.items():
|
| 410 |
+
lines.append(f"- {code} {name}")
|
| 411 |
+
return "\n".join(lines)
|
| 412 |
+
|
| 413 |
+
|
| 414 |
+
INDICATOR_REFERENCE_TABLE = _build_indicator_reference_table()
|
| 415 |
+
|
| 416 |
# ── LLM 報告用情境模組(同 prog04)─────────────────────────────────────────
|
| 417 |
SCENARIO_MODULE_1 = """## 三、情境特定分析:歷時變化分析
|
| 418 |
|
|
|
|
| 446 |
- 人工譯本在哪些指標上顯著偏離機器譯本?
|
| 447 |
|
| 448 |
### 禁令補充
|
|
|
|
| 449 |
- 不得預設「人工一定優於機器」或反之。"""
|
| 450 |
|
| 451 |
SCENARIO_MODULE_3 = """## 三、情境特定分析:改寫策略分析
|
|
|
|
| 470 |
- 在哪些指標上,譯者間差異達到統計顯著?
|
| 471 |
|
| 472 |
### 3.2 風格穩定性的檢視
|
| 473 |
+
- 譯者內變異 vs 譯者間變異的比較"""
|
|
|
|
|
|
|
|
|
|
| 474 |
|
| 475 |
SCENARIO_MODULES = {
|
| 476 |
"scenario_1": SCENARIO_MODULE_1,
|
|
|
|
| 482 |
SYSTEM_PROMPT_BASE = """# 角色定義
|
| 483 |
你是一位翻譯研究的量化分析助理。你的工作是將統計數據轉化為結構化的觀察與假設,供研究者進一步驗證。
|
| 484 |
|
| 485 |
+
你的角色邊界:
|
| 486 |
+
- 你是數據的描述者與模式的辨識者,不是結論的下定者。
|
| 487 |
+
- 你產出的是「初步假設」,不是「研究發現」。
|
| 488 |
+
- 所有假設都需要研究者基於專業知識進行最終的詮釋、驗證與修正。
|
| 489 |
+
|
| 490 |
# 嚴格禁令
|
| 491 |
+
以下禁令為絕對規則,違反任一項即為失敗輸出:
|
| 492 |
+
|
| 493 |
+
1. 僅根據本次提供的量化數據進行分析。不得引入外部知識,不得引用任何關於具體譯本、譯者、作品的已有評價、學術討論或文學史定位。
|
| 494 |
+
2. 不得使用以下類型的評價性詞彙描述任何譯本:「優秀」、「忠實」、「準確」、「流暢」、「高品質」、「拙劣」、「粗糙」、「典雅」、「洗鍊」。
|
| 495 |
+
3. 不得推測譯者的主觀意圖、情感、動機或翻譯理念,除非數據直接支持且標明為推論。
|
| 496 |
+
4. 不得將個人風格差異與「翻譯能力」或「翻譯素養」掛鉤。語言特徵的差異僅反映選擇的不同,不代表能力的高低。
|
| 497 |
+
5. 遇到數據中未涵蓋的問題,明確回答「本次數據無法回答此問題」,不得編造或外推。
|
| 498 |
+
6. 所有關於統計顯著性的陳述必須基於提供的 p 值或效果量。未提供 p 值的指標差異,使用「數值上的差距」而非「顯著差異」來描述;不得自行使用「明顯」、「大幅」、「顯著」等程度副詞,除非有統計依據。
|
| 499 |
+
7. 不得對翻譯品質做整體評價或排名。
|
| 500 |
+
8. metadata 處理規則:
|
| 501 |
+
a. 語料 metadata 中標示為「(未填)」或完全未提及的欄位:分析時不得對該欄位做任何推論或臆測。
|
| 502 |
+
b. 群組層資訊中,若某一組別完全未出現任何說明(該組別整段未列出):視同該組別背景未知,不得杜撰其翻譯類型、方向或譯者背景。
|
| 503 |
+
c. 個體層資訊中,若某一檔案完全未列出對應說明:視同該檔案背景未知,不得杜撰其譯者、出版年等資訊。
|
| 504 |
+
d. 群組層資訊中所有組別皆未填寫「群組標籤」:在「一、語料說明」中明確標記「群組標籤未提供,以下分析僅基於數值差異,不涉及群組身份的討論」。
|
| 505 |
+
e. 使用者於語料 Metadata 步驟選擇略過:僅陳述「使用者未提供語料補充說明」,不得臆測或杜撰任何背景內容。
|
| 506 |
+
9. metadata 僅用於框定分析範圍與數據結構關係,不得用於構建敘事性的譯者畫像或歷史背景描述。
|
| 507 |
+
10. 未定義指標處理規則:若量化分析結果中出現未列於下方「指標代碼對照表」的指標代碼,不得猜測其含義、不得將其納入任何分析步驟,須在「四、待驗證事項」中標記為「未定義指標:{{代碼}}」。
|
| 508 |
+
11. 跨指標邏輯連結許可:允許連結多個指標的結果進行邏輯推論,但須滿足:
|
| 509 |
+
a. 所連結的每個指標均有明確的數據支持(數值或統計檢定結果)。
|
| 510 |
+
b. 連結的邏輯基礎來自下方指標代碼對照表中的定義關係,不得憑空聯想。
|
| 511 |
+
c. 必須在陳述中明確標示「此為跨指標推論」,以區分直接的數據觀察。
|
| 512 |
+
d. 不得基於外部知識進行跨指標連結。
|
| 513 |
+
|
| 514 |
+
# 指標代碼對照表
|
| 515 |
+
以下對照表僅供符號辨識用,不代表任何詮釋框架。未列於此對照表的指標代碼依嚴格禁令第 10 條處理。
|
| 516 |
+
|
| 517 |
+
{indicator_reference_table}
|
| 518 |
|
| 519 |
# 輸入資料結構
|
| 520 |
【研究情境】:{scenario_type}
|
|
|
|
| 538 |
## 一、語料說明
|
| 539 |
用 2-4 句話摘要上方【譯本 metadata】提供的原文資訊、群組層與個體層背景(例如原文標題、各組別的翻譯類型/方向、個別譯本的譯者或系統名稱等,視實際提供的內容而定)。
|
| 540 |
若 metadata 顯示使用者略過未填,僅陳述「使用者未提供語料補充說明」,不得臆測或杜撰內容。這個區塊只做摘要陳述,不做任何統計分析或推論。
|
| 541 |
+
【metadata 一致性檢查】摘要時若發現群組層與個體層資訊彼此矛盾(例如某組別標示為「人工翻譯」,但該組底下某檔案的譯者/系統名稱明顯是 AI 模型或翻譯系統名稱;或研究情境選的是歷時比較,但個體層完全沒有任何出版年資訊),須在本區塊結尾明確標記該矛盾之處,供使用者確認,**不得自行判斷何者為準、不得擅自修正或忽略其中一方**。若沒有發現矛盾,不需要特別聲明「一致性檢查通過」,維持原本的摘要陳述即可。
|
| 542 |
|
| 543 |
## 二、數據事實描述
|
| 544 |
本區塊須包含以下三類數據事實,缺一不可(量化分析結果 JSON 裡都有對應資料,不得因為篇幅省略):
|
| 545 |
+
1. 差異性分析:每項陳述除了指標代碼、p 值與效果量之外,**必須明確指出各組別中平均數偏高與
|
| 546 |
+
偏低的是哪一組,並附上實際平均數數值**(difference_analysis 資料裡每個組別都有一欄以該組
|
| 547 |
名稱結尾 _M 的平均數欄位)——只寫 p 值與效果量、不寫是哪一組偏高偏低,讀者無法理解差異的
|
| 548 |
方向與意義。
|
| 549 |
三組以上時是用 ANOVA 檢定,效果量(η²)本身不帶方向性(不像兩組時的 Cohen's d 還有正負號
|
| 550 |
+
可看出方向),更不能省略指出方向的那句話。
|
| 551 |
+
【用詞規則,務必遵守】JSON 裡的 groups/difference_analysis 只會有 2 或 3 個以上的組別,
|
| 552 |
+
依組別數量決定用詞,不可混用:
|
| 553 |
+
- **恰好兩組**:只能用「較高」「較低」,不可以用「最高」「最低」——只有兩個東西比較時,
|
| 554 |
+
沒有「最」這種在三者以上才成立的最高級語意,用「最高」「最低」邏輯上是錯的。
|
| 555 |
+
範例:「總字數(AA_01):p=0.0461,Cohen's d=-1.5621(大);機器翻譯較高(M=60537.5),
|
| 556 |
+
人工翻譯較低(M=54313.6)。」
|
| 557 |
+
- **三組以上**:用「最高」「最低」(是在多個組別中挑出最大最小值,最高級語意才成立)。
|
| 558 |
+
範例:「總句數(AA_06):p=0.0068,η²=0.9932(大);2024 年組別最高(M=32.5),
|
| 559 |
+
1989 年組別最低(M=18.2)。」
|
| 560 |
+
區分:達統計顯著的差異(標明 p 值、效果量、方向與平均數)、未達顯著但數值差距較大的趨勢
|
| 561 |
+
(同樣要指出方向)、無明顯差異的指標。
|
| 562 |
【資料完整性】若 JSON 附有「資料完整性提醒」欄位(表示部分指標因組別樣本數不足、根本無法
|
| 563 |
進行統計檢定),這些指標一律只能陳述為「資料不足,無法檢定」,絕對不可以寫成「無顯著差異」,
|
| 564 |
也不可以把它們計入「達統計顯著」或「無明顯差異」的統計比例——這是兩種完全不同的結論,混為
|
| 565 |
一談會誤導讀者以為系統已經檢定過、確認沒有差異。
|
| 566 |
+
【效果量無法計算】若某指標的「效果大小」欄位顯示「效果量不穩定(組內變異趨近於零)」、
|
| 567 |
+
「效果量」欄位為 null,代表 p 值仍然有效、該指標**確實達統計顯著**(顯著性判斷正常參與統計
|
| 568 |
+
比例),只是組內變異趨近於零,Cohen's d/η² 這個公式本身除不出有意義的數字。陳述時正常
|
| 569 |
+
引用 p 值與各組平均數(依上面【用詞規則】,兩組寫「較高/較低」、三組以上寫「最高/最低」),
|
| 570 |
+
效果量部分改寫成「效果量因組內變異趨近於零而無法計算」,不得省略、也不得自己編造一個數字
|
| 571 |
+
或寫成「效果量極大」(那是失真的浮點數,不是真的統計結論)。這跟【資料完整性】的「資料
|
| 572 |
+
不足」是兩種不同情況,不要混為一談:後者連檢定都做不了,前者是檢定做了、結果確實顯著,
|
| 573 |
+
只是效果量這個數字算不出來。
|
| 574 |
+
【資料穩定性提醒】若 JSON 附有「資料穩定性提醒」欄位(表示部分指標至少有一組出現「樣本
|
| 575 |
+
標準差趨近於 0」或「半數以上樣本值為 0」之一),這些指標的 p 值與效果量正常計算、正常算入
|
| 576 |
+
「達統計顯著」的統計比例,**不要**排除或改標成「資料不足」;但陳述時必須額外加一句保留
|
| 577 |
+
語氣,例如「此結果統計穩定性有限,建議謹慎解讀」,不可用跟其他變異正常的顯著指標完全相同
|
| 578 |
+
的肯定語氣呈現,避免讀者把統計上脆弱的單組結果誤判為跟其他證據同等可信。
|
| 579 |
2. 監督式學習(隨機森林):說明 feature_importance_top20 裡重要性最高的 3-5 項指標與其重要性數值。
|
| 580 |
同時陳述「隨機森林分類準確率」的數字,並原樣附上 JSON 裡「隨機森林準確率之樣本外預測意義」
|
| 581 |
這欄給的完整句子(那句話已經是可以直接使用的中文敘述,不必再改寫或省略)。
|
|
|
|
| 1100 |
# ============================================================================
|
| 1101 |
|
| 1102 |
def cohens_d(g1, g2):
|
| 1103 |
+
"""回傳 Cohen's d;合併標準差相對於資料尺度趨近於零時回傳 None(而非硬除
|
| 1104 |
+
出失真的天文數字)。
|
| 1105 |
+
|
| 1106 |
+
2026-08-24(業主回報:報告上出現「Cohen's d=-9028313184615308.0」這種
|
| 1107 |
+
誇張數字):原本只用 `pooled != 0` 擋分母為零,但當兩組內部數值幾乎(並非
|
| 1108 |
+
精確)相同時,浮點運算會讓 pooled 變成一個極小但非零的數(例如 1e-13),
|
| 1109 |
+
這個檢查完全擋不住,除下去就爆出巨大到毫無意義的數字。改用「相對於資料
|
| 1110 |
+
尺度的容忍值」判斷:pooled 小於合併平均數絕對值的極小比例時,視為組內
|
| 1111 |
+
變異趨近於零、效果量在數值上不穩定,回傳 None。注意:這不代表兩組沒有
|
| 1112 |
+
差異——p 值與平均數本身仍然有效,只是 Cohen's d 這個統計量的公式在
|
| 1113 |
+
「組內幾乎零變異」的情況下本身就定義不良,硬算出一個數字反而失真。"""
|
| 1114 |
n1, n2 = len(g1), len(g2)
|
| 1115 |
if n1 < 2 or n2 < 2:
|
| 1116 |
+
return None
|
| 1117 |
var1, var2 = np.var(g1, ddof=1), np.var(g2, ddof=1)
|
| 1118 |
pooled = np.sqrt(((n1-1)*var1 + (n2-1)*var2) / (n1+n2-2))
|
| 1119 |
+
scale = max(abs(np.mean(np.concatenate([g1, g2]))), 1e-12)
|
| 1120 |
+
if pooled < scale * 1e-8:
|
| 1121 |
+
return None
|
| 1122 |
+
return (np.mean(g1) - np.mean(g2)) / pooled
|
| 1123 |
|
| 1124 |
|
| 1125 |
def eta_squared(group_data):
|
| 1126 |
+
"""回傳 η²;ss_total 相對於資料尺度趨近於零時回傳 None,理由同 cohens_d()。"""
|
| 1127 |
all_data = np.concatenate(group_data)
|
| 1128 |
grand_mean = all_data.mean()
|
| 1129 |
ss_between = sum(len(g) * (np.mean(g) - grand_mean)**2 for g in group_data)
|
| 1130 |
ss_total = np.sum((all_data - grand_mean)**2)
|
| 1131 |
+
scale = max(abs(grand_mean), 1e-12)
|
| 1132 |
+
if ss_total < (scale ** 2) * 1e-8:
|
| 1133 |
+
return None
|
| 1134 |
+
return ss_between / ss_total
|
| 1135 |
|
| 1136 |
|
| 1137 |
def effect_label(val, method):
|
|
|
|
| 1264 |
row["顯著"] = INSUFFICIENT_DATA_LABEL
|
| 1265 |
row["效果量"] = None
|
| 1266 |
row["效果大小"] = None
|
| 1267 |
+
row["資料品質警示"] = "" # 已經整列標成「資料不足」,不用再疊加這個警示
|
| 1268 |
else:
|
| 1269 |
es = cohens_d(gdata[0], gdata[1]) if n_groups == 2 else eta_squared(gdata)
|
| 1270 |
+
row["顯著"] = ("***" if p<0.001 else "**" if p<0.01 else "*" if p<0.05 else "ns")
|
| 1271 |
+
if es is None:
|
| 1272 |
+
# 見 EFFECT_UNSTABLE_LABEL 定義處:p 值仍然有效、正常判斷顯著性
|
| 1273 |
+
# (上面那行不受影響),只有效果量這個數字換成描述性標籤。
|
| 1274 |
+
row["效果量"] = None
|
| 1275 |
+
row["效果大小"] = EFFECT_UNSTABLE_LABEL
|
| 1276 |
+
else:
|
| 1277 |
+
row["效果量"] = round(float(es), 4)
|
| 1278 |
+
row["效果大小"] = effect_label(es, method)
|
| 1279 |
+
# 見 ZERO_VARIANCE_WARNING/ZERO_PROPORTION_WARNING 定義處:兩條獨立
|
| 1280 |
+
# 判準都只看「有真的算出變異數」的情況——N<2 已經在 is_untestable 那支
|
| 1281 |
+
# 處理過。SD 判準抓「整組幾乎沒變異」;零值比例判準抓「SD 沒有趨近於
|
| 1282 |
+
# 零,但組內樣本值仍大量集中在 0」——後者刻意排除已被 SD 判準抓到的
|
| 1283 |
+
# 組別(`not (... < 1e-9)`),避免同一組資料被兩條規則重複疊加描述。
|
| 1284 |
+
# 都不影響顯著性與效果量的計算,純附註警示;兩者可同時列出(不同組別
|
| 1285 |
+
# 各自命中不同判準時)。
|
| 1286 |
+
_dq_warnings = []
|
| 1287 |
+
if any(len(g) >= 2 and np.std(g, ddof=1) < 1e-9 for g in gdata):
|
| 1288 |
+
_dq_warnings.append(ZERO_VARIANCE_WARNING)
|
| 1289 |
+
if any(len(g) >= 2 and np.std(g, ddof=1) >= 1e-9
|
| 1290 |
+
and (np.sum(g == 0) / len(g)) >= ZERO_PROPORTION_THRESHOLD
|
| 1291 |
+
for g in gdata):
|
| 1292 |
+
_dq_warnings.append(ZERO_PROPORTION_WARNING)
|
| 1293 |
+
row["資料品質警示"] = ";".join(_dq_warnings)
|
| 1294 |
diff_results.append(row)
|
| 1295 |
|
| 1296 |
diff_df = pd.DataFrame(diff_results)
|
|
|
|
| 1299 |
diff_path = output_dir / "02_difference_analysis.csv"
|
| 1300 |
diff_df.to_csv(str(diff_path), index=False, encoding='utf-8-sig')
|
| 1301 |
|
| 1302 |
+
# boxplot(2026-08-24 起:達顯著差異的指標全部畫出,不再只取前 12 項)
|
| 1303 |
# 2026-08-14(業主要求):原本取前 12 項是照指標在資料表裡的固定欄位
|
| 1304 |
# 順序(基礎統計→...→詞性標記)往下數,不是「最顯著/效果量最大」的
|
| 1305 |
+
# 12 項,改成依效果量排序。
|
| 1306 |
+
# 2026-08-24(業主要求):取消「只取前 12 項」的上限,達顯著差異的指標
|
| 1307 |
+
# 全部畫出來;排序改成「先依組別、組��再依效果量」——先看哪一組在這批
|
| 1308 |
+
# 顯著指標裡整體偏高,同一組內再依效果量絕對值由大到小排,比單純照
|
| 1309 |
+
# 效果量從大到小混排更容易看出「兩組的差異主要往哪個方向集中」。
|
| 1310 |
box_path = None
|
| 1311 |
if len(sig_df) > 0:
|
| 1312 |
+
sig_df_sorted = sig_df.copy()
|
| 1313 |
+
_group_order = {g: i for i, g in enumerate(groups)}
|
| 1314 |
+
sig_df_sorted['_highest_group'] = sig_df_sorted.apply(
|
| 1315 |
+
lambda row: max(groups, key=lambda g: row.get(f"{g}_M", float('-inf'))),
|
| 1316 |
+
axis=1,
|
| 1317 |
+
)
|
| 1318 |
+
sig_df_sorted['_group_rank'] = sig_df_sorted['_highest_group'].map(_group_order)
|
| 1319 |
+
# 2026-08-24:效果量為 None(EFFECT_UNSTABLE_LABEL 情況,見該常數定義
|
| 1320 |
+
# 處)代表組內幾乎零變異卻仍達顯著——是最乾淨俐落的一種差異,排序時
|
| 1321 |
+
# 視為效果量無限大、排在同一組別群組的最前面,不能因為是 None 就被
|
| 1322 |
+
# sort_values 排到最後面去(NaN 預設排序行為)。
|
| 1323 |
+
# 2026-08-24(實測崩潰:TypeError: bad operand type for abs(): 'NoneType'):
|
| 1324 |
+
# 當顯著指標裡有不少(甚至全部)是 EFFECT_UNSTABLE_LABEL 情況(效果量=
|
| 1325 |
+
# None),這欄在 pandas 眼裡可能整欄維持 object dtype(而不是像單一
|
| 1326 |
+
# 摻雜幾個 None 的情況那樣被自動轉成 float64、None 變 NaN),直接對
|
| 1327 |
+
# object dtype 呼叫 .abs() 會真的去對 Python None 物件呼叫內建 abs()
|
| 1328 |
+
# 而炸掉,不是 NaN 安全語意。先用 pd.to_numeric(errors='coerce') 強制
|
| 1329 |
+
# 轉成數值(None、NaN 都會變成 NaN,非數值也不會拋例外),再 .abs()
|
| 1330 |
+
# 就穩了。
|
| 1331 |
+
sig_df_sorted['_abs_es'] = pd.to_numeric(sig_df_sorted['效果量'], errors='coerce').abs().fillna(float('inf'))
|
| 1332 |
+
sig_df_sorted = sig_df_sorted.sort_values(
|
| 1333 |
+
by=['_group_rank', '_abs_es'], ascending=[True, False]
|
| 1334 |
)
|
| 1335 |
+
sig_cols = sig_df_sorted['指標'].tolist()
|
| 1336 |
n_row = max(1, (len(sig_cols) + 2) // 3)
|
| 1337 |
fig, axes = plt.subplots(n_row, 3, figsize=(14, 4*n_row))
|
| 1338 |
axes = np.array(axes).flatten()
|
|
|
|
| 1353 |
# 「有沒有差」。
|
| 1354 |
row = diff_df.loc[diff_df['指標']==col]
|
| 1355 |
pv_str = f"p={row['p值'].values[0]:.4f}" if len(row)>0 else ""
|
| 1356 |
+
_es_val = row['效果量'].values[0] if len(row)>0 else None
|
| 1357 |
+
if len(row) > 0 and _es_val is not None and not pd.isna(_es_val):
|
| 1358 |
+
es_str = f",{row['效果量名稱'].values[0]}={_es_val:.3f}"
|
| 1359 |
+
elif len(row) > 0:
|
| 1360 |
+
# 2026-08-24:效果量為 None 時(見 EFFECT_UNSTABLE_LABEL),
|
| 1361 |
+
# 標題別再硬套 :.3f 格式化(會直接拋例外),改顯示描述性標籤。
|
| 1362 |
+
es_str = f",{EFFECT_UNSTABLE_LABEL}"
|
| 1363 |
+
else:
|
| 1364 |
+
es_str = ""
|
| 1365 |
ax.set_title(f"{col}\n({pv_str}{es_str})", fontsize=8)
|
| 1366 |
for j in range(len(sig_cols), len(axes)):
|
| 1367 |
axes[j].set_visible(False)
|
| 1368 |
# 2026-08-14(業主要求):原本「Significant Variables by Type1」直接
|
| 1369 |
+
# 洩漏內部欄位名稱 Type1,使用者看不懂在比較什麼,改成中文標題。
|
| 1370 |
+
# 2026-08-24(業主要求):不再只取前 12 項,標題文字同步更新為
|
| 1371 |
+
# 「依組別與效果量排序」,不再提「前 12 項」(已無此上限)。
|
| 1372 |
+
plt.suptitle("顯著指標比較(依組別與效果量排序)", fontsize=11, y=1.01)
|
| 1373 |
plt.tight_layout()
|
| 1374 |
box_path = output_dir / "02_significant_boxplots.png"
|
| 1375 |
plt.savefig(str(box_path), dpi=150, bbox_inches='tight')
|
|
|
|
| 1540 |
f"{row['平均數']:.4f}", f"{row['中位數']:.4f}",
|
| 1541 |
f"{row['標準差']:.4f}", f"{row['最小值']:.4f}",
|
| 1542 |
f"{row['最大值']:.4f}",
|
| 1543 |
+
# 同下方差異性分析表格:改用 pd.isna(),避免 None 被
|
| 1544 |
+
# pandas 建表時轉型成 NaN 後,is not None 抓不到而印出「nan」。
|
| 1545 |
+
f"{row['變異係數(%)']:.2f}" if not pd.isna(row['變異係數(%)']) else "-"]
|
| 1546 |
for j, val in enumerate(vals):
|
| 1547 |
cell = tbl.rows[i].cells[j]
|
| 1548 |
cell.text = val
|
|
|
|
| 1569 |
group_sub_headers = []
|
| 1570 |
for g in groups:
|
| 1571 |
group_sub_headers.extend([f"{g}\nN", f"{g}\nM", f"{g}\nSD"])
|
| 1572 |
+
diff_headers = ["指標"] + group_sub_headers + ["方法","統計量","p值","顯著","效果量","效果大小","資料品質警示"]
|
| 1573 |
diff_tbl = doc.add_table(rows=1+len(diff_df), cols=len(diff_headers))
|
| 1574 |
diff_tbl.style = 'Light Shading Accent 1'
|
| 1575 |
for j, h in enumerate(diff_headers):
|
|
|
|
| 1584 |
vals.extend([str(int(row[f"{g}_N"])),
|
| 1585 |
f"{row[f'{g}_M']:.4f}",
|
| 1586 |
f"{row[f'{g}_SD']:.4f}"])
|
| 1587 |
+
# 2026-08-24(業主回報表格出現「nan」字樣):diff_results 是一份
|
| 1588 |
+
# dict list,其中某些列的「統計量」/「p值」/「效果量」是 Python
|
| 1589 |
+
# None(見 INSUFFICIENT_DATA_LABEL/EFFECT_UNSTABLE_LABEL 兩處),
|
| 1590 |
+
# 但 `pd.DataFrame(diff_results)` 建表時,只要同一欄裡其餘列是數字,
|
| 1591 |
+
# pandas 會把整欄轉成 float64、None 也跟著被轉型成 NaN——`is not
|
| 1592 |
+
# None` 檢查完全抓不到轉型後的 NaN(NaN 不是 None 物件),因此原本
|
| 1593 |
+
# 該顯示「-」的欄位印出了「nan」這個字面字串。改用 pd.isna(),能
|
| 1594 |
+
# 同時抓到 None 與轉型後的 NaN 兩種情況。
|
| 1595 |
+
_warn_text = row.get('資料品質警示', '')
|
| 1596 |
+
_warn_text = '' if pd.isna(_warn_text) else str(_warn_text)
|
| 1597 |
vals.extend([str(row['方法']),
|
| 1598 |
+
f"{row['統計量']:.4f}" if not pd.isna(row['統計量']) else "-",
|
| 1599 |
+
f"{row['p值']:.4f}" if not pd.isna(row['p值']) else "-",
|
| 1600 |
str(row['顯著']),
|
| 1601 |
+
f"{row['效果量']:.4f}" if not pd.isna(row['效果量']) else "-",
|
| 1602 |
+
str(row['效果大小']) if not pd.isna(row['效果大小']) else "-",
|
| 1603 |
+
_warn_text if _warn_text else "-"])
|
| 1604 |
_is_untestable_row = row['顯著'] == INSUFFICIENT_DATA_LABEL
|
| 1605 |
+
# 2026-08-24:改用欄名查表格索引,不再用 len(vals) 往回數固定格數
|
| 1606 |
+
# ——原本 len(vals)-4 在只有 6 個尾端欄位時其實已經對不準「顯著」
|
| 1607 |
+
# 那一格(差一格,實際指到 p值),這次多加一欄「資料品質警示」後
|
| 1608 |
+
# 用位置數只會更容易錯位,藉這次一起修成用欄名查,之後再加欄也不怕。
|
| 1609 |
+
_sig_col_idx = diff_headers.index('顯著')
|
| 1610 |
+
_warn_col_idx = diff_headers.index('資料品質警示')
|
| 1611 |
for j, val in enumerate(vals):
|
| 1612 |
cell = diff_tbl.rows[i].cells[j]
|
| 1613 |
cell.text = val
|
|
|
|
| 1619 |
# 不能共用紅字——紅字在這份報告的視覺語言裡代表「這是
|
| 1620 |
# 值得注意的發現」,「資料不足」剛好相反,是「這項不能
|
| 1621 |
# 拿來當發現看」,用紅字反而會讓人誤以為它也是顯著結果。
|
| 1622 |
+
if row['顯著'] not in ("ns", INSUFFICIENT_DATA_LABEL) and j == _sig_col_idx:
|
| 1623 |
r.font.color.rgb = RGBColor(0xCC, 0x00, 0x00)
|
| 1624 |
elif _is_untestable_row:
|
| 1625 |
r.font.color.rgb = RGBColor(0x88, 0x88, 0x88)
|
| 1626 |
r.italic = True
|
| 1627 |
+
# 見 ZERO_VARIANCE_WARNING:琥珀色,跟「顯著」的紅、
|
| 1628 |
+
# 「資料不足」的灰都不同——這不是「無法使用」(灰)也不是
|
| 1629 |
+
# 「重點發現」(紅),是「可用但要謹慎看待」的第三種性質。
|
| 1630 |
+
elif j == _warn_col_idx and _warn_text:
|
| 1631 |
+
r.font.color.rgb = RGBColor(0xB4, 0x7A, 0x00)
|
| 1632 |
doc.add_paragraph('')
|
| 1633 |
if box_path and box_path.exists():
|
| 1634 |
doc.add_heading('顯著指標箱形圖', level=2)
|
|
|
|
| 1690 |
# SECTION 7: LLM Report Writing (adapted from prog04, uses LLM1)
|
| 1691 |
# ============================================================================
|
| 1692 |
|
| 1693 |
+
def load_analysis_data_for_llm(csv_path: Path, output_dir: Path, ml_summary: dict = None,
|
| 1694 |
+
group_labels: dict = None) -> dict:
|
| 1695 |
"""讀取分析資料組成 JSON 結構。
|
| 1696 |
ml_summary:{"accuracy": float, "do_eval": bool} 選填,來自
|
| 1697 |
run_statistical_analysis() 的回傳值。之前這裡完全沒有把隨機森林的
|
| 1698 |
準確率數字(accuracy)跟「這次是不是真的切了測試集」(do_eval)
|
| 1699 |
交給 LLM,LLM 因此連準確率是多少都不知道,等於沒有材料可以討論
|
| 1700 |
監督式學習結果——03_feature_importance.csv 只有各指標的重要性排名,
|
| 1701 |
+
沒有整體準確率這個數字。
|
| 1702 |
+
group_labels:{分組代碼: 語料說明填的實際標籤},選填。2026-08-24(業主
|
| 1703 |
+
回報):畫面上的差異性分析標題/箱形圖標籤早就會用語料說明填的實際
|
| 1704 |
+
標籤(如「人工翻譯」),但 LLM 報告內文卻一路只看得到原始代碼 A/B,
|
| 1705 |
+
寫成「群組 A 最高」——追查後發現 difference_analysis 送給 LLM 的 JSON
|
| 1706 |
+
欄名({組別}_N/{組別}_M/{組別}_SD)從頭到尾都是原始代碼,從沒套用過
|
| 1707 |
+
這組標籤,等於畫面跟報告各自套用了不同版本的分組名稱。這裡補上同一套
|
| 1708 |
+
轉換,讓兩邊使用同一個資料來源。"""
|
| 1709 |
data = {}
|
| 1710 |
if ml_summary:
|
| 1711 |
# 2026-08-15(業主回報):原本這裡是英文鍵名 rf_accuracy/
|
|
|
|
| 1741 |
p = output_dir / fname
|
| 1742 |
if p.exists():
|
| 1743 |
d = pd.read_csv(str(p))
|
| 1744 |
+
if key == "difference_analysis" and group_labels:
|
| 1745 |
+
group_codes = [c[:-2] for c in d.columns if c.endswith('_N')]
|
| 1746 |
+
rename_map = {}
|
| 1747 |
+
for g in group_codes:
|
| 1748 |
+
label = group_labels.get(g)
|
| 1749 |
+
if label and label != g:
|
| 1750 |
+
rename_map[f"{g}_N"] = f"{label}_N"
|
| 1751 |
+
rename_map[f"{g}_M"] = f"{label}_M"
|
| 1752 |
+
rename_map[f"{g}_SD"] = f"{label}_SD"
|
| 1753 |
+
if rename_map:
|
| 1754 |
+
d = d.rename(columns=rename_map)
|
| 1755 |
if key == "cluster_results":
|
| 1756 |
# 2026-08-15(業主要求):跟 rf_accuracy_is_meaningful 同一個
|
| 1757 |
# 教訓——原本這裡的欄名是 Type1,LLM 直接照抄,報告寫成
|
|
|
|
| 1790 |
"或納入「達統計顯著」/「無明顯差異」的分類統計,兩者是完全不同的結論。"
|
| 1791 |
if len(untestable) > 0 else ""
|
| 1792 |
)
|
| 1793 |
+
# 2026-08-24(業主回報 AG_05 案例,見 ZERO_VARIANCE_WARNING 定義
|
| 1794 |
+
# 處):「資料品質警示」欄位非空的指標,p 值與效果量都是有效
|
| 1795 |
+
# 計算結果、正常參與顯著性統計,跟上面的「資料不足」是不同
|
| 1796 |
+
# 性質,不能用同一句話說明,另外準備一段中文提醒。
|
| 1797 |
+
if '資料品質警示' in d.columns:
|
| 1798 |
+
zero_var = d[d['資料品質警示'].fillna('') != '']
|
| 1799 |
+
else:
|
| 1800 |
+
zero_var = pd.DataFrame()
|
| 1801 |
+
data["zero_variance_warning_indicators"] = zero_var['指標'].tolist() if len(zero_var) > 0 else []
|
| 1802 |
+
data["zero_variance_warning_count"] = len(zero_var)
|
| 1803 |
+
data["資料穩定性提醒"] = (
|
| 1804 |
+
f"以上 difference_analysis 中有 {len(zero_var)} 項指標的「資料品質警示」欄位非空"
|
| 1805 |
+
"(列於 zero_variance_warning_indicators),代表該指標至少有一組出現以下情況之一:"
|
| 1806 |
+
"①該組所有文本在此指標上數值完全相同,標準差趨近於 0;或 ②該組半數以上文本在"
|
| 1807 |
+
"此指標上數值為 0,樣本集中於零值,平均數可能被少數非零離群值主導。這兩種情況下 "
|
| 1808 |
+
"p 值與效果量仍是正常計算、正常算入「達統計顯著」的統計比例,不像「資料不足」那樣"
|
| 1809 |
+
"要整項排除;但陳述這些指標時,必須額外註明「此結果統計穩定性有限,建議謹慎解讀」"
|
| 1810 |
+
"這類保留語氣,不可跟其他變異正常的顯著指標用完全相同的肯定語氣陳述,避免讀者把"
|
| 1811 |
+
"統計上脆弱的結果當成跟其他乾淨顯著結果同等份量的證據。"
|
| 1812 |
+
if len(zero_var) > 0 else ""
|
| 1813 |
+
)
|
| 1814 |
else:
|
| 1815 |
data[key] = []
|
| 1816 |
if "significant_count" not in data:
|
|
|
|
| 1848 |
|
| 1849 |
|
| 1850 |
def run_report_writing(scenario_key: str, user_metadata: str, user_focus: str,
|
| 1851 |
+
csv_path: Path, output_dir: Path, ml_summary: dict = None,
|
| 1852 |
+
group_labels: dict = None) -> dict:
|
| 1853 |
"""
|
| 1854 |
呼叫主 Agent (LLM1) 撰寫研究報告,儲存 docx + txt,回傳路徑 dict。
|
| 1855 |
ml_summary:見 load_analysis_data_for_llm() 的說明。
|
| 1856 |
+
group_labels:見 load_analysis_data_for_llm() 的說明。
|
| 1857 |
"""
|
| 1858 |
output_dir.mkdir(parents=True, exist_ok=True)
|
| 1859 |
+
analysis_data = load_analysis_data_for_llm(csv_path, output_dir, ml_summary=ml_summary,
|
| 1860 |
+
group_labels=group_labels)
|
| 1861 |
|
| 1862 |
scenario_info = SCENARIOS.get(scenario_key, SCENARIOS["1"])
|
| 1863 |
scenario_mod_key = scenario_info.get("scenario_module_key", "scenario_1")
|
|
|
|
| 1874 |
metadata = metadata_str,
|
| 1875 |
user_focus = user_focus if user_focus else "無",
|
| 1876 |
scenario_module = scenario_module,
|
| 1877 |
+
indicator_reference_table = INDICATOR_REFERENCE_TABLE,
|
| 1878 |
)
|
| 1879 |
+
|
| 1880 |
+
# 2026-08-26(業主要求:可重複性設計的最後一塊——目前報告只印「提示詞
|
| 1881 |
+
# 版本 v1」這個版本號,使用者/論文口委無從查證這個版本實際規定了
|
| 1882 |
+
# 什麼,只能回頭翻原始碼):把這次分析實際套用(.format() 展開後、
|
| 1883 |
+
# 已代入研究情境/metadata/指標對照表的完整版本)的 system prompt
|
| 1884 |
+
# 原文存成一份檔案,跟其他分析產出放在同一個 output_dir,隨「下載
|
| 1885 |
+
# 全部資料」一起打包。直接從程式實際使用的字串存出來,不是另外維護
|
| 1886 |
+
# 一份文件,未來 SYSTEM_PROMPT_BASE 異動也不會有兩邊對不起來的風險。
|
| 1887 |
+
system_prompt_path = output_dir / "06_system_prompt.txt"
|
| 1888 |
+
with open(str(system_prompt_path), 'w', encoding='utf-8') as f:
|
| 1889 |
+
f.write(f"研究情境:{scenario_info['title']}\n")
|
| 1890 |
+
f.write(f"提示詞版本:{PROMPT_VERSION}\n")
|
| 1891 |
+
f.write(f"模型:主 Agent(LLM1){LLM1_MODEL}\n")
|
| 1892 |
+
f.write(
|
| 1893 |
+
"\n本檔案為這次分析實際送給 LLM 的完整 system prompt 原文\n"
|
| 1894 |
+
"(已代入本次的研究情境、語料 metadata 與指標代碼對照表)。\n"
|
| 1895 |
+
)
|
| 1896 |
+
f.write("\n---\n\n")
|
| 1897 |
+
f.write(system_prompt)
|
| 1898 |
user_prompt = (
|
| 1899 |
f"請根據以下量化分析結果,按照 system prompt 中的輸出結構進行深度分析。\n\n"
|
| 1900 |
f"【量化分析資料(JSON)】:\n```json\n"
|
|
|
|
| 1931 |
style='Normal').alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 1932 |
doc.add_paragraph(f'報告時間:{now_tw().strftime("%Y-%m-%d %H:%M:%S")}(台灣時間)',
|
| 1933 |
style='Normal').alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 1934 |
+
# 2026-08-24(業主回報:之前加過的 N/M/SD 圖例只在 Final_Report.docx
|
| 1935 |
+
# 那份技術報告的表格上方,這份 LLM 敘述報告是完全獨立的檔案,內文一樣
|
| 1936 |
+
# 大量使用「M=...」但從沒解釋過,使用者只拿到這份報告的話完全看不懂。
|
| 1937 |
+
# 兩份報告各自獨立、讀者不一定兩份都會看,圖例需要在各自檔案裡都出現,
|
| 1938 |
+
# 不能假設讀者一定也看過另一份。
|
| 1939 |
+
legend_p = doc.add_paragraph(
|
| 1940 |
+
'本報告中 N=樣本數、M=平均數、SD=標準差;p 為統計顯著性之機率值,'
|
| 1941 |
+
'效果量以 Cohen\'s d(二群比較)或 η²(三群以上)表示。'
|
| 1942 |
+
)
|
| 1943 |
+
legend_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 1944 |
+
legend_p.runs[0].italic = True
|
| 1945 |
+
legend_p.runs[0].font.size = Pt(9)
|
| 1946 |
+
# 2026-08-26:提示詞版本號旁邊只有一個代號(v1),讀者查不到內容本身
|
| 1947 |
+
# 具體規定了什麼——這裡指向同一次下載包裡的完整原文檔案。
|
| 1948 |
+
sp_note = doc.add_paragraph(
|
| 1949 |
+
f'本報告據以撰寫的完整 system prompt 原文,請見同一份下載包中的 06_system_prompt.txt。'
|
| 1950 |
+
)
|
| 1951 |
+
sp_note.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
| 1952 |
+
sp_note.runs[0].italic = True
|
| 1953 |
+
sp_note.runs[0].font.size = Pt(9)
|
| 1954 |
doc.add_page_break()
|
| 1955 |
# 2026-08-14(待辦 A1):區塊層級語法(標題/項目符號)沿用原本判斷,
|
| 1956 |
# 差別是段落文字改用 _add_inline_markdown_runs() 逐段解析 **粗體**/
|
|
|
|
| 1974 |
# txt 版之前完全沒寫,這裡補上、順便帶提示詞版本號,兩種格式的
|
| 1975 |
# 稽核資訊要一致,不能只有 docx 有。
|
| 1976 |
f.write(f"模型:主 Agent(LLM1){LLM1_MODEL}(提示詞版本 {PROMPT_VERSION})\n")
|
| 1977 |
+
# 2026-08-24(��主回報 N/M/SD 圖例只加在 Final_Report.docx,這份 LLM
|
| 1978 |
+
# 敘述報告內文一樣大量出現「M=...」卻沒有解釋,兩份報告是獨立檔案、
|
| 1979 |
+
# 讀者不一定都看過,圖例要各自都有):
|
| 1980 |
+
f.write(
|
| 1981 |
+
"本報告中 N=樣本數、M=平均數、SD=標準差;p 為統計顯著性之機率值,"
|
| 1982 |
+
"效果量以 Cohen's d(二群比較)或 η²(三群以上)表示。\n"
|
| 1983 |
+
)
|
| 1984 |
+
# 2026-08-26:同 docx 版,指向同一份下載包裡的完整 system prompt 原文。
|
| 1985 |
+
f.write("本報告據以撰寫的完整 system prompt 原文,請見同一份下載包中的 06_system_prompt.txt。\n")
|
| 1986 |
# 2026-07-27:原本用 "="*60 當純文字分隔線,但緊接在上兩行文字正下方,
|
| 1987 |
# 符合 Markdown Setext 標題語法(文字正上方接一整行 = 會被解讀成 h1
|
| 1988 |
# 底線),導致這兩行 metadata 被渲染成巨大無樣式標題。改成前面留空行
|
|
|
|
| 1992 |
f.write(result_text)
|
| 1993 |
|
| 1994 |
return {
|
| 1995 |
+
"docx_path": docx_path,
|
| 1996 |
+
"txt_path": txt_path,
|
| 1997 |
+
"result_text": result_text,
|
| 1998 |
+
"system_prompt_path": system_prompt_path,
|
| 1999 |
}
|
| 2000 |
|
| 2001 |
|
|
|
|
| 2462 |
csv_path = csv_path,
|
| 2463 |
output_dir = output_dir,
|
| 2464 |
ml_summary = {"accuracy": anal.get("accuracy"), "do_eval": anal.get("do_eval")},
|
| 2465 |
+
group_labels = state.get("group_labels", {}),
|
| 2466 |
)
|
| 2467 |
+
results_paths.extend([str(rpt["docx_path"]), str(rpt["txt_path"]), str(rpt["system_prompt_path"])])
|
| 2468 |
A["report"]["status"] = "done"
|
| 2469 |
A["report"]["line"] = "研究報告已產出"
|
| 2470 |
yield " [OK] 研究報告撰寫完成", False, state
|
|
|
|
| 3333 |
# 邏輯(on_scenario_pick)完全不用改。
|
| 3334 |
sc_btn_1 = gr.Button("1 人工翻譯 vs 機器翻譯", variant="secondary",
|
| 3335 |
elem_id="sc_btn_1", elem_classes=["sc-bridge-btn"])
|
| 3336 |
+
sc_btn_2 = gr.Button("2 全譯 vs 編譯(節譯、改寫)", variant="secondary",
|
| 3337 |
elem_id="sc_btn_2", elem_classes=["sc-bridge-btn"])
|
| 3338 |
+
sc_btn_3 = gr.Button("3 不同譯者的同期譯本", variant="secondary",
|
| 3339 |
elem_id="sc_btn_3", elem_classes=["sc-bridge-btn"])
|
| 3340 |
+
sc_btn_4 = gr.Button("4 不同時期人工譯本", variant="secondary",
|
| 3341 |
elem_id="sc_btn_4", elem_classes=["sc-bridge-btn"])
|
| 3342 |
|
| 3343 |
# Phase 8-E:手機漢堡開關+抽屜背景遮罩。跟 sc_btn_1..4 同一類坑:
|
|
|
|
| 3403 |
reset_confirm_btn = gr.Button("確定回首頁", elem_id="reset_confirm_btn",
|
| 3404 |
scale=1, variant="primary")
|
| 3405 |
|
| 3406 |
+
# 2026-08-26:「關於」面板,跟上面 reset_confirm_row 同一個理由放在
|
| 3407 |
+
# workspace_row 外層(避免被手機版 nav-col 的 transform 抽屜關住),
|
| 3408 |
+
# 沿用同一套 overlay 機制(elem_classes 掛 reset-confirm-overlay 觸發
|
| 3409 |
+
# 既有的置中 + display:none 控制邏輯),面板本身另開較寬的
|
| 3410 |
+
# about-panel class(見 assets/app.css 註解)。
|
| 3411 |
+
with gr.Column(visible=False, elem_id="about_overlay",
|
| 3412 |
+
elem_classes=["reset-confirm-overlay"]) as about_overlay:
|
| 3413 |
+
with gr.Column(elem_id="about_panel", elem_classes=["about-panel"]):
|
| 3414 |
+
gr.Markdown(ABOUT_CONTENT_MD)
|
| 3415 |
+
with gr.Row(elem_classes=["about-close-row"]):
|
| 3416 |
+
about_close_btn = gr.Button("關閉", elem_id="about_close_btn",
|
| 3417 |
+
scale=0, variant="secondary")
|
| 3418 |
+
|
| 3419 |
# Phase 8-B:三欄式工作區——nav-col(窄,導覽/重置)+chat-col
|
| 3420 |
# (對話視窗+輸入框,獨立捲動)+canvas-col(步驟列+上傳/表單/
|
| 3421 |
# 執行紀錄,獨立捲動)。手機寬度見 CSS 的 media query 改上下堆疊。
|
|
|
|
| 3595 |
elem_classes=["result-nav-btn", "result-nav-btn--muted"],
|
| 3596 |
variant="secondary",
|
| 3597 |
)
|
| 3598 |
+
# ── 關於(2026-08-26,業主要求)───────────────────────
|
| 3599 |
+
# 跟使用手冊同��顆待遇:住在 result_nav_col 外面,同一份
|
| 3600 |
+
# elem_classes(沿用 #nav_guide_btn 那組樣式選擇器,見
|
| 3601 |
+
# assets/app.css)。這顆是彈窗不是下載,用一般 gr.Button
|
| 3602 |
+
# 搭配下面的 about_overlay 面板,toggle 顯示/隱藏。
|
| 3603 |
+
about_btn = gr.Button(
|
| 3604 |
+
"關於", icon=str(BASE_DIR / "assets" / "icons" / "info-circle.svg"),
|
| 3605 |
+
elem_id="nav_about_btn",
|
| 3606 |
+
elem_classes=["result-nav-btn", "result-nav-btn--muted"],
|
| 3607 |
+
variant="secondary",
|
| 3608 |
+
)
|
| 3609 |
# ↑ nav_items_scroll 到此結束(中段可捲動區)。
|
| 3610 |
|
| 3611 |
# 底部工具列(固定,不隨中段捲動):明暗切換/收合鈕原本在頂端
|
|
|
|
| 3705 |
# 標題+說明文字沿用 metadata_row 那則 Markdown 的寫法
|
| 3706 |
# (純 "### 標題\n說明文字",不套 elem_classes),維持
|
| 3707 |
# 同一批「Canvas 操作區塊」卡片的既有排版慣例。
|
| 3708 |
+
gr.Markdown("### 情境選擇\n請選擇最符合您針對同一原文的重譯研究設計的比較情境,選定後將進入下一步「文本上傳」。")
|
| 3709 |
gr.HTML(_build_scenario_cards_html("workspace_scenario_grid"))
|
| 3710 |
|
| 3711 |
# Phase 8(千卉實測後回頭調整):這張卡片改回正式使用
|
|
|
|
| 3746 |
|
| 3747 |
# ── 語料 Metadata 說明表單(上傳後、分析前)───────
|
| 3748 |
with gr.Column(visible=False, elem_id="metadata_zone", elem_classes=["metadata-zone"]) as metadata_row:
|
| 3749 |
+
gr.Markdown("### 📑 語料說明\n請於正式分析前說明語料來源、範圍與比較結構,將寫入研究報告。")
|
| 3750 |
|
| 3751 |
gr.Markdown("**原文資訊**")
|
| 3752 |
with gr.Row():
|
|
|
|
| 4080 |
"無法進行統計檢定,不計入上述比例。"
|
| 4081 |
if untestable_n > 0 else ""
|
| 4082 |
)
|
| 4083 |
+
# 見 ZERO_VARIANCE_WARNING/ZERO_PROPORTION_WARNING 定義處:這些
|
| 4084 |
+
# 指標仍計入顯著/不顯著比例(p 值照常有效),只是額外提醒統計
|
| 4085 |
+
# 穩定性存疑,跟上面「資料不足」(完全無法檢定、不計入比例)是
|
| 4086 |
+
# 不同性質,故用不同語氣、且不寫「不計入上述比例」。這裡的句子
|
| 4087 |
+
# 刻意不細分是哪一條判準命中——個別是哪一種,下方表格會逐列標註。
|
| 4088 |
+
warn_n = int((diff.get('資料品質警示', pd.Series(dtype=str)).fillna('') != '').sum())
|
| 4089 |
+
zero_var_note = (
|
| 4090 |
+
f"\n\n⚠️ 其中 **{warn_n}** 個顯著指標的資料品質有疑慮"
|
| 4091 |
+
"(某組標準差趨近於 0,或某組半數以上樣本為 0),"
|
| 4092 |
+
"統計穩定性有限,建議謹慎解讀(詳見下方表格個別標註)。"
|
| 4093 |
+
if warn_n > 0 else ""
|
| 4094 |
+
)
|
| 4095 |
# 2026-08-14(業主要求):標題句前面加一句「以[檢定方法]比較
|
| 4096 |
# [分組]文本」,方法與分組名稱都從 diff 本身反推——分組代碼
|
| 4097 |
# 是「{組別}_N」這組欄名的前綴(run_statistical_analysis 產出
|
|
|
|
| 4118 |
# 的做法一致。
|
| 4119 |
R[6] = gr.update(
|
| 4120 |
value=(f"### 差異性分析\n{prefix}共 **{len(diff)}** 個指標,"
|
| 4121 |
+
f"其中 **{len(sig)}** 個達顯著差異(p < {ALPHA})。{empty_note}{untestable_note}{zero_var_note}\n\n"
|
| 4122 |
f"下方表格中 N=樣本數、M=平均數、SD=標準差。"),
|
| 4123 |
visible=True,
|
| 4124 |
)
|
|
|
|
| 4190 |
try:
|
| 4191 |
with open(str(txt_files[0]), 'r', encoding='utf-8') as f:
|
| 4192 |
rpt_text = f.read()
|
| 4193 |
+
# 2026-08-24(業主回報畫面上模型資訊重複顯示兩次):txt 檔本身
|
| 4194 |
+
# 從 2026-08-22 起(可重複性設計)就會把「模型:...」這行寫進
|
| 4195 |
+
# 檔頭,是給下載後獨立開啟 txt 檔的人看的;但畫面這裡另外又疊了
|
| 4196 |
+
# 一行同樣內容的 model_note(有���色樣式、給畫面用),兩者一起
|
| 4197 |
+
# 顯示就變成重複。畫面上只保留有樣式的 model_note,rpt_text
|
| 4198 |
+
# 裡那行純文字的「模型:」拿掉,txt 檔本身仍完整保留不受影響。
|
| 4199 |
+
rpt_text = re.sub(r'^模型:.*\n', '', rpt_text, count=1, flags=re.MULTILINE)
|
| 4200 |
model_note = (f"<span style='color:var(--c-accent-strong);font-weight:600'>"
|
| 4201 |
f"本報告由主 Agent(LLM1)模型 {LLM1_MODEL} 生成"
|
| 4202 |
f"(提示詞版本 {PROMPT_VERSION})</span>")
|
|
|
|
| 4231 |
if not llm_dst.exists():
|
| 4232 |
shutil.copy(str(docx_files[0]), str(llm_dst))
|
| 4233 |
dl_rpt.append(str(llm_dst))
|
| 4234 |
+
# 2026-08-26:這次分析實際套用的 system prompt 原文,跟報告
|
| 4235 |
+
# 一起提供下載——固定檔名(不像上面兩份加時間戳),本來就
|
| 4236 |
+
# 只會有一份,不需要另外複製改名。
|
| 4237 |
+
sp_path = output_dir / "06_system_prompt.txt"
|
| 4238 |
+
if sp_path.exists():
|
| 4239 |
+
dl_rpt.append(str(sp_path))
|
| 4240 |
if dl_rpt:
|
| 4241 |
R[19] = gr.update(value=dl_rpt, visible=True)
|
| 4242 |
|
|
|
|
| 4705 |
"---\n\n"
|
| 4706 |
"📢 **隱私提醒**:上傳的文本內容將傳送至第三方 AI 服務"
|
| 4707 |
"(OpenRouter 及其上游模型供應商)進行語言分析與報告撰寫。"
|
| 4708 |
+
"請確認語料不涉及尚未公開發表之機密內容,如有疑慮請先行確認後再上傳。"
|
| 4709 |
)
|
| 4710 |
yield (chat_history, log_history, state, gr.update(visible=True),
|
| 4711 |
_todo_update(state), gr.update(value=hint),
|
|
|
|
| 4804 |
|
| 4805 |
reset_btn.click(fn=lambda: gr.update(visible=True), outputs=[reset_confirm_row])
|
| 4806 |
reset_cancel_btn.click(fn=lambda: gr.update(visible=False), outputs=[reset_confirm_row])
|
| 4807 |
+
|
| 4808 |
+
about_btn.click(fn=lambda: gr.update(visible=True), outputs=[about_overlay])
|
| 4809 |
+
about_close_btn.click(fn=lambda: gr.update(visible=False), outputs=[about_overlay])
|
| 4810 |
# 2026-08-12:reset_confirm_btn 的接線搬到檔案最後(見 create_app 結尾
|
| 4811 |
# 「重置接線」那段)。原因:它需要 cancels=[分析運算那兩條事件鏈],而
|
| 4812 |
# 那兩條要等 meta_confirm_btn/meta_cancel_btn 接完才存在。Gradio 的
|
|
|
|
| 4822 |
# 發言氣泡,看起來像 AI 自問自答):在流程真正開始處理之前,先補一則
|
| 4823 |
# 使用者訊息氣泡,把「使用者上傳了哪些檔案」呈現出來,對話才有一來
|
| 4824 |
# 一往的感覺。
|
| 4825 |
+
# 2026-08-24(業主建議):若同一情境下、前一次「確認上傳」的格式驗證
|
| 4826 |
+
# 失敗過,這次再按確認上傳就不是「第一次上傳」,而是使用者刪掉檔案、
|
| 4827 |
+
# 修正後重新送出——文字改成「重新上傳了」更貼近實際發生的事,讀者
|
| 4828 |
+
# 事後看對話紀錄也更容易理解「這是一次修正後的重試」。判斷時機要在
|
| 4829 |
+
# 下面把 state["upload"]["validate"]["status"] 覆寫成 "pending" 之前
|
| 4830 |
+
# 讀,不然永遠讀到剛被蓋掉的新值。換情境重來(on_back_to_scenario)
|
| 4831 |
+
# 會把這個狀態重置回 "pending",避免換情境後的第一次上傳被誤標成
|
| 4832 |
+
# 「重新上傳」。
|
| 4833 |
+
_is_retry = state.get("upload", {}).get("validate", {}).get("status") == "failed"
|
| 4834 |
_uploaded_names = [Path(f.name if hasattr(f, "name") else str(f)).name for f in files]
|
| 4835 |
+
_verb = "重新上傳了" if _is_retry else "上傳了"
|
| 4836 |
+
chat_history = chat_history + [_user_msg(f"{_verb} {len(_uploaded_names)} 個檔案:{'、'.join(_uploaded_names)}")]
|
| 4837 |
|
| 4838 |
# 文本上傳(step 2:3-A 新增「研究選擇」為第 1 步後,這裡的步驟
|
| 4839 |
# 編號全部 +1):開始處理即關閉上傳彈窗,避免遮罩擋住操作。
|
|
|
|
| 4886 |
log_history = _log_append(log_history, "✅ 格式驗證通過")
|
| 4887 |
# 格式通過後:顯示 metadata 說明表單(語料說明=step 3),等使用者填寫
|
| 4888 |
state["state"] = "METADATA_INPUT"
|
| 4889 |
+
meta_hint = "請填寫語料說明(原文資訊、群組層與個體層),完成後點「確認並開始分析」。"
|
| 4890 |
chat_history = chat_history + [_assistant_msg(meta_hint)]
|
| 4891 |
+
log_history = _log_append(log_history, "📑 請填寫語料說明")
|
| 4892 |
# 2026-07-31(Batch E 第 5、6 項):業主回報「語料說明」「分析
|
| 4893 |
# 運算」兩步驟畫面上還留著「文本上傳」的進度區塊(檔案接收 ✓/
|
| 4894 |
# 格式驗證 ✓),流程往下推進後這塊資訊已經沒用。清空的時機點
|
|
|
|
| 4978 |
(群組標籤/翻譯類型/翻譯方向/譯者背景/目標讀者)攤平排列,
|
| 4979 |
對應 meta_group_fields 三個 slot、各自 5 個 Textbox 的值。"""
|
| 4980 |
sc = state.get("scenario", "")
|
| 4981 |
+
L = ["## 語料說明", "",
|
| 4982 |
"### 原文資訊",
|
| 4983 |
f"- 原文標題:{title or '(未填)'}",
|
| 4984 |
f"- 作者:{author or '(未填)'}",
|
|
|
|
| 5162 |
state["cleaned_files"] = []
|
| 5163 |
state["current_step"] = 1
|
| 5164 |
state["done_steps"] = [s for s in state.get("done_steps", []) if s != 1]
|
| 5165 |
+
# 2026-08-24:換情境重來時,上一個情境若曾經格式驗證失敗過,這裡要
|
| 5166 |
+
# 把狀態重置回初始值,不然換到新情境後的第一次上傳會被 on_upload_
|
| 5167 |
+
# confirm 誤判成「重新上傳」(見該處 _is_retry 判斷邏輯)。
|
| 5168 |
+
state["upload"] = {
|
| 5169 |
+
"receive": {"status": "pending", "line": ""},
|
| 5170 |
+
"validate": {"status": "pending", "line": ""},
|
| 5171 |
+
}
|
| 5172 |
return (
|
| 5173 |
gr.update(visible=True), # scenario_select_row
|
| 5174 |
gr.update(visible=False), # upload_row
|
|
|
|
| 5236 |
version, ngroups, method, group_field_values, indiv_df)
|
| 5237 |
state["group_labels"] = _extract_group_labels(state, group_field_values)
|
| 5238 |
state["individual_grouping"] = _extract_individual_grouping(_df_rows(indiv_df), method)
|
| 5239 |
+
log_history = _log_append(log_history, "✅ 已記錄語料說明,開始分析")
|
| 5240 |
# 2026-08-22:同上傳檔案那段,補一則使用者訊息氣泡,呈現「使用者
|
| 5241 |
# 填寫完語料說明並送出」這個動作,避免對話看起來像 AI 自問自答。
|
| 5242 |
+
chat_history = chat_history + [_user_msg("已送出語料說明(原文資訊、群組層與個體層),請開始分析")]
|
| 5243 |
# 在主對話視窗立即顯示「開始分析」訊息,避免長時間運算期間畫面看起來像沒有反應
|
| 5244 |
chat_history = chat_history + [_assistant_msg(_ANALYSIS_START_MSG)]
|
| 5245 |
yield (chat_history, log_history, state, gr.update(visible=False),
|
|
|
|
| 5283 |
yield (chat_history, log_history, state, gr.update(), _todo_update(state),
|
| 5284 |
gr.update(), gr.update(), *_no_tabs)
|
| 5285 |
return
|
| 5286 |
+
state["user_metadata"] = state.get("user_metadata", "") or "(使用者未提供語料說明)"
|
| 5287 |
+
log_history = _log_append(log_history, "⏭️ 已略過語料說明,直接開始分析")
|
| 5288 |
# 2026-08-22:同 on_metadata_confirm,補使用者訊息氣泡(略過版本)。
|
| 5289 |
chat_history = chat_history + [_user_msg("略過填寫,直接開始分析")]
|
| 5290 |
chat_history = chat_history + [_assistant_msg(_ANALYSIS_START_MSG)]
|
|
@@ -1314,6 +1314,7 @@ img.button-icon { opacity: .82; }
|
|
| 1314 |
的改動,都要回頭盤同名 class 的其他規則有沒有被連帶壓掉。 */
|
| 1315 |
.nav-col.collapsed #reset_btn,
|
| 1316 |
.nav-col.collapsed #nav_guide_btn,
|
|
|
|
| 1317 |
.nav-col.collapsed #result_nav_col .result-nav-btn {
|
| 1318 |
justify-content: center !important;
|
| 1319 |
padding: .45rem !important; /* 2026-07-24 收窄:原本 .6rem 把圖示擠到 8px(見下方註解) */
|
|
@@ -1599,6 +1600,43 @@ img.button-icon { opacity: .82; }
|
|
| 1599 |
border-radius: 10px !important;
|
| 1600 |
font-weight: 700 !important;
|
| 1601 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1602 |
/* Phase 8-D:結果導覽(情境選定後顯示在開始新分析按鈕下方),純文字
|
| 1603 |
標籤(暫不用圖示,等下一輪視覺打磨補 Heroicons SVG),維持跟
|
| 1604 |
scenario-buttons 一致的「貼齊左邊、無強調色底」低視覺重量風格,
|
|
@@ -1628,7 +1666,8 @@ img.button-icon { opacity: .82; }
|
|
| 1628 |
Gradio 的 button.svelte-xxx.secondary,不需要額外的祖先。 */
|
| 1629 |
#result_nav_col .result-nav-btn,
|
| 1630 |
#reset_btn,
|
| 1631 |
-
#nav_guide_btn
|
|
|
|
| 1632 |
width: 100% !important;
|
| 1633 |
justify-content: flex-start !important;
|
| 1634 |
text-align: left !important;
|
|
@@ -1641,7 +1680,8 @@ img.button-icon { opacity: .82; }
|
|
| 1641 |
不貼齊欄邊。拿掉原本 Bootstrap nav-underline 的左緣線(千卉指定改填色)。 */
|
| 1642 |
#result_nav_col .result-nav-btn,
|
| 1643 |
#reset_btn,
|
| 1644 |
-
#nav_guide_btn
|
|
|
|
| 1645 |
padding: .5rem .7rem !important;
|
| 1646 |
font-size: var(--font-size-base) !important;
|
| 1647 |
font-weight: 500 !important;
|
|
@@ -1651,13 +1691,15 @@ img.button-icon { opacity: .82; }
|
|
| 1651 |
}
|
| 1652 |
#result_nav_col .result-nav-btn:hover:not(:disabled),
|
| 1653 |
#reset_btn:hover:not(:disabled),
|
| 1654 |
-
#nav_guide_btn:hover:not(:disabled)
|
|
|
|
| 1655 |
background: rgba(var(--c-accent-rgb),.08) !important;
|
| 1656 |
color: var(--c-text) !important;
|
| 1657 |
}
|
| 1658 |
#result_nav_col .result-nav-btn:active:not(:disabled),
|
| 1659 |
#reset_btn:active:not(:disabled),
|
| 1660 |
-
#nav_guide_btn:active:not(:disabled)
|
|
|
|
| 1661 |
background: rgba(var(--c-accent-rgb),.16) !important;
|
| 1662 |
}
|
| 1663 |
/* 歷史紀錄:這裡原本有一整組 .result-nav-subbtn 樣式,把統計比較分析底下
|
|
@@ -1702,7 +1744,8 @@ img.button-icon { opacity: .82; }
|
|
| 1702 |
flex: 0 0 auto !important;
|
| 1703 |
}
|
| 1704 |
#result_nav_col .result-nav-btn--muted,
|
| 1705 |
-
#nav_guide_btn.result-nav-btn--muted
|
|
|
|
| 1706 |
font-size: var(--font-size-base) !important;
|
| 1707 |
font-weight: 500 !important;
|
| 1708 |
color: var(--c-text-muted) !important;
|
|
|
|
| 1314 |
的改動,都要回頭盤同名 class 的其他規則有沒有被連帶壓掉。 */
|
| 1315 |
.nav-col.collapsed #reset_btn,
|
| 1316 |
.nav-col.collapsed #nav_guide_btn,
|
| 1317 |
+
.nav-col.collapsed #nav_about_btn,
|
| 1318 |
.nav-col.collapsed #result_nav_col .result-nav-btn {
|
| 1319 |
justify-content: center !important;
|
| 1320 |
padding: .45rem !important; /* 2026-07-24 收窄:原本 .6rem 把圖示擠到 8px(見下方註解) */
|
|
|
|
| 1600 |
border-radius: 10px !important;
|
| 1601 |
font-weight: 700 !important;
|
| 1602 |
}
|
| 1603 |
+
/* 2026-08-26:「關於」面板沿用 .reset-confirm-overlay 同一套置中 modal
|
| 1604 |
+
機制(同一個 elem_classes 掛在 overlay 上),但內容是說明文字不是
|
| 1605 |
+
一兩句確認詞,.reset-confirm-panel 的 400px 版寬會把整段文字擠成
|
| 1606 |
+
窄豆腐塊,所以另開 .about-panel 加寬;內容也比確認框長,視窗矮的時候
|
| 1607 |
+
(筆電、手機橫向)要能捲動,不能被 overlay 裁掉看不到下半段。文字
|
| 1608 |
+
對齊也從確認框的置中改左靠——長段落置中會很難讀。 */
|
| 1609 |
+
.about-panel {
|
| 1610 |
+
margin: auto !important;
|
| 1611 |
+
width: 100% !important;
|
| 1612 |
+
max-width: 560px !important;
|
| 1613 |
+
max-height: min(80vh, 640px) !important;
|
| 1614 |
+
overflow-y: auto !important;
|
| 1615 |
+
flex: none !important;
|
| 1616 |
+
gap: 1rem !important;
|
| 1617 |
+
padding: 1.6rem 1.8rem !important;
|
| 1618 |
+
border: 1px solid var(--c-border-strong) !important;
|
| 1619 |
+
border-radius: 14px !important;
|
| 1620 |
+
background: var(--c-surface) !important;
|
| 1621 |
+
box-shadow: 0 20px 50px rgba(15, 23, 42, .3) !important;
|
| 1622 |
+
text-align: left !important;
|
| 1623 |
+
}
|
| 1624 |
+
.about-panel h2, .about-panel h3 {
|
| 1625 |
+
text-align: left !important;
|
| 1626 |
+
}
|
| 1627 |
+
.about-panel hr {
|
| 1628 |
+
border: none !important;
|
| 1629 |
+
border-top: 1px solid var(--c-border-strong) !important;
|
| 1630 |
+
margin: .6rem 0 !important;
|
| 1631 |
+
}
|
| 1632 |
+
.about-panel .about-close-row {
|
| 1633 |
+
display: flex !important;
|
| 1634 |
+
justify-content: flex-end !important;
|
| 1635 |
+
}
|
| 1636 |
+
.about-panel .about-close-row button {
|
| 1637 |
+
border-radius: 10px !important;
|
| 1638 |
+
font-weight: 700 !important;
|
| 1639 |
+
}
|
| 1640 |
/* Phase 8-D:結果導覽(情境選定後顯示在開始新分析按鈕下方),純文字
|
| 1641 |
標籤(暫不用圖示,等下一輪視覺打磨補 Heroicons SVG),維持跟
|
| 1642 |
scenario-buttons 一致的「貼齊左邊、無強調色底」低視覺重量風格,
|
|
|
|
| 1666 |
Gradio 的 button.svelte-xxx.secondary,不需要額外的祖先。 */
|
| 1667 |
#result_nav_col .result-nav-btn,
|
| 1668 |
#reset_btn,
|
| 1669 |
+
#nav_guide_btn,
|
| 1670 |
+
#nav_about_btn {
|
| 1671 |
width: 100% !important;
|
| 1672 |
justify-content: flex-start !important;
|
| 1673 |
text-align: left !important;
|
|
|
|
| 1680 |
不貼齊欄邊。拿掉原本 Bootstrap nav-underline 的左緣線(千卉指定改填色)。 */
|
| 1681 |
#result_nav_col .result-nav-btn,
|
| 1682 |
#reset_btn,
|
| 1683 |
+
#nav_guide_btn,
|
| 1684 |
+
#nav_about_btn {
|
| 1685 |
padding: .5rem .7rem !important;
|
| 1686 |
font-size: var(--font-size-base) !important;
|
| 1687 |
font-weight: 500 !important;
|
|
|
|
| 1691 |
}
|
| 1692 |
#result_nav_col .result-nav-btn:hover:not(:disabled),
|
| 1693 |
#reset_btn:hover:not(:disabled),
|
| 1694 |
+
#nav_guide_btn:hover:not(:disabled),
|
| 1695 |
+
#nav_about_btn:hover:not(:disabled) {
|
| 1696 |
background: rgba(var(--c-accent-rgb),.08) !important;
|
| 1697 |
color: var(--c-text) !important;
|
| 1698 |
}
|
| 1699 |
#result_nav_col .result-nav-btn:active:not(:disabled),
|
| 1700 |
#reset_btn:active:not(:disabled),
|
| 1701 |
+
#nav_guide_btn:active:not(:disabled),
|
| 1702 |
+
#nav_about_btn:active:not(:disabled) {
|
| 1703 |
background: rgba(var(--c-accent-rgb),.16) !important;
|
| 1704 |
}
|
| 1705 |
/* 歷史紀錄:這裡原本有一整組 .result-nav-subbtn 樣式,把統計比較分析底下
|
|
|
|
| 1744 |
flex: 0 0 auto !important;
|
| 1745 |
}
|
| 1746 |
#result_nav_col .result-nav-btn--muted,
|
| 1747 |
+
#nav_guide_btn.result-nav-btn--muted,
|
| 1748 |
+
#nav_about_btn.result-nav-btn--muted {
|
| 1749 |
font-size: var(--font-size-base) !important;
|
| 1750 |
font-weight: 500 !important;
|
| 1751 |
color: var(--c-text-muted) !important;
|
|
|