#!/usr/bin/env python3 """從合併後的標註產生各角色的對白索引與人格卡的素材。 用法:build_pack.py <資料目錄> 每個角色輸出到 <資料目錄>/<角色代號>/: - index.jsonl:執行期檢索用,一行一句 {id, function, emotion, mode, text}。 - source.jsonl:撰寫人格卡用,同上再附前文與冊章。 資料目錄放在 repo 之外(預設 ~/.claude/anime-companion);原文不入版本控制。 """ import json import sys from collections import Counter, defaultdict from pathlib import Path CHARACTERS = {"sao-asuna": "asuna", "sao-suguha": "suguha"} MIN_LEN, MAX_LEN = 4, 80 CAPTIVE_WORDS = ("提泰妮亞", "蒂塔妮亞", "提坦妮亞", "須鄉", "奧伯龍", "鳥籠", "世界樹") REAL_SUGUHA = ("直葉", "小直", "和人", "哥哥") def context(rec): return "".join((rec["before"], rec["after"], rec["prev_par"], rec["next_par"])) def mode_asuna(rec): ctx = context(rec) if "明日奈" in ctx: return "real" if any(w in ctx for w in CAPTIVE_WORDS): return "captive" return {1: "ingame", 3: "captive", 4: "reunion"}.get(rec["volume"]) def mode_suguha(rec): ctx = context(rec) leafa, real = ctx.count("莉法"), sum(ctx.count(w) for w in REAL_SUGUHA) if leafa == 0 and real == 0: return None return "leafa" if leafa >= real else "real" MODE_FUNCS = {"asuna": mode_asuna, "suguha": mode_suguha} def usable(rec, speaker): text = rec["text"] return ( rec["final_speaker"] == speaker and rec["label"]["confidence"] == "high" and MIN_LEN <= len(text) <= MAX_LEN and not rec["suspect_duplicate"] and not (rec["label"]["function"] == "other" and len(text) < 6) ) def build(records, speaker): seen, rows = set(), [] for rec in records: if not usable(rec, speaker) or rec["text"] in seen: continue seen.add(rec["text"]) rows.append(rec) # 沒判斷出模式的句子,沿用同一章其他句子的多數模式 modes = [MODE_FUNCS[speaker](r) for r in rows] by_chapter = defaultdict(Counter) for r, m in zip(rows, modes): if m: by_chapter[(r["volume"], r["chapter"])][m] += 1 out = [] for r, m in zip(rows, modes): if m is None and by_chapter[(r["volume"], r["chapter"])]: m = by_chapter[(r["volume"], r["chapter"])].most_common(1)[0][0] lb = r["label"] out.append({"id": r["id"], "function": lb["function"], "emotion": lb["emotion"], "mode": m, "text": r["text"], "_ctx": {"before": r["before"], "prev_par": r["prev_par"], "volume": r["volume"], "chapter": r["chapter"]}}) return out def write_pack(rows, folder): folder.mkdir(parents=True, exist_ok=True) dump = lambda items: "".join(json.dumps(i, ensure_ascii=False) + "\n" for i in items) (folder / "index.jsonl").write_text(dump({k: v for k, v in r.items() if k != "_ctx"} for r in rows), encoding="utf-8") (folder / "source.jsonl").write_text(dump({**{k: v for k, v in r.items() if k != "_ctx"}, **r["_ctx"]} for r in rows), encoding="utf-8") def main(): final, data = Path(sys.argv[1]), Path(sys.argv[2]) records = [json.loads(line) for line in final.read_text(encoding="utf-8").splitlines()] for code, speaker in CHARACTERS.items(): rows = build(records, speaker) write_pack(rows, data / code) print(f"{code}: {len(rows)} 句;功能 {dict(Counter(r['function'] for r in rows).most_common(5))};模式 {dict(Counter(r['mode'] for r in rows))}") if __name__ == "__main__": main()