linkgravity 1.2.0 → 1.2.1
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 +68 -21
- package/bin/setup.js +1 -3
- package/package.json +1 -1
- package/src/cogs/general_cog.py +1 -4
- package/src/cogs/voice_cog.py +6 -15
- package/src/config.py +2 -6
package/bin/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ const color = {
|
|
|
14
14
|
green: '\x1b[32m',
|
|
15
15
|
cyan: '\x1b[36m',
|
|
16
16
|
yellow: '\x1b[33m',
|
|
17
|
+
red: '\x1b[31m',
|
|
17
18
|
dim: '\x1b[2m',
|
|
18
19
|
};
|
|
19
20
|
|
|
@@ -76,31 +77,71 @@ function runPm2(args, silent = true) {
|
|
|
76
77
|
// Only strips the FIRST bracket group if present, so aiohttp's second
|
|
77
78
|
// "[INFO ]" bracket (not a timestamp) is left alone.
|
|
78
79
|
const TIMESTAMP_PREFIX = /^\[?\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}\]?\s*/;
|
|
80
|
+
// loguru's colorize=True puts an ANSI code before the timestamp digits, breaking the '^' anchor above.
|
|
81
|
+
// eslint-disable-next-line no-control-regex
|
|
82
|
+
const ANSI_ESCAPE = /\x1b\[[0-9;]*m/g;
|
|
83
|
+
|
|
84
|
+
const LEVEL_COLOR = {
|
|
85
|
+
DEBUG: color.dim,
|
|
86
|
+
INFO: color.cyan,
|
|
87
|
+
WARNING: color.yellow,
|
|
88
|
+
ERROR: color.red,
|
|
89
|
+
CRITICAL: color.red,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Recolors the level word ourselves so Python (loguru) and Node (plain console.log) lines match.
|
|
93
|
+
function colorizeLevel(line) {
|
|
94
|
+
return line.replace(/\b(DEBUG|INFO|WARNING|ERROR|CRITICAL)\b/, (match) => {
|
|
95
|
+
const c = LEVEL_COLOR[match];
|
|
96
|
+
return c ? `${c}${match}${color.reset}` : match;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
79
99
|
|
|
80
100
|
function runPm2LogsClean(args, showStamps = false) {
|
|
81
101
|
const cp = spawn('npx', ['-y', 'pm2', ...args], { cwd: path.join(__dirname, '..') });
|
|
82
102
|
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
) {
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
console.log(showStamps ? line : line.replace(TIMESTAMP_PREFIX, ''));
|
|
103
|
+
const printLine = (line) => {
|
|
104
|
+
if (line.trim().length === 0) return;
|
|
105
|
+
if (
|
|
106
|
+
line.includes('In-memory PM2') ||
|
|
107
|
+
line.includes('pm2 update') ||
|
|
108
|
+
line.includes('[TAILING]') ||
|
|
109
|
+
line.includes('.pm2/logs/lgy') ||
|
|
110
|
+
line.includes('In memory PM2 version') ||
|
|
111
|
+
line.includes('Local PM2 version') ||
|
|
112
|
+
line.match(/^>+ /)
|
|
113
|
+
) {
|
|
114
|
+
return;
|
|
99
115
|
}
|
|
116
|
+
const clean = line.replace(ANSI_ESCAPE, '');
|
|
117
|
+
const displayLine = colorizeLevel(showStamps ? clean : clean.replace(TIMESTAMP_PREFIX, ''));
|
|
118
|
+
console.log(displayLine);
|
|
100
119
|
};
|
|
101
120
|
|
|
102
|
-
|
|
103
|
-
|
|
121
|
+
// A line can arrive split across two 'data' events, so buffer until '\n' is seen.
|
|
122
|
+
function makeChunkHandler() {
|
|
123
|
+
let buffer = '';
|
|
124
|
+
const handler = (data) => {
|
|
125
|
+
buffer += data.toString();
|
|
126
|
+
const lines = buffer.split('\n');
|
|
127
|
+
buffer = lines.pop(); // last element: '' if buffer ended in '\n', else the incomplete tail
|
|
128
|
+
for (const line of lines) printLine(line);
|
|
129
|
+
};
|
|
130
|
+
handler.flush = () => {
|
|
131
|
+
if (buffer) printLine(buffer);
|
|
132
|
+
buffer = '';
|
|
133
|
+
};
|
|
134
|
+
return handler;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const stdoutHandler = makeChunkHandler();
|
|
138
|
+
const stderrHandler = makeChunkHandler();
|
|
139
|
+
cp.stdout.on('data', stdoutHandler);
|
|
140
|
+
cp.stderr.on('data', stderrHandler);
|
|
141
|
+
cp.on('close', () => {
|
|
142
|
+
stdoutHandler.flush();
|
|
143
|
+
stderrHandler.flush();
|
|
144
|
+
});
|
|
104
145
|
}
|
|
105
146
|
|
|
106
147
|
function verifyStartup() {
|
|
@@ -168,7 +209,14 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
|
168
209
|
runPm2(['restart', 'lgy', '--update-env']);
|
|
169
210
|
verifyStartup();
|
|
170
211
|
} else if (cmd === 'logs') {
|
|
171
|
-
|
|
212
|
+
const SHORT_FLAGS = ['-f', '-n', '-t'];
|
|
213
|
+
let args = process.argv.slice(3).flatMap((arg) => {
|
|
214
|
+
// Only split bare combined short flags (e.g. "-fn" -> "-f", "-n"), not "--long" flags.
|
|
215
|
+
if (!/^-[a-z]{2,}$/.test(arg)) return [arg];
|
|
216
|
+
const chars = arg.slice(1).split('');
|
|
217
|
+
if (!chars.every((c) => SHORT_FLAGS.includes(`-${c}`))) return [arg];
|
|
218
|
+
return chars.map((c) => `-${c}`);
|
|
219
|
+
});
|
|
172
220
|
let pm2Args = ['logs', 'lgy'];
|
|
173
221
|
let isFollow = false;
|
|
174
222
|
let showStamps = false;
|
|
@@ -265,8 +313,7 @@ if (cmd === 'version' || cmd === '-v' || cmd === '--version') {
|
|
|
265
313
|
if (restartResult.status === 0) {
|
|
266
314
|
verifyStartup();
|
|
267
315
|
} else if ((restartResult.stderr || '').toString().includes('not found')) {
|
|
268
|
-
//
|
|
269
|
-
// of reporting a restart that never had anything to restart.
|
|
316
|
+
// Wasn't running before the update - start fresh instead of a false "restarted".
|
|
270
317
|
info("Daemon wasn't running - starting it fresh...");
|
|
271
318
|
runPm2(['start', botPath, '--interpreter', pythonExe, '--name', 'lgy']);
|
|
272
319
|
verifyStartup();
|
package/bin/setup.js
CHANGED
|
@@ -278,9 +278,7 @@ async function runSetup() {
|
|
|
278
278
|
} else {
|
|
279
279
|
const stderr = (restartResult.stderr || '').toString();
|
|
280
280
|
if (stderr.includes('not found')) {
|
|
281
|
-
// Nothing to restart yet
|
|
282
|
-
// reset e.g. after a reboot without `pm2 save`) - start it instead of
|
|
283
|
-
// reporting a false "restarted" success.
|
|
281
|
+
// Nothing to restart yet - start it instead of a false "restarted".
|
|
284
282
|
const botPath = path.join(__dirname, '..', 'src', 'main.py');
|
|
285
283
|
const startResult = spawnSync(
|
|
286
284
|
'npx',
|
package/package.json
CHANGED
package/src/cogs/general_cog.py
CHANGED
|
@@ -273,10 +273,7 @@ class GeneralCog(commands.Cog):
|
|
|
273
273
|
prev_tts.cancel()
|
|
274
274
|
session_manager.remove_tts_task(thread_id)
|
|
275
275
|
|
|
276
|
-
#
|
|
277
|
-
# reusing conv_id risks silently hanging on the next message.
|
|
278
|
-
# conversation_id must be cleared too, not just status: the
|
|
279
|
-
# text path only treats a session as pending when both are unset.
|
|
276
|
+
# Must clear conversation_id too, not just status - pending requires both unset.
|
|
280
277
|
session_manager.set_session(thread_id, {**session, "status": "pending", "conversation_id": None})
|
|
281
278
|
|
|
282
279
|
from core.agy_runner import stop_active_process
|
package/src/cogs/voice_cog.py
CHANGED
|
@@ -44,8 +44,7 @@ class VoiceCog(commands.Cog):
|
|
|
44
44
|
self.save_bot_settings = save_bot_settings
|
|
45
45
|
self.logger = logger
|
|
46
46
|
self._voice_state = {}
|
|
47
|
-
# guild_id -> in-flight handle_stt_input task
|
|
48
|
-
# utterance cancels the previous one so replies can't race out of order.
|
|
47
|
+
# guild_id -> in-flight handle_stt_input task; a new utterance cancels the previous one.
|
|
49
48
|
self._active_turns = {}
|
|
50
49
|
self.enrollment = EnrollmentManager(
|
|
51
50
|
bot, self._voice_state, self._play_audio, self.bot_settings, self.save_bot_settings, self.logger
|
|
@@ -292,8 +291,7 @@ class VoiceCog(commands.Cog):
|
|
|
292
291
|
updated = []
|
|
293
292
|
wake_word_pending = False
|
|
294
293
|
if wake_word is not None:
|
|
295
|
-
# Deferred
|
|
296
|
-
# recorded and confirmed (see EnrollmentManager), not here.
|
|
294
|
+
# Deferred - wake_words is set later once 5 samples are recorded (see EnrollmentManager).
|
|
297
295
|
wake_word_pending = True
|
|
298
296
|
if active_times is not None:
|
|
299
297
|
self.bot_settings["active_timer"] = active_times
|
|
@@ -383,8 +381,7 @@ class VoiceCog(commands.Cog):
|
|
|
383
381
|
await self.stt_session.clear_partial_msg(str(guild_id))
|
|
384
382
|
return
|
|
385
383
|
|
|
386
|
-
# Node runs STT itself before
|
|
387
|
-
# already-recognized text, not raw audio - keeps STT off every VAD hit.
|
|
384
|
+
# Node runs STT itself before this call, so 'text' is already-recognized, not raw audio.
|
|
388
385
|
text = data.get("text")
|
|
389
386
|
|
|
390
387
|
thread_id = self._voice_state.get(str(guild_id))
|
|
@@ -407,10 +404,7 @@ class VoiceCog(commands.Cog):
|
|
|
407
404
|
import difflib
|
|
408
405
|
import re
|
|
409
406
|
|
|
410
|
-
# Wake detection is Node's
|
|
411
|
-
# No text-similarity fallback: an unenrolled user (or a failed
|
|
412
|
-
# .rpw build - see EnrollmentManager._build_rustpotter_reference)
|
|
413
|
-
# just can't wake the bot until that's fixed.
|
|
407
|
+
# Wake detection is Node's Rustpotter detector's job - no text-similarity fallback.
|
|
414
408
|
is_waking_up = bool(data.get("wake_confirmed"))
|
|
415
409
|
matched_wake_word = data.get("matched_wake_word")
|
|
416
410
|
is_active = self.stt_session.is_active(str(guild_id))
|
|
@@ -444,8 +438,7 @@ class VoiceCog(commands.Cog):
|
|
|
444
438
|
if prefix_similarity >= 0.6:
|
|
445
439
|
text_to_ai = " ".join(words[wake_word_count:]).strip()
|
|
446
440
|
|
|
447
|
-
#
|
|
448
|
-
# signal, more false-wakes) -> require a closer match to trust it.
|
|
441
|
+
# Short wake words have less acoustic signal (more false-wakes) -> need a closer match.
|
|
449
442
|
wake_syllables = len(re.sub(r"[^\w가-힣]", "", matched_wake_word or ""))
|
|
450
443
|
min_prefix_similarity = 0.55 if wake_syllables <= 2 else 0.35
|
|
451
444
|
if is_waking_up and prefix_similarity is not None and prefix_similarity < min_prefix_similarity:
|
|
@@ -458,9 +451,7 @@ class VoiceCog(commands.Cog):
|
|
|
458
451
|
|
|
459
452
|
self.logger.debug(f"STT recognized: {text} -> AI: {text_to_ai}")
|
|
460
453
|
|
|
461
|
-
# A
|
|
462
|
-
# it, kill its agy process (like /stop), and stop its playback,
|
|
463
|
-
# rather than letting two turns race to completion.
|
|
454
|
+
# A stale in-flight turn for this guild - cancel it, kill its agy process, stop playback.
|
|
464
455
|
prev_task = self._active_turns.get(str(guild_id))
|
|
465
456
|
if prev_task and not prev_task.done():
|
|
466
457
|
prev_task.cancel()
|
package/src/config.py
CHANGED
|
@@ -19,8 +19,7 @@ DEFAULT_LGY_CONFIG = {
|
|
|
19
19
|
"voice_threshold": 3000,
|
|
20
20
|
"tts_voice": "ko-KR-SunHiNeural",
|
|
21
21
|
"tts_enabled": True,
|
|
22
|
-
# Sticky default for
|
|
23
|
-
# so a new thread doesn't fall back to agy's own settings.json model.
|
|
22
|
+
# Sticky default for /new sessions, set whenever /model succeeds.
|
|
24
23
|
"default_model": "",
|
|
25
24
|
}
|
|
26
25
|
|
|
@@ -94,10 +93,7 @@ def is_allowed_session_channel(channel) -> bool:
|
|
|
94
93
|
|
|
95
94
|
TMP_FILE_DIR = WORKSPACE_DIR / "tmp-files"
|
|
96
95
|
TMP_VOICE_DIR = WORKSPACE_DIR / "tmp-voice"
|
|
97
|
-
# Per-user wake-word recordings + built .rpw reference (see
|
|
98
|
-
# EnrollmentManager in cogs/voice/enrollment.py). No folder/.rpw yet means
|
|
99
|
-
# "not enrolled" - falls back to transcribing everything and matching
|
|
100
|
-
# bot_settings["wake_words"] as text.
|
|
96
|
+
# Per-user wake-word recordings + built .rpw reference (see EnrollmentManager).
|
|
101
97
|
WAKE_REF_DIR = WORKSPACE_DIR / "wake_refs"
|
|
102
98
|
|
|
103
99
|
TMP_FILE_DIR.mkdir(parents=True, exist_ok=True)
|