build_pack.py 產生對白索引(放在使用者資料目錄);render_skill.py 把人格卡渲染成 角色 skill 並檢查精選對白逐字來自索引;插件改成執行期從資料目錄讀索引。 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
352ce7dcad
commit
697ed6a0b9
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
"""從合併後的標註產生各角色的對白索引與人格卡的素材。
|
||||
|
||||
用法:build_pack.py <final.jsonl> <資料目錄>
|
||||
|
||||
每個角色輸出到 <資料目錄>/<角色代號>/:
|
||||
- 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()
|
||||
Reference in New Issue
Block a user