diff --git a/tools/corpus/README.md b/tools/corpus/README.md index 2c905d2..8064d94 100644 --- a/tools/corpus/README.md +++ b/tools/corpus/README.md @@ -68,3 +68,23 @@ python3 tools/corpus/merge_labels.py ~/anime-companion-data/sao/dialogues.jsonl - 標註全部通過白名單檢查,採用 70 句(high 且非 unknown)。 - 規則高信心與標註一致 20/25,中信心 15/25。不一致的例子顯示規則會把「對她說道」的聽者當成說話者,也會把「我眼前」的「我」當成說話者,所以最終以標註為準,規則只當參考。 - sub-agent 回報的模糊處(非說出口的「」、無名角色、信心門檻)已補進 `label_prompt.md`。 + +### 全量結果 + +全部 4,324 句分 87 批(每批 50 句)標註,再對早期批次被標成 `none` 的 62 句用新版指示重標。 + +```bash +python3 tools/corpus/make_batches.py ~/anime-companion-data/sao/dialogues.jsonl ~/anime-companion-data/sao/labeling/full/in --all --batch-size 50 +# 每批交給 sub-agent,輸出到 .../full/out/ +python3 tools/corpus/verify_batches.py ~/anime-companion-data/sao/labeling/full # 逐批比對 id +python3 tools/corpus/make_relabel.py ~/anime-companion-data/sao/labeling/full ~/anime-companion-data/sao/labeling/relabel/in --max-batch 24 +python3 tools/corpus/merge_labels.py ~/anime-companion-data/sao/dialogues.jsonl ~/anime-companion-data/sao/labeling/full/out,~/anime-companion-data/sao/labeling/relabel/out ~/anime-companion-data/sao/labeling/final.jsonl --review-dir ~/anime-companion-data/sao/labeling/review +``` + +- 全部通過白名單檢查。早期批次把 `none` 一律標成非說出口的話,重標後只有 2 句改成有說話者。 +- 一個批次把 `confidence` 縮寫成 h/m/l,合併時對應回完整值(`merge_labels.py` 的 `CONFIDENCE_ALIASES`);`worry` 加進情緒白名單。 +- 採用(high 且非 unknown)2,340 句;`none` 673 句;亞絲娜 304 句(第 1 冊 228、第 3 冊 15、第 4 冊 61),直葉/莉法 514 句(第 3 冊 303、第 4 冊 211)。 +- 規則高信心與標註一致 188/262(72%),中信心 82/142(58%),所以最終以標註為準。 +- 多個 agent 並行時共用暫存目錄會互相覆蓋腳本,所以 `verify_batches.py` 逐批比對 id;全部一致。 +- wiki 的部分頁面有重複或錯置的段落,標註 agent 對這類句子多半降為 medium 或 low,不會進入採用的 2,340 句。 +- `mode`(亞絲娜的「被囚禁/重逢」、直葉的「現實/莉法」)目前只有規則依別名推得的少數句子有值,大多數是空的;W5、W6 要另外依章節情境補上。 diff --git a/tools/corpus/label_prompt.md b/tools/corpus/label_prompt.md index 3ccfabd..44dafbc 100644 --- a/tools/corpus/label_prompt.md +++ b/tools/corpus/label_prompt.md @@ -8,6 +8,7 @@ - `before`、`after`:同段落中對白前後的敘述。 - `prev_par`、`next_par`:前後段落。 - `candidates`:敘述中出現的角色名字。 +- 這份 wiki 的部分頁面有重複或錯置的段落,所以前後文有時和對白對不上;遇到這種情況說話者降為 `low` 或 `medium`,不要硬推。 - `suspect_duplicate`:為 true 表示這句疑似是 wiki 頁面銜接造成的重複;照常標註即可,之後會去重。 - `prev`、`next`:同一章前後各 2 句對白,用來判斷輪流發言。 @@ -22,7 +23,8 @@ - `speaker`:角色代號。已知代號:`asuna`(亞絲娜、明日奈)、`kirito`(桐人、和人)、`suguha`(直葉、莉法)、`klein`、`agil`、`lisbeth`、`silica`、`yui`、`kayaba`、`kuradeel`、`sugou`。 - 文中有名字、但不在上面清單的角色寫 `other:名字`。 - 沒有名字的角色(路人、護士、敵方 NPC)、或判斷不出來,一律寫 `unknown`。 - - 不是「說出口的話」的「」(專有名詞、技能或系統名稱、強調的詞、擬聲詞、心聲、嵌在敘述中的片段)寫 `none`,`function` 寫 `other`,`emotion` 寫 `neutral`,`confidence` 寫 `high`。 + - 不是「說出口的話」的「」(專有名詞、技能或系統名稱、強調的詞、擬聲詞、心聲)寫 `none`,`function` 寫 `other`,`emotion` 寫 `neutral`,`confidence` 寫 `high`。 + - 嵌在敘述中、但確實是某人說出口的話(例如「好久不見了,艾基爾」亞絲娜說。),算說出口的話,照常標說話者;只有專有名詞、單詞、擬聲詞這類才寫 `none`。有疑問時,一句完整的話(有主詞或語氣、能想像有人這樣說)算說出口的話。 - 沉默或省略號(「……」)若敘述指出是誰沉默,照常標說話者;否則寫 `unknown`。 - `confidence`: - `high`:敘述明指說話者,或問答、輪流發言毫無歧義。 diff --git a/tools/corpus/make_batches.py b/tools/corpus/make_batches.py index cb3ca1b..33bc172 100644 --- a/tools/corpus/make_batches.py +++ b/tools/corpus/make_batches.py @@ -3,7 +3,7 @@ 用法: make_batches.py <輸出目錄> --pilot N # 試跑:分層抽樣 N 句 - make_batches.py <輸出目錄> --all # 全量:規則未判定或中信心的對白 + make_batches.py <輸出目錄> --all # 全量:所有對白(規則高信心也只有約八成準,所以一併標註) 每批一個 jsonl,一行一句,附前後各 2 句對白當輪流發言的線索。 批次檔不含規則判定的說話者,讓標註獨立,之後才能拿來和規則比對。 @@ -57,7 +57,7 @@ def main(): ap.error("--pilot N 與 --all 擇一") recs = [json.loads(line) for line in Path(a.dialogues).read_text(encoding="utf-8").splitlines()] nb = with_neighbors(recs) - chosen = pilot_sample(recs, a.pilot, a.seed) if a.pilot else [r for r in recs if r["confidence"] != "high"] + chosen = pilot_sample(recs, a.pilot, a.seed) if a.pilot else recs out = Path(a.outdir) out.mkdir(parents=True, exist_ok=True) for i in range(0, len(chosen), a.batch_size): diff --git a/tools/corpus/make_relabel.py b/tools/corpus/make_relabel.py new file mode 100644 index 0000000..4fa9fe8 --- /dev/null +++ b/tools/corpus/make_relabel.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +"""挑出需要用新版指示重標的 none 對白。 + +用法:make_relabel.py <輸出目錄> --max-batch N + +早期批次用的指示把「嵌在敘述中的片段」一律標 none,會漏掉確實是某人說出口的話。 +這裡從批次 1..N 的標註結果挑出 speaker=none 且看起來像完整句子的對白(長度 ≥ 6 字, +或含標點),重新切成批次檔,格式與 make_batches.py 相同。 +""" +import argparse +import json +import re +from pathlib import Path + +SENTENCE_HINT = re.compile(r"[,。!?…、]") + + +def looks_spoken(text): + return len(text) >= 6 or bool(SENTENCE_HINT.search(text)) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("full_dir") + ap.add_argument("outdir") + ap.add_argument("--max-batch", type=int, required=True) + ap.add_argument("--batch-size", type=int, default=50) + a = ap.parse_args() + full = Path(a.full_dir) + picked = [] + for n in range(1, a.max_batch + 1): + name = f"batch-{n:03d}.jsonl" + src = {json.loads(l)["id"]: json.loads(l) for l in (full / "in" / name).read_text(encoding="utf-8").splitlines()} + for line in (full / "out" / name).read_text(encoding="utf-8").splitlines(): + lb = json.loads(line) + if lb["speaker"] == "none" and looks_spoken(src[lb["id"]]["text"]): + picked.append(src[lb["id"]]) + out = Path(a.outdir) + out.mkdir(parents=True, exist_ok=True) + for i in range(0, len(picked), a.batch_size): + path = out / f"batch-{i // a.batch_size + 1:03d}.jsonl" + path.write_text("".join(json.dumps(r, ensure_ascii=False) + "\n" for r in picked[i:i + a.batch_size]), encoding="utf-8") + print(f"{len(picked)} 句 -> {(len(picked) + a.batch_size - 1) // a.batch_size} 批 -> {out}") + + +if __name__ == "__main__": + main() diff --git a/tools/corpus/merge_labels.py b/tools/corpus/merge_labels.py index cd1c3f1..6d787a9 100644 --- a/tools/corpus/merge_labels.py +++ b/tools/corpus/merge_labels.py @@ -22,6 +22,16 @@ def read_jsonl(path): return [json.loads(line) for line in Path(path).read_text(encoding="utf-8").splitlines() if line.strip()] +# 有的批次輸出把 confidence 縮寫成一個字母;明確對應才轉換,其他值照常丟棄。 +CONFIDENCE_ALIASES = {"h": "high", "m": "medium", "l": "low"} + + +def normalize(label): + if label.get("confidence") in CONFIDENCE_ALIASES: + return {**label, "confidence": CONFIDENCE_ALIASES[label["confidence"]]} + return label + + def check(label): errs = [] if set(label) != KEYS: @@ -38,14 +48,16 @@ def check(label): def main(): ap = argparse.ArgumentParser() ap.add_argument("dialogues") - ap.add_argument("labels_dir") + ap.add_argument("labels_dir", help="標註輸出目錄;可用逗號接多個,後面的覆蓋前面的(用於重標)") ap.add_argument("out") ap.add_argument("--review-dir") a = ap.parse_args() recs = {r["id"]: r for r in read_jsonl(a.dialogues)} labels, rejected = {}, [] - for f in sorted(Path(a.labels_dir).glob("*.jsonl")): + files = [f for d in a.labels_dir.split(",") for f in sorted(Path(d).glob("*.jsonl"))] + for f in files: for lb in read_jsonl(f): + lb = normalize(lb) errs = check(lb) if lb.get("id") not in recs: errs.append("id 不存在") diff --git a/tools/corpus/tags.json b/tools/corpus/tags.json index aefff8f..c84a6b4 100644 --- a/tools/corpus/tags.json +++ b/tools/corpus/tags.json @@ -27,6 +27,7 @@ "fear": "害怕", "shy": "害羞", "resolve": "堅定", - "affection": "親愛" + "affection": "親愛", + "worry": "擔心" } } diff --git a/tools/corpus/test_labeling.py b/tools/corpus/test_labeling.py index a8beac9..b8db2a3 100644 --- a/tools/corpus/test_labeling.py +++ b/tools/corpus/test_labeling.py @@ -50,6 +50,14 @@ class Check(unittest.TestCase): def test_bad_confidence_rejected(self): self.assertTrue(merge_labels.check({**self.good, "confidence": "sure"})) + def test_worry_is_a_valid_emotion(self): + self.assertEqual(merge_labels.check({**self.good, "emotion": "worry"}), []) + + def test_confidence_abbreviations_are_normalized(self): + self.assertEqual(merge_labels.normalize({**self.good, "confidence": "m"})["confidence"], "medium") + self.assertEqual(merge_labels.normalize({**self.good, "confidence": "h"})["confidence"], "high") + self.assertEqual(merge_labels.normalize({**self.good, "confidence": "sure"})["confidence"], "sure") + def test_none_speaker_is_valid_but_never_final(self): self.assertEqual(merge_labels.check({**self.good, "speaker": "none", "function": "other", "emotion": "neutral"}), []) diff --git a/tools/corpus/verify_batches.py b/tools/corpus/verify_batches.py new file mode 100644 index 0000000..3f70ded --- /dev/null +++ b/tools/corpus/verify_batches.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""逐批檢查標註輸出和輸入的 id 完全一致、順序相同,並列出缺漏與錯置。 + +用法:verify_batches.py <批次目錄,內含 in/ 與 out/> +多個 agent 並行寫檔時可能互相覆蓋或寫錯批次,只看格式抓不到,所以要比對 id。 +""" +import json +import sys +from pathlib import Path + + +def ids(path): + return [json.loads(line)["id"] for line in Path(path).read_text(encoding="utf-8").splitlines() if line.strip()] + + +def main(): + root = Path(sys.argv[1]) + bad, missing = [], [] + for src in sorted((root / "in").glob("batch-*.jsonl")): + out = root / "out" / src.name + if not out.exists(): + missing.append(src.name) + continue + a, b = ids(src), ids(out) + if a != b: + bad.append((src.name, len(a), len(b), sorted(set(a) ^ set(b))[:3])) + print(f"缺 {len(missing)} 批:{missing}") + print(f"id 不一致 {len(bad)} 批:") + for row in bad: + print(" ", row) + sys.exit(1 if bad or missing else 0) + + +if __name__ == "__main__": + main()