#!/usr/bin/env python3 """把人格卡渲染成角色 skill(SKILL.md),並檢查精選對白都逐字來自對白索引。 用法:render_skill.py <人格卡.md> <輸出 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"^- \[(?Pv\d+c\d+-\d+)\] [^:]*:「(?P.*)」\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()