feat: user-customizable event voice rules (gift/blind box/enter/follow/like/guard/super chat)

- New event_rules.py: load/save event_rules.json, robust template renderer
  (missing vars -> empty, empty template -> muted), guard level mapping
- danmaku_handler.py: custom SEND_GIFT callback to extract blind_gift_name;
  implements all event callbacks feeding the TTS queue per user rules
- server.py: GET/POST /api/rules, load rules at startup, pass to client
- Web UI: 'Event Voice Rules' panel (per-event toggle + template + save)
- main.py CLI loads rules; build.py bundles event_rules.json
- skip_free_gift option to mute silver free gifts
- Rebuild portable zip (83.8 MB)
This commit is contained in:
2026-08-08 16:17:28 +08:00
parent f4c99b1692
commit f3a71e908e
8 changed files with 468 additions and 16 deletions
+111
View File
@@ -202,6 +202,27 @@
animation: spin 0.8s linear infinite; margin: 0 auto;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Event rules */
.rule-row {
display: flex; gap: 10px; align-items: center;
margin-bottom: 8px;
}
.rule-row .rule-name {
width: 110px; font-size: 0.82rem; color: var(--text-dim);
flex-shrink: 0;
}
.rule-row input[type="checkbox"] {
accent-color: var(--accent); width: 15px; height: 15px;
flex-shrink: 0;
}
.rule-row input[type="text"] {
flex: 1; padding: 6px 10px; border-radius: 6px;
border: 1px solid var(--border);
background: var(--input-bg); color: var(--text);
font-size: 0.85rem; outline: none;
}
.rule-row input[type="text"]:focus { border-color: var(--accent); }
</style>
</head>
<body>
@@ -280,6 +301,22 @@
</div>
</div>
<!-- Event Voice Rules Card -->
<div class="card" id="rules-card">
<h2>Event Voice Rules</h2>
<div class="rule-list" id="rule-list"></div>
<div class="form-row" style="margin-top:10px; align-items:center">
<label class="chk-label"><input type="checkbox" id="rule-skip-free"> Skip free gifts</label>
<span style="flex:1"></span>
<button class="btn btn-login" onclick="saveRules()">Save Rules</button>
</div>
<div class="rule-hint" style="margin-top:8px; font-size:0.78rem; color:var(--text-dim); line-height:1.6">
Variables: {uname} {msg} | {viewer_name} {gift_name} {gift_num} {blind_box_gift} |
{ships_viewer_name} {ships_level} {ships_num} {guard_days} | {super_chat_content} {super_chat_price} |
{interact_type} {user_uid} — empty template = event muted.
</div>
</div>
<!-- Status Card -->
<div class="card">
<div class="status-bar">
@@ -530,7 +567,81 @@ async function checkExistingLogin() {
} catch(e) {}
}
// ── Event voice rules ────────────────────────────────────────────────────
const RULE_LABELS = {
danmaku: 'Danmaku', gift: 'Gift', blind_box: 'Blind Box',
enter_room: 'Enter Room', follow: 'Follow', like: 'Like',
interact_other: 'Interact', buy_guard: 'Buy Guard',
guard_toast: 'Guard Toast', super_chat: 'Super Chat',
};
async function loadRules() {
try {
const resp = await fetch('/api/rules');
const rules = await resp.json();
renderRules(rules);
} catch(e) {}
}
function renderRules(rules) {
const list = document.getElementById('rule-list');
list.innerHTML = '';
for (const key of Object.keys(RULE_LABELS)) {
const row = document.createElement('div');
row.className = 'rule-row';
const name = document.createElement('span');
name.className = 'rule-name';
name.textContent = RULE_LABELS[key];
const chk = document.createElement('input');
chk.type = 'checkbox';
chk.dataset.key = key;
chk.checked = !!(rules.enabled && rules.enabled[key]);
const input = document.createElement('input');
input.type = 'text';
input.dataset.key = key;
input.placeholder = 'Empty = muted';
input.value = (rules.templates && rules.templates[key]) || '';
row.appendChild(name);
row.appendChild(chk);
row.appendChild(input);
list.appendChild(row);
}
const skip = document.getElementById('rule-skip-free');
skip.checked = !!rules.skip_free_gift;
}
async function saveRules() {
const enabled = {};
const templates = {};
document.querySelectorAll('#rule-list input[type="checkbox"]').forEach(c => {
enabled[c.dataset.key] = c.checked;
});
document.querySelectorAll('#rule-list input[type="text"]').forEach(i => {
templates[i.dataset.key] = i.value;
});
const payload = {
enabled,
templates,
skip_free_gift: document.getElementById('rule-skip-free').checked,
};
try {
const resp = await fetch('/api/rules', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const data = await resp.json();
if (data.error) { alert('Save failed: ' + data.error); return; }
renderRules(data.rules);
alert('Rules saved. Restart TTS to apply.');
} catch(e) {
alert('Save failed: ' + e.message);
}
}
checkExistingLogin();
loadRules();
</script>
</body>
</html>