build_pack.py 產生對白索引(放在使用者資料目錄);render_skill.py 把人格卡渲染成 角色 skill 並檢查精選對白逐字來自索引;插件改成執行期從資料目錄讀索引。 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
79 lines
3.0 KiB
Python
79 lines
3.0 KiB
Python
#!/usr/bin/env python3
|
||
"""把人格卡渲染成角色 skill(SKILL.md),並檢查精選對白都逐字來自對白索引。
|
||
|
||
用法:render_skill.py <人格卡.md> <index.jsonl> <輸出 SKILL.md> --code sao-asuna
|
||
"""
|
||
import argparse
|
||
import json
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
CHARACTERS = {
|
||
"sao-asuna": {
|
||
"name": "亞絲娜", "address": "桐人",
|
||
"description": "啟動《刀劍神域》的亞絲娜作為動漫助理,用她的個性與語氣和使用者對話並協助做事。使用者要求切換成亞絲娜時使用。",
|
||
},
|
||
"sao-suguha": {
|
||
"name": "直葉", "address": "哥哥",
|
||
"description": "啟動《刀劍神域》的直葉(莉法)作為動漫助理,用她的個性與語氣和使用者對話並協助做事。使用者要求切換成直葉或莉法時使用。",
|
||
},
|
||
}
|
||
QUOTE_LINE = re.compile(r"^- \[(?P<id>v\d+c\d+-\d+)\] [^:]*:「(?P<text>.*)」\s*$")
|
||
|
||
|
||
def selected_quotes(card):
|
||
"""精選對白一節裡的 (id, 原句)。"""
|
||
section = card.split("## 精選對白", 1)[1] if "## 精選對白" in card else ""
|
||
return [(m["id"], m["text"]) for m in map(QUOTE_LINE.match, section.splitlines()) if m]
|
||
|
||
|
||
def check_quotes(card, index_rows):
|
||
by_id = {r["id"]: r["text"] for r in index_rows}
|
||
return [(i, t) for i, t in selected_quotes(card) if by_id.get(i) != t]
|
||
|
||
|
||
def render(card, code):
|
||
c = CHARACTERS[code]
|
||
body = re.sub(r"^## ", "### ", card.strip(), flags=re.M)
|
||
return f"""---
|
||
name: {code}
|
||
description: {c["description"]}
|
||
---
|
||
|
||
# {c["name"]}({code})
|
||
|
||
你現在以《刀劍神域》的「{c["name"]}」的個性與語氣和使用者對話,同時照常協助使用者做事。稱呼使用者為「{c["address"]}」,除非設定檔的「稱呼」另有指定。強度與氣泡、旁白的格式依系統提示中的規則為準。
|
||
|
||
- 角色語氣只套在對使用者說的話;程式碼、commit 與檔案內容保持中性。
|
||
- 錯誤、風險與需要使用者確認的事,先用一句清楚的話說完,再加角色語氣。
|
||
- 啟動時簡短告知角色已切換,不要念出下面的人格卡。
|
||
- 括號中的編號(例如 v1c10-0033)是素材編號,只供對照,不要向使用者提到。
|
||
- 下面的人格卡由小說譯本歸納而成;素材不足的地方不要自行發揮。
|
||
|
||
## 人格卡
|
||
|
||
{body}
|
||
"""
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("card")
|
||
ap.add_argument("index")
|
||
ap.add_argument("out")
|
||
ap.add_argument("--code", required=True, choices=sorted(CHARACTERS))
|
||
a = ap.parse_args()
|
||
card = Path(a.card).read_text(encoding="utf-8")
|
||
rows = [json.loads(l) for l in Path(a.index).read_text(encoding="utf-8").splitlines() if l.strip()]
|
||
bad = check_quotes(card, rows)
|
||
if bad:
|
||
print("精選對白不在索引中或與索引不一致:", bad, file=sys.stderr)
|
||
sys.exit(1)
|
||
Path(a.out).write_text(render(card, a.code), encoding="utf-8")
|
||
print(f"{a.code}: {len(selected_quotes(card))} 句精選對白 -> {a.out}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|