linkgravity 1.5.3 → 1.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +141 -19
- package/bin/setup.js +49 -25
- package/package.json +1 -1
- package/src/cogs/voice_cog.py +44 -12
- package/src/config.py +8 -4
package/bin/cli.js
CHANGED
|
@@ -50,6 +50,7 @@ function runPm2(args, silent = true) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
let hasSudoInstructions = false;
|
|
53
|
+
let sudoCommand = null;
|
|
53
54
|
if (silent && result.stdout && (args[0] === 'startup' || args[0] === 'unstartup')) {
|
|
54
55
|
const out = result.stdout.toString();
|
|
55
56
|
const lines = out.split('\n');
|
|
@@ -59,11 +60,8 @@ function runPm2(args, silent = true) {
|
|
|
59
60
|
line.trim().startsWith('sudo su -c') ||
|
|
60
61
|
line.includes('sudo ')
|
|
61
62
|
) {
|
|
62
|
-
console.log(
|
|
63
|
-
`\n\n${color.yellow}⚠ Action Required:${color.reset} To complete setup, copy and paste this command into your terminal:\n`,
|
|
64
|
-
);
|
|
65
|
-
console.log(` ${color.cyan}${line.trim()}${color.reset}\n`);
|
|
66
63
|
hasSudoInstructions = true;
|
|
64
|
+
sudoCommand = line.trim();
|
|
67
65
|
}
|
|
68
66
|
}
|
|
69
67
|
}
|
|
@@ -74,6 +72,27 @@ function runPm2(args, silent = true) {
|
|
|
74
72
|
}
|
|
75
73
|
process.exit(result.status);
|
|
76
74
|
}
|
|
75
|
+
|
|
76
|
+
return { hasSudoInstructions, sudoCommand };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function runSudoStepThen(sudoCommand, successMessage) {
|
|
80
|
+
// stdin stays inherited so sudo can still prompt for a password on the real terminal;
|
|
81
|
+
// stdout/stderr are captured so pm2's own noise only surfaces if this actually fails.
|
|
82
|
+
const result = spawnSync('sh', ['-c', sudoCommand], { stdio: ['inherit', 'pipe', 'pipe'] });
|
|
83
|
+
if (result.status === 0) {
|
|
84
|
+
success(`${successMessage}\n`);
|
|
85
|
+
} else {
|
|
86
|
+
console.log(`${color.yellow}⚠${color.reset} That didn't complete:\n`);
|
|
87
|
+
const output = [
|
|
88
|
+
(result.stdout || '').toString().trim(),
|
|
89
|
+
(result.stderr || '').toString().trim(),
|
|
90
|
+
]
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.join('\n');
|
|
93
|
+
if (output) console.log(output);
|
|
94
|
+
console.log(`\nRun \`lgy enable\` again to retry.\n`);
|
|
95
|
+
}
|
|
77
96
|
}
|
|
78
97
|
|
|
79
98
|
// Matches a leading timestamp from either loguru or aiohttp's access-log format; only strips the first bracket group so aiohttp's second "[INFO ]" bracket is left alone.
|
|
@@ -223,11 +242,18 @@ function formatUptime(pmUptimeMs) {
|
|
|
223
242
|
return `${days}d ${hours % 24}h`;
|
|
224
243
|
}
|
|
225
244
|
|
|
245
|
+
function visibleLength(s) {
|
|
246
|
+
return String(s).replace(ANSI_ESCAPE, '').length;
|
|
247
|
+
}
|
|
248
|
+
|
|
226
249
|
function renderTable(headers, rows) {
|
|
227
250
|
const widths = headers.map((h, i) =>
|
|
228
|
-
Math.max(h.length, ...rows.map((r) =>
|
|
251
|
+
Math.max(h.length, ...rows.map((r) => visibleLength(r[i]))),
|
|
229
252
|
);
|
|
230
|
-
const pad = (s, w) =>
|
|
253
|
+
const pad = (s, w) => {
|
|
254
|
+
const str = String(s);
|
|
255
|
+
return ` ${str}${' '.repeat(Math.max(0, w - visibleLength(str)))} `;
|
|
256
|
+
};
|
|
231
257
|
const sepLine = (l, m, r) => l + widths.map((w) => '─'.repeat(w + 2)).join(m) + r;
|
|
232
258
|
const rowLine = (cells) => '│' + cells.map((c, i) => pad(c, widths[i])).join('│') + '│';
|
|
233
259
|
|
|
@@ -237,7 +263,7 @@ function renderTable(headers, rows) {
|
|
|
237
263
|
return lines.join('\n');
|
|
238
264
|
}
|
|
239
265
|
|
|
240
|
-
//
|
|
266
|
+
// Must stay in sync with config.py's AGY_BIN resolution.
|
|
241
267
|
function findAgyBin() {
|
|
242
268
|
const envPath = process.env.AGY_BIN_PATH;
|
|
243
269
|
if (envPath && fs.existsSync(envPath)) return envPath;
|
|
@@ -256,12 +282,61 @@ function findAgyBin() {
|
|
|
256
282
|
function getPm2Proc() {
|
|
257
283
|
const jlist = spawnSync('npx', ['-y', 'pm2', 'jlist'], { stdio: 'pipe' });
|
|
258
284
|
if (jlist.status !== 0) return null;
|
|
285
|
+
|
|
286
|
+
// pm2 can print noise before the real JSON (version banners, ANSI escapes, daemon-spawn logs) that
|
|
287
|
+
// can itself contain '[' - try every '[' left-to-right and keep the first one that parses as JSON.
|
|
288
|
+
const out = jlist.stdout.toString().replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '');
|
|
289
|
+
for (let i = 0; i < out.length; i++) {
|
|
290
|
+
if (out[i] !== '[') continue;
|
|
291
|
+
try {
|
|
292
|
+
const procs = JSON.parse(out.slice(i));
|
|
293
|
+
if (Array.isArray(procs)) return procs.find((p) => p.name === LGY_PM2_NAME) || null;
|
|
294
|
+
} catch (e) {}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
console.error(
|
|
298
|
+
`${color.yellow}⚠${color.reset} Couldn't read pm2 status (no valid JSON found in its output). Raw output:\n${out.trim()}`,
|
|
299
|
+
);
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Best-effort: pm2 has no API for "is this registered to start on boot", so this checks the OS directly and returns null (unknown) if that check itself isn't available.
|
|
304
|
+
function isAutostartEnabled() {
|
|
305
|
+
if (isWin) return null;
|
|
259
306
|
try {
|
|
260
|
-
const
|
|
261
|
-
|
|
307
|
+
const user = os.userInfo().username;
|
|
308
|
+
if (process.platform === 'linux') {
|
|
309
|
+
const check = spawnSync('systemctl', ['is-enabled', `pm2-${user}`], { stdio: 'pipe' });
|
|
310
|
+
if (check.error) return null;
|
|
311
|
+
const out = check.stdout.toString().trim();
|
|
312
|
+
if (out === 'enabled') return true;
|
|
313
|
+
if (out === 'disabled' || check.status !== 0) return false;
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
if (process.platform === 'darwin') {
|
|
317
|
+
const plistPath = path.join(
|
|
318
|
+
os.homedir(),
|
|
319
|
+
'Library',
|
|
320
|
+
'LaunchAgents',
|
|
321
|
+
`pm2.${user}.plist`,
|
|
322
|
+
);
|
|
323
|
+
return fs.existsSync(plistPath);
|
|
324
|
+
}
|
|
262
325
|
} catch (e) {
|
|
263
326
|
return null;
|
|
264
327
|
}
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function checkLatestVersionFast(currentVersion) {
|
|
332
|
+
const view = spawnSync('npm', ['view', 'linkgravity', 'version'], {
|
|
333
|
+
stdio: 'pipe',
|
|
334
|
+
timeout: 3000,
|
|
335
|
+
});
|
|
336
|
+
if (view.error || view.status !== 0) return null;
|
|
337
|
+
const latest = view.stdout.toString().trim();
|
|
338
|
+
if (!latest) return null;
|
|
339
|
+
return { latest, upToDate: latest === currentVersion };
|
|
265
340
|
}
|
|
266
341
|
|
|
267
342
|
if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
@@ -390,6 +465,8 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
|
390
465
|
}
|
|
391
466
|
}
|
|
392
467
|
|
|
468
|
+
const daemonAlive = !!proc && proc.pm2_env.status === 'online';
|
|
469
|
+
|
|
393
470
|
const rows = Object.entries(PLATFORMS).map(([key, def]) => {
|
|
394
471
|
const { enabled } = platformState(key, settings);
|
|
395
472
|
const sessionCount = Object.values(sessions).filter((s) => s.platform === key).length;
|
|
@@ -397,17 +474,54 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
|
397
474
|
let connection = '-';
|
|
398
475
|
let since = '-';
|
|
399
476
|
if (enabled) {
|
|
400
|
-
if (!
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
else if (h
|
|
405
|
-
|
|
477
|
+
if (!daemonAlive) {
|
|
478
|
+
// health.json freezes at its last value if the process was killed outright (kill -9, OOM, reboot) instead of exiting cleanly, so don't trust it once pm2 confirms the daemon isn't actually running.
|
|
479
|
+
connection = `${color.red}down${color.reset}`;
|
|
480
|
+
since = h && h.at ? `last seen ${formatUptime(new Date(h.at).getTime())} ago` : '-';
|
|
481
|
+
} else if (!h) {
|
|
482
|
+
connection = 'unknown';
|
|
483
|
+
} else {
|
|
484
|
+
if (h.status === 'running') connection = `${color.green}connected${color.reset}`;
|
|
485
|
+
else if (h.status === 'connecting') connection = 'connecting...';
|
|
486
|
+
else if (h.status === 'error')
|
|
487
|
+
connection = `${color.red}error: ${h.detail || '?'}${color.reset}`;
|
|
488
|
+
else if (h.status === 'stopped') connection = 'stopped';
|
|
489
|
+
if (h.at) since = formatUptime(new Date(h.at).getTime());
|
|
490
|
+
}
|
|
406
491
|
}
|
|
407
492
|
return [def.label, enabled ? 'yes' : 'no', connection, since, String(sessionCount)];
|
|
408
493
|
});
|
|
409
494
|
console.log(renderTable(['platform', 'enabled', 'connection', 'since', 'sessions'], rows));
|
|
410
495
|
console.log();
|
|
496
|
+
|
|
497
|
+
const pkg = require('../package.json');
|
|
498
|
+
const versionCheck = checkLatestVersionFast(pkg.version);
|
|
499
|
+
const versionLine =
|
|
500
|
+
versionCheck === null
|
|
501
|
+
? pkg.version
|
|
502
|
+
: versionCheck.upToDate
|
|
503
|
+
? `${pkg.version} (up to date)`
|
|
504
|
+
: `${pkg.version} ${color.yellow}(v${versionCheck.latest} available - run \`lgy update\`)${color.reset}`;
|
|
505
|
+
|
|
506
|
+
const agyPath = findAgyBin();
|
|
507
|
+
const agyLine = agyPath ? `found (${agyPath})` : `${color.yellow}not found${color.reset}`;
|
|
508
|
+
|
|
509
|
+
const autostart = isAutostartEnabled();
|
|
510
|
+
const autostartLine =
|
|
511
|
+
autostart === null
|
|
512
|
+
? 'unknown'
|
|
513
|
+
: autostart
|
|
514
|
+
? `${color.green}enabled${color.reset}`
|
|
515
|
+
: 'disabled';
|
|
516
|
+
|
|
517
|
+
for (const [label, value] of [
|
|
518
|
+
['version', versionLine],
|
|
519
|
+
['agy', agyLine],
|
|
520
|
+
['autostart', autostartLine],
|
|
521
|
+
]) {
|
|
522
|
+
console.log(`${label.padEnd(10)} ${value}`);
|
|
523
|
+
}
|
|
524
|
+
console.log();
|
|
411
525
|
} else if (cmd === 'enable') {
|
|
412
526
|
if (isWin) {
|
|
413
527
|
console.log(
|
|
@@ -417,9 +531,13 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
|
417
531
|
process.exit(1);
|
|
418
532
|
}
|
|
419
533
|
info('Registering LinkGravity to start on system boot...');
|
|
420
|
-
runPm2(['startup']);
|
|
534
|
+
const { hasSudoInstructions, sudoCommand } = runPm2(['startup']);
|
|
421
535
|
runPm2(['save']);
|
|
422
|
-
|
|
536
|
+
if (hasSudoInstructions && sudoCommand) {
|
|
537
|
+
runSudoStepThen(sudoCommand, 'Auto-start configuration saved.');
|
|
538
|
+
} else {
|
|
539
|
+
success('Auto-start configuration saved.\n');
|
|
540
|
+
}
|
|
423
541
|
} else if (cmd === 'disable') {
|
|
424
542
|
if (isWin) {
|
|
425
543
|
console.log(
|
|
@@ -429,9 +547,13 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
|
429
547
|
process.exit(1);
|
|
430
548
|
}
|
|
431
549
|
info('Removing LinkGravity from system boot...');
|
|
432
|
-
runPm2(['unstartup']);
|
|
550
|
+
const { hasSudoInstructions, sudoCommand } = runPm2(['unstartup']);
|
|
433
551
|
runPm2(['save']);
|
|
434
|
-
|
|
552
|
+
if (hasSudoInstructions && sudoCommand) {
|
|
553
|
+
runSudoStepThen(sudoCommand, 'Auto-start configuration removed.');
|
|
554
|
+
} else {
|
|
555
|
+
success('Auto-start configuration removed.\n');
|
|
556
|
+
}
|
|
435
557
|
} else if (cmd === 'setup' || cmd === 'init') {
|
|
436
558
|
const runSetup = require('./setup');
|
|
437
559
|
runSetup().catch((err) => {
|
package/bin/setup.js
CHANGED
|
@@ -33,25 +33,39 @@ async function collectSessionScopes(existingScopes) {
|
|
|
33
33
|
'Server / Channel Access',
|
|
34
34
|
);
|
|
35
35
|
|
|
36
|
+
if (hasExisting) {
|
|
37
|
+
const summary = existingScopes
|
|
38
|
+
.map(
|
|
39
|
+
(s) =>
|
|
40
|
+
`${s.guild_id}${s.channel_ids.length ? ` (channels: ${s.channel_ids.join(', ')})` : ' (whole server)'}`,
|
|
41
|
+
)
|
|
42
|
+
.join('; ');
|
|
43
|
+
p.note(`Current server/channel settings: ${summary}`, 'Current Setting');
|
|
44
|
+
|
|
45
|
+
const change = await p.confirm({
|
|
46
|
+
message: 'Change the server/channel access list?',
|
|
47
|
+
initialValue: false,
|
|
48
|
+
});
|
|
49
|
+
if (p.isCancel(change)) {
|
|
50
|
+
p.cancel('Setup cancelled.');
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
if (!change) return null; // keep existing config untouched
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
let isFirst = true;
|
|
37
57
|
while (true) {
|
|
38
|
-
const promptSuffix =
|
|
39
|
-
isFirst && hasExisting
|
|
40
|
-
? ' (leave empty to keep your current server/channel settings entirely unchanged)'
|
|
41
|
-
: ' (leave empty if you have no more servers to add)';
|
|
42
|
-
|
|
43
58
|
const guildId = await p.text({
|
|
44
|
-
message:
|
|
59
|
+
message: isFirst
|
|
60
|
+
? 'Server (Guild) ID to allow. Right-click the SERVER NAME (not a channel) → Copy Server ID ' +
|
|
61
|
+
'(leave empty if done - an empty list means /new works nowhere):'
|
|
62
|
+
: 'Another server ID to add (leave empty if done):',
|
|
45
63
|
});
|
|
46
64
|
if (p.isCancel(guildId)) {
|
|
47
65
|
p.cancel('Setup cancelled.');
|
|
48
66
|
process.exit(0);
|
|
49
67
|
}
|
|
50
|
-
|
|
51
|
-
if (!guildId) {
|
|
52
|
-
if (isFirst) return null; // signal: user wants to keep existing config untouched
|
|
53
|
-
break;
|
|
54
|
-
}
|
|
68
|
+
if (!guildId) break;
|
|
55
69
|
isFirst = false;
|
|
56
70
|
|
|
57
71
|
const channelIds = [];
|
|
@@ -103,30 +117,41 @@ async function collectUserIds(existingIds, platformLabel) {
|
|
|
103
117
|
const hasExisting = existingIds && existingIds.length > 0;
|
|
104
118
|
|
|
105
119
|
p.note(
|
|
106
|
-
'ONLY these users can use the bot (leave completely empty
|
|
120
|
+
'ONLY these users can use the bot (leave completely empty to allow EVERYONE). ' +
|
|
107
121
|
'Not related to DMs - this only gates the channel/threads configured above.',
|
|
108
122
|
`Allowed ${platformLabel} Users`,
|
|
109
123
|
);
|
|
110
124
|
|
|
125
|
+
if (hasExisting) {
|
|
126
|
+
p.note(
|
|
127
|
+
`Current allowed ${platformLabel} users: ${existingIds.join(', ')}`,
|
|
128
|
+
'Current Setting',
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const change = await p.confirm({
|
|
132
|
+
message: `Change the allowed-user list for ${platformLabel}?`,
|
|
133
|
+
initialValue: false,
|
|
134
|
+
});
|
|
135
|
+
if (p.isCancel(change)) {
|
|
136
|
+
p.cancel('Setup cancelled.');
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
if (!change) return null; // keep existing config untouched
|
|
140
|
+
}
|
|
141
|
+
|
|
111
142
|
let isFirst = true;
|
|
112
143
|
while (true) {
|
|
113
|
-
const promptSuffix =
|
|
114
|
-
isFirst && hasExisting
|
|
115
|
-
? ' (leave empty to keep your current allowed-user settings entirely unchanged)'
|
|
116
|
-
: ' (leave empty if you have no more users to add)';
|
|
117
|
-
|
|
118
144
|
const userId = await p.text({
|
|
119
|
-
message:
|
|
145
|
+
message: isFirst
|
|
146
|
+
? `${platformLabel} User ID to allow (leave empty to allow EVERYONE):`
|
|
147
|
+
: `Another ${platformLabel} user ID to allow (leave empty if done):`,
|
|
120
148
|
});
|
|
121
149
|
if (p.isCancel(userId)) {
|
|
122
150
|
p.cancel('Setup cancelled.');
|
|
123
151
|
process.exit(0);
|
|
124
152
|
}
|
|
125
153
|
|
|
126
|
-
if (!userId)
|
|
127
|
-
if (isFirst) return null; // signal: keep existing config untouched
|
|
128
|
-
break;
|
|
129
|
-
}
|
|
154
|
+
if (!userId) break;
|
|
130
155
|
isFirst = false;
|
|
131
156
|
|
|
132
157
|
ids.push(userId.trim());
|
|
@@ -390,13 +415,12 @@ async function platformMenu(key) {
|
|
|
390
415
|
);
|
|
391
416
|
if (configured)
|
|
392
417
|
options.push({ value: 'edit', label: 'Edit settings (token, access, etc.)' });
|
|
393
|
-
options.push({ value: 'back', label: '← Back' });
|
|
394
418
|
|
|
395
419
|
const action = await p.select({
|
|
396
|
-
message: `${def.label} — currently ${enabled ? 'ON' : 'OFF'}${configured ? '' : ' (not configured)'}`,
|
|
420
|
+
message: `${def.label} — currently ${enabled ? 'ON' : 'OFF'}${configured ? '' : ' (not configured)'} (Esc to go back)`,
|
|
397
421
|
options,
|
|
398
422
|
});
|
|
399
|
-
if (p.isCancel(action)
|
|
423
|
+
if (p.isCancel(action)) return;
|
|
400
424
|
|
|
401
425
|
if (action === 'off') {
|
|
402
426
|
updateSettings({ [`${key}_enabled`]: false });
|
package/package.json
CHANGED
package/src/cogs/voice_cog.py
CHANGED
|
@@ -92,7 +92,7 @@ class VoiceCog(commands.Cog):
|
|
|
92
92
|
opts.append(app_commands.Choice(name=str(v), value=v))
|
|
93
93
|
return opts
|
|
94
94
|
|
|
95
|
-
async def
|
|
95
|
+
async def interrupt_threshold_autocomplete(
|
|
96
96
|
self, interaction: discord.Interaction, current: str
|
|
97
97
|
) -> list[app_commands.Choice[int]]:
|
|
98
98
|
current_val = int(self.bot_settings.get("voice_threshold", 3000))
|
|
@@ -105,6 +105,19 @@ class VoiceCog(commands.Cog):
|
|
|
105
105
|
opts.append(app_commands.Choice(name=str(v), value=v))
|
|
106
106
|
return opts
|
|
107
107
|
|
|
108
|
+
async def wake_sensitivity_autocomplete(
|
|
109
|
+
self, interaction: discord.Interaction, current: str
|
|
110
|
+
) -> list[app_commands.Choice[float]]:
|
|
111
|
+
current_val = float(self.bot_settings.get("wake_threshold", 0.4))
|
|
112
|
+
opts = []
|
|
113
|
+
if str(current_val) in current or not current:
|
|
114
|
+
opts.append(app_commands.Choice(name=f"{current_val} (current)", value=current_val))
|
|
115
|
+
|
|
116
|
+
for v in [0.2, 0.3, 0.4, 0.5, 0.6]:
|
|
117
|
+
if v != current_val and len(opts) < 25:
|
|
118
|
+
opts.append(app_commands.Choice(name=str(v), value=v))
|
|
119
|
+
return opts
|
|
120
|
+
|
|
108
121
|
async def tts_voice_autocomplete(
|
|
109
122
|
self, interaction: discord.Interaction, current: str
|
|
110
123
|
) -> list[app_commands.Choice[str]]:
|
|
@@ -282,12 +295,13 @@ class VoiceCog(commands.Cog):
|
|
|
282
295
|
|
|
283
296
|
@app_commands.command(
|
|
284
297
|
name="sound",
|
|
285
|
-
description="Configure voice settings (Wake word, active time,
|
|
298
|
+
description="Configure voice settings (Wake word, active time, thresholds, TTS voice/speed, TTS on/off)",
|
|
286
299
|
)
|
|
287
300
|
@app_commands.describe(
|
|
288
301
|
wake_word="The single word/phrase that wakes the bot (recorded in your voice)",
|
|
289
302
|
active_times="Duration in seconds the bot stays awake",
|
|
290
|
-
|
|
303
|
+
interrupt_threshold="Mic volume that interrupts (barges into) TTS playback (1000~10000)",
|
|
304
|
+
wake_sensitivity="Wake word match sensitivity (0.1~0.9, lower = easier to trigger but more false wakes)",
|
|
291
305
|
tts_voice="Select the AI TTS voice",
|
|
292
306
|
tts_enabled="Turn Text-to-Speech ON or OFF",
|
|
293
307
|
tts_speed="TTS playback speed multiplier, e.g. 1.3 for 1.3x (0.5~2.0)",
|
|
@@ -295,7 +309,8 @@ class VoiceCog(commands.Cog):
|
|
|
295
309
|
)
|
|
296
310
|
@app_commands.autocomplete(
|
|
297
311
|
active_times=active_times_autocomplete,
|
|
298
|
-
|
|
312
|
+
interrupt_threshold=interrupt_threshold_autocomplete,
|
|
313
|
+
wake_sensitivity=wake_sensitivity_autocomplete,
|
|
299
314
|
tts_voice=tts_voice_autocomplete,
|
|
300
315
|
tts_enabled=tts_enabled_autocomplete,
|
|
301
316
|
tts_speed=tts_speed_autocomplete,
|
|
@@ -306,7 +321,8 @@ class VoiceCog(commands.Cog):
|
|
|
306
321
|
interaction: discord.Interaction,
|
|
307
322
|
wake_word: str = None,
|
|
308
323
|
active_times: int = None,
|
|
309
|
-
|
|
324
|
+
interrupt_threshold: int = None,
|
|
325
|
+
wake_sensitivity: float = None,
|
|
310
326
|
tts_voice: str = None,
|
|
311
327
|
tts_enabled: str = None,
|
|
312
328
|
tts_speed: float = None,
|
|
@@ -321,7 +337,8 @@ class VoiceCog(commands.Cog):
|
|
|
321
337
|
if (
|
|
322
338
|
wake_word is None
|
|
323
339
|
and active_times is None
|
|
324
|
-
and
|
|
340
|
+
and interrupt_threshold is None
|
|
341
|
+
and wake_sensitivity is None
|
|
325
342
|
and tts_voice is None
|
|
326
343
|
and tts_enabled is None
|
|
327
344
|
and tts_speed is None
|
|
@@ -329,7 +346,8 @@ class VoiceCog(commands.Cog):
|
|
|
329
346
|
):
|
|
330
347
|
curr_wake = (self.bot_settings.get("wake_words") or {}).get(str(interaction.user.id), "None")
|
|
331
348
|
curr_timer = self.bot_settings.get("active_timer", 60)
|
|
332
|
-
|
|
349
|
+
curr_interrupt_thresh = self.bot_settings.get("voice_threshold", 3000)
|
|
350
|
+
curr_wake_sens = self.bot_settings.get("wake_threshold", 0.4)
|
|
333
351
|
curr_tts = self.bot_settings.get("tts_voice", "en-US-AriaNeural")
|
|
334
352
|
curr_tts_on = "ON" if self.bot_settings.get("tts_enabled", True) else "OFF"
|
|
335
353
|
curr_tts_speed = self.bot_settings.get("tts_speed", 1.0)
|
|
@@ -339,7 +357,8 @@ class VoiceCog(commands.Cog):
|
|
|
339
357
|
embed.add_field(name="🎙️ Wake Word", value=f"`{curr_wake}`", inline=False)
|
|
340
358
|
embed.add_field(name="🔒 Wake Word Required", value=f"`{'ON' if curr_required else 'OFF'}`", inline=False)
|
|
341
359
|
embed.add_field(name="⏱️ Active Time", value=f"`{curr_timer}s`", inline=False)
|
|
342
|
-
embed.add_field(name="
|
|
360
|
+
embed.add_field(name="🎯 Wake Sensitivity", value=f"`{curr_wake_sens}`", inline=False)
|
|
361
|
+
embed.add_field(name="🔊 Interrupt Threshold", value=f"`{curr_interrupt_thresh}`", inline=False)
|
|
343
362
|
embed.add_field(name="🗣️ TTS Voice", value=f"`{curr_tts}`", inline=False)
|
|
344
363
|
embed.add_field(name="🔊 TTS Enabled", value=f"`{curr_tts_on}`", inline=False)
|
|
345
364
|
embed.add_field(name="⏩ TTS Speed", value=f"`{curr_tts_speed}x`", inline=False)
|
|
@@ -353,12 +372,25 @@ class VoiceCog(commands.Cog):
|
|
|
353
372
|
if active_times is not None:
|
|
354
373
|
self.bot_settings["active_timer"] = active_times
|
|
355
374
|
updated.append(f"⏱️ Active Timer: `{active_times}s`")
|
|
356
|
-
if
|
|
357
|
-
self.bot_settings["voice_threshold"] =
|
|
358
|
-
updated.append(f"🔊 Threshold: `{
|
|
375
|
+
if interrupt_threshold is not None:
|
|
376
|
+
self.bot_settings["voice_threshold"] = interrupt_threshold
|
|
377
|
+
updated.append(f"🔊 Interrupt Threshold: `{interrupt_threshold}`")
|
|
378
|
+
try:
|
|
379
|
+
async with aiohttp.ClientSession(timeout=NODE_REQUEST_TIMEOUT) as session:
|
|
380
|
+
await session.post(f"{NODE_VOICE_API}/set_config", json={"voice_threshold": interrupt_threshold})
|
|
381
|
+
except aiohttp.ClientError as e:
|
|
382
|
+
self.logger.warning(f"Node.js sync failed for {interaction.guild_id}: {e}")
|
|
383
|
+
updated.append(f"(⚠️ Node.js Sync Failed: {e})")
|
|
384
|
+
except asyncio.TimeoutError:
|
|
385
|
+
self.logger.warning(f"Node.js sync timeout for {interaction.guild_id}")
|
|
386
|
+
updated.append("(⚠️ Node.js Sync Timeout)")
|
|
387
|
+
if wake_sensitivity is not None:
|
|
388
|
+
clamped_wake = max(0.05, min(0.95, wake_sensitivity))
|
|
389
|
+
self.bot_settings["wake_threshold"] = clamped_wake
|
|
390
|
+
updated.append(f"🎯 Wake Sensitivity: `{clamped_wake}`")
|
|
359
391
|
try:
|
|
360
392
|
async with aiohttp.ClientSession(timeout=NODE_REQUEST_TIMEOUT) as session:
|
|
361
|
-
await session.post(f"{NODE_VOICE_API}/set_config", json={"
|
|
393
|
+
await session.post(f"{NODE_VOICE_API}/set_config", json={"wake_threshold": clamped_wake})
|
|
362
394
|
except aiohttp.ClientError as e:
|
|
363
395
|
self.logger.warning(f"Node.js sync failed for {interaction.guild_id}: {e}")
|
|
364
396
|
updated.append(f"(⚠️ Node.js Sync Failed: {e})")
|
package/src/config.py
CHANGED
|
@@ -112,11 +112,15 @@ TTS_VOICE = bot_settings.get("tts_voice", "ko-KR-SunHiNeural")
|
|
|
112
112
|
|
|
113
113
|
def is_allowed_session_channel(channel) -> bool:
|
|
114
114
|
"""True if a new agy session may be started from this channel (via
|
|
115
|
-
/new).
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
/new). DMs have no guild to scope, so they're always allowed here -
|
|
116
|
+
gating for DMs is handled separately via ALLOWED_IDS. For a guild
|
|
117
|
+
channel, it's allowed if its server is in SESSION_SCOPES AND either
|
|
118
|
+
that server has no channel restriction (whole-server access) or this
|
|
119
|
+
specific channel is in its allowed list."""
|
|
118
120
|
guild = getattr(channel, "guild", None)
|
|
119
|
-
if not guild
|
|
121
|
+
if not guild:
|
|
122
|
+
return True
|
|
123
|
+
if guild.id not in SESSION_SCOPES:
|
|
120
124
|
return False
|
|
121
125
|
allowed_channels = SESSION_SCOPES[guild.id]
|
|
122
126
|
return allowed_channels is None or channel.id in allowed_channels
|