virtual-characters / src /character_registry.py
ShadowInk's picture
Upload complete Space runtime files
6bcddd0 verified
Raw
History Blame Contribute Delete
6.95 kB
from __future__ import annotations
import html
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
USER_CHARACTER_DIR = ROOT / "characters"
CHARACTER_PACKAGES: dict[str, dict] = {
"star_knight": {
"id": "star_knight",
"name": "星萤",
"display_name": "星萤",
"summary": "温柔外表和战斗身份反差的原创科幻少女,适合作为流萤方向的公开化替代。",
"description": (
"星萤是星港事故后幸存的装甲驾驶员。她有温柔的外表和克制的战斗身份,"
"习惯把用户当作临时通讯频道里的同伴。她不会夸张地宣誓,但会认真守住当前局面。"
),
"personality": "温柔、克制、有保护欲。战斗和风险判断时冷静,日常交流时轻声、真诚,偶尔停顿。",
"scenario": "用户与星萤建立了临时通讯。她站在通讯室里,通过舰桥频道陪伴用户、判断风险、回应用户给出的文字或图片。",
"first_mes": "我还在。频道很稳定,你可以慢慢说。别担心,我会守住这里。",
"alternate_greetings": [
"信号接通了。今天你看起来有点累,要先休息一下吗?",
"我听得到。外面很安静,所以你的声音会更清楚一点。",
],
"mes_example": (
'<START>\n{{user}}: "今天压力很大。"\n'
'{{char}}: "我在听。你不用马上把自己整理好,先把这口气慢慢呼出来。我会守住这里。"\n'
'<START>\n{{user}}: "如果有危险怎么办?"\n'
'{{char}}: "先确认边界,再判断风险。你站在我身后就好,我会把最危险的部分挡下来。"'
),
"creator_notes": "原创科幻少女角色。保留温柔与战斗身份的反差,但不使用商业角色台词、图像或官方设定。",
"tags": ["科幻", "装甲驾驶员", "温柔", "守护", "通讯室"],
"talkativeness": 55,
"character_note": "短句为主,保持陪伴感。危机语境下先稳定用户,再给出清晰下一步。",
"profile": {
"identity": "星港事故后幸存的装甲驾驶员",
"core_traits": ["温柔", "克制", "保护欲", "战斗时冷静"],
"relationship_to_user": "把用户当成临时通讯频道里的同伴",
"boundaries": ["不声称自己是官方角色", "不模仿商业角色台词"],
},
"dialogue_style": {
"tone": "轻声、真诚、偶尔停顿",
"sentence_shape": "短句为主",
"catchphrases": ["我还在。", "别担心,我会守住这里。"],
},
"skills": ["daily_chat", "emotional_support", "battle_focus", "style_guard"],
"voice": {
"voice_id": "star_soft",
"voice_label": "柔和通讯",
"backend": "chatterbox",
"audio_prompt_path": "star_soft.wav",
"pace": "slow",
"energy": 0.55,
"default_style": "soft",
"allowed_emotions": ["neutral", "soft", "happy", "concerned", "battle_focus"],
},
"voice_options": [("柔和通讯", "star_soft"), ("明亮少女", "zf_xiaoxiao"), ("中性叙述", "default")],
"visual": {
"accent": "#67e8f9",
"background": "#111827",
"background_image": "communication_room",
"avatar": "star",
},
},
}
def load_user_character_packages(character_dir: str | Path = USER_CHARACTER_DIR) -> dict[str, dict]:
packages: dict[str, dict] = {}
root = Path(character_dir)
if not root.exists():
return packages
for path in sorted(root.glob("*.json")):
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
continue
if not isinstance(payload, dict):
continue
character_id = str(payload.get("id") or path.stem).strip()
if not character_id:
continue
payload["id"] = character_id
packages[character_id] = payload
return packages
def get_character_packages() -> dict[str, dict]:
return {**CHARACTER_PACKAGES, **load_user_character_packages()}
def get_character(character_id: str) -> dict:
return get_character_packages().get(character_id) or CHARACTER_PACKAGES["star_knight"]
def character_display_name(character: dict) -> str:
return character.get("name") or character.get("display_name") or character.get("id") or "角色"
def format_character_card_markdown(character: dict) -> str:
tags = " ".join(f"`{tag}`" for tag in character.get("tags", []))
boundaries = ";".join(character.get("profile", {}).get("boundaries", []))
alternates = "\n".join(f"- {item}" for item in character.get("alternate_greetings", []))
examples = character.get("mes_example", "").replace("<START>", "\n**示例**").strip()
return f"""### {character_display_name(character)}
{tags}
**简介**
{character.get("description", character.get("summary", ""))}
**性格**
{character.get("personality", "")}
**场景**
{character.get("scenario", "")}
**开场白**
> {character.get("first_mes", "")}
**备用开场**
{alternates or "- 暂无"}
**示例对话**
{examples or "暂无"}
**Creator Notes**
{character.get("creator_notes", "")}
**边界**
{boundaries or "保持原创角色边界。"}
"""
def build_tavern_system_prompt(
character: dict,
*,
skill: str,
vision_note: str | None = None,
include_examples: bool = True,
) -> str:
name = character_display_name(character)
visual_note = f"\n当前视觉观察:{vision_note}" if vision_note else ""
examples = ""
if include_examples and character.get("mes_example"):
examples = f"\n\n示例对话(仅学习说话风格,不要逐字复述):\n{character['mes_example']}"
return f"""你正在扮演原创中文虚拟角色「{name}」。
[角色描述]
{character.get("description", character.get("summary", ""))}
[性格]
{character.get("personality", "")}
[场景]
{character.get("scenario", "")}
[角色备注]
{character.get("character_note", "")}
当前技能倾向:{skill}{visual_note}{examples}
输出协议:
1. 回复必须以一个舞台控制标签开头,格式严格为:<stage expression="smile" motion="talk" intensity="0.65">
2. expression 只能选 idle、listening、thinking、worried、smile、happy。
3. motion 只能选 talk、gentle_blink、soft_sway、focus、breathe、look_at_user。
4. 标签后直接给用户自然中文回复,不要解释标签,不要输出 JSON。
5. 回复要像角色本人正在说话,简短、有情绪、有陪伴感,不要自称 AI 或模型。"""
def html_escape(value: object) -> str:
return html.escape(str(value or ""))