"""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"}