Files
bililive-touhou-tts/event_rules.py
T
chun_qiu d4de39f294 feat: add {gift_count} variable to gift/blind box default rules
- New variable {gift_count} (gift quantity) alongside existing {gift_num}
- Default templates: 感谢由{viewer_name}投喂的{gift_count}个{gift_name}
- danmaku_handler passes gift_count=message.num
- README variable table and examples updated
- Rebuild portable zip (83.9 MB)
2026-08-08 17:41:10 +08:00

124 lines
4.3 KiB
Python

"""Event voice rules: load/save event_rules.json and render templates.
Each live event (danmaku, gift, blind box, enter room, follow, like,
interact, buy guard, guard toast, super chat) can be voiced using a
user-customizable template. Empty template = event is not spoken.
"""
import json
import logging
import pathlib
import re
import sys
logger = logging.getLogger(__name__)
DEFAULT_RULES = {
"enabled": {
"danmaku": True,
"gift": True,
"blind_box": True,
"enter_room": True,
"follow": False,
"interact_other": False,
"buy_guard": True,
"guard_toast": False,
"super_chat": True,
},
"templates": {
"danmaku": "{uname}\u8bf4\u3001 {msg}",
"gift": "\u611f\u8c22\u7531{viewer_name}\u6295\u5582\u7684{gift_count}\u4e2a{gift_name}",
"blind_box": "\u611f\u8c22\u7531{viewer_name}\u6295\u5582\u7684{gift_count}\u4e2a{gift_name}\u7206\u51fa\u4e86{blind_box_gift}",
"enter_room": "\u6b22\u8fce{viewer_name}\u8fdb\u5165\u76f4\u64ad\u95f4",
"follow": "",
"interact_other": "",
"buy_guard": "\u606d\u559c{ships_viewer_name}\u6210\u4e3a\u5c0a\u8d35\u7684{ships_level}",
"guard_toast": "",
"super_chat": "{viewer_name}\u7684\u9192\u76ee\u7559\u8a00\uff1a{super_chat_content}",
},
"skip_free_gift": True,
}
ALL_EVENTS = [
"danmaku", "gift", "blind_box", "enter_room", "follow",
"interact_other", "buy_guard", "guard_toast", "super_chat",
]
_TEMPLATE_RE = re.compile(r"\{(\w+)\}")
def _base_dir() -> pathlib.Path:
if getattr(sys, "frozen", False):
return pathlib.Path(sys.executable).parent.parent.parent
return pathlib.Path(__file__).parent
def rules_path() -> pathlib.Path:
return _base_dir() / "event_rules.json"
def load_rules(path: str | None = None) -> dict:
"""Load rules from file; create default file if missing."""
p = pathlib.Path(path) if path else rules_path()
if not p.exists():
try:
p.write_text(json.dumps(DEFAULT_RULES, ensure_ascii=False, indent=2), encoding="utf-8")
logger.info("Created default event_rules.json at %s", p)
except OSError:
logger.warning("Cannot write default rules to %s", p)
return json.loads(json.dumps(DEFAULT_RULES))
try:
data = json.loads(p.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError) as e:
logger.error("Failed to load rules from %s: %s", p, e)
return json.loads(json.dumps(DEFAULT_RULES))
# merge with defaults to fill missing keys
merged = json.loads(json.dumps(DEFAULT_RULES))
for section in ("enabled", "templates"):
if isinstance(data.get(section), dict):
merged[section].update({k: v for k, v in data[section].items() if k in merged[section]})
if isinstance(data.get("skip_free_gift"), bool):
merged["skip_free_gift"] = data["skip_free_gift"]
return merged
def save_rules(rules: dict, path: str | None = None) -> None:
p = pathlib.Path(path) if path else rules_path()
p.write_text(json.dumps(rules, ensure_ascii=False, indent=2), encoding="utf-8")
logger.info("Saved event rules to %s", p)
def render_template(template: str, **vars_dict) -> str:
"""Render a template with given variables. Missing variables become empty.
Unknown variables are kept as-is (so users can see typos in the UI log)."""
if not template:
return ""
def _replace(m: re.Match) -> str:
name = m.group(1)
return str(vars_dict.get(name, "")) if name in vars_dict else m.group(0)
return _TEMPLATE_RE.sub(_replace, template)
def is_enabled(rules: dict, event: str) -> bool:
return bool(rules.get("enabled", {}).get(event, False))
def get_template(rules: dict, event: str) -> str:
return str(rules.get("templates", {}).get(event, ""))
def event_text(rules: dict, event: str, **vars_dict) -> str:
"""Build the speech text for an event, or '' if disabled/empty template."""
if not is_enabled(rules, event):
return ""
text = render_template(get_template(rules, event), **vars_dict)
return text.strip()
# ── guard level mapping ─────────────────────────────────────────────────
GUARD_LEVEL_NAMES = {1: "\u603b\u7763", 2: "\u63d0\u7763", 3: "\u8230\u957f"}