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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user