Files
Jeffery (jiantw83)andClaude Sonnet 5.5 697ed6a0b9 feat: 亞絲娜與直葉的角色包 (#6, #7)
build_pack.py 產生對白索引(放在使用者資料目錄);render_skill.py 把人格卡渲染成
角色 skill 並檢查精選對白逐字來自索引;插件改成執行期從資料目錄讀索引。

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 20:52:40 +00:00

99 lines
3.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()