linkgravity 1.4.1 → 1.5.0
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/README.md +54 -19
- package/bin/cli.js +27 -3
- package/bin/platforms.js +18 -1
- package/bin/setup.js +43 -0
- package/package.json +4 -2
- package/requirements.txt +1 -0
- package/src/cogs/general_cog.py +0 -21
- package/src/cogs/voice_cog.py +48 -4
- package/src/config.py +15 -2
- package/src/core/logger.py +6 -0
- package/src/core/platform_health.py +28 -0
- package/src/handlers/thread_reply.py +1 -1
- package/src/main.py +33 -7
- package/src/main_discord.py +3 -0
- package/src/main_slack.py +252 -0
- package/src/main_telegram.py +3 -0
- package/src/messengers/base.py +5 -0
- package/src/messengers/discord_adapter.py +19 -1
- package/src/messengers/slack_adapter.py +492 -0
- package/src/services/audio_service.py +6 -1
- package/src/services/streaming.py +6 -5
- package/voice-service/index.js +43 -193
package/voice-service/index.js
CHANGED
|
@@ -70,14 +70,7 @@ const connections = new Map();
|
|
|
70
70
|
const players = new Map();
|
|
71
71
|
|
|
72
72
|
function stereoToMono(buffer) {
|
|
73
|
-
// Discord voice receive is
|
|
74
|
-
// official discordjs/voice-examples recorder, which decodes with
|
|
75
|
-
// channels: 2) - everything downstream here (WAV writing via
|
|
76
|
-
// createWavHeader, Rustpotter's config) was built assuming MONO
|
|
77
|
-
// input, so this converts the interleaved L/R stream to mono right
|
|
78
|
-
// after decoding, in one place, rather than decoding as channels: 1
|
|
79
|
-
// and having the native Opus decoder silently misinterpret an
|
|
80
|
-
// actually-stereo stream as mono.
|
|
73
|
+
// Discord voice receive is 48kHz stereo; everything downstream (WAV, Rustpotter) expects mono.
|
|
81
74
|
const samples = buffer.length >> 2; // 2 bytes/sample * 2 channels
|
|
82
75
|
const mono = Buffer.alloc(samples * 2);
|
|
83
76
|
for (let i = 0; i < samples; i++) {
|
|
@@ -110,21 +103,8 @@ client.once(Events.ClientReady, () => {
|
|
|
110
103
|
console.log(`🎤 Node.js Voice Microservice is online as ${client.user.tag}`);
|
|
111
104
|
});
|
|
112
105
|
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
// library (recognize_google) has used for years: audio encoded as FLAC,
|
|
116
|
-
// POSTed to Google's v2 speech API with the "chromium" key that library
|
|
117
|
-
// ships as its default. It's not a secret - it's the same publicly
|
|
118
|
-
// documented key referenced all over SpeechRecognition's own source and
|
|
119
|
-
// years of writeups (search "AIzaSyBOti4mM-6x9WDnZIjIeyEU21OpBXqWBgw
|
|
120
|
-
// google speech api" if you want to verify that yourself). It's still an
|
|
121
|
-
// unofficial/reverse-engineered API that Google could change or rate-
|
|
122
|
-
// limit at any time - same risk the project already had via Python, just
|
|
123
|
-
// no longer duplicated in two languages.
|
|
124
|
-
//
|
|
125
|
-
// Requires ffmpeg to do the WAV -> FLAC conversion - already a
|
|
126
|
-
// dependency of this package (ffmpeg-static bundles the binary per
|
|
127
|
-
// platform), so nothing extra to install.
|
|
106
|
+
// Unofficial Google speech API endpoint/key - same one Python's SpeechRecognition library
|
|
107
|
+
// (recognize_google) ships as its default; publicly known but could be rate-limited/changed anytime.
|
|
128
108
|
const GOOGLE_STT_KEY = 'AIzaSyBOti4mM-6x9WDnZIjIeyEU21OpBXqWBgw';
|
|
129
109
|
|
|
130
110
|
function flacEncode(wavBuffer) {
|
|
@@ -177,9 +157,7 @@ async function googleSTT(wavBuffer, lang = 'ko-KR') {
|
|
|
177
157
|
}
|
|
178
158
|
|
|
179
159
|
const raw = await res.text();
|
|
180
|
-
// Response is newline-delimited JSON, one object per line
|
|
181
|
-
// {"result":[]}
|
|
182
|
-
// {"result":[{"alternative":[{"transcript":"...","confidence":0.9}],"final":true}],"result_index":0}
|
|
160
|
+
// Response is newline-delimited JSON, one object per line.
|
|
183
161
|
for (const line of raw.trim().split('\n')) {
|
|
184
162
|
if (!line) continue;
|
|
185
163
|
try {
|
|
@@ -218,47 +196,28 @@ function interruptTTS(guildId) {
|
|
|
218
196
|
|
|
219
197
|
const activeStreams = new Map();
|
|
220
198
|
|
|
221
|
-
// user_id ->
|
|
222
|
-
// /enroll_start, /enroll_stop). Utterances from these users get routed
|
|
223
|
-
// to /enroll_sample instead of the normal wake-check/STT pipeline.
|
|
199
|
+
// user_id -> recording wake-word samples right now; routes to /enroll_sample instead of STT.
|
|
224
200
|
const enrollingUsers = new Set();
|
|
225
201
|
|
|
226
|
-
// guild_id -> ms epoch until
|
|
227
|
-
// the wake word" window (set by Python via /set_active whenever it
|
|
228
|
-
// starts/renews that countdown - see VoiceCog._extend_active_window).
|
|
202
|
+
// guild_id -> ms epoch until the "awake, skip wake word" window closes (set via /set_active).
|
|
229
203
|
const activeUntil = new Map();
|
|
230
204
|
|
|
205
|
+
// user_id -> opted out of wake-word gating via /sound - scoped per-user, unlike activeUntil.
|
|
206
|
+
const wakeWordOptedOut = new Set();
|
|
207
|
+
|
|
231
208
|
function isGuildActive(guildId) {
|
|
232
209
|
return Date.now() < (activeUntil.get(guildId) || 0);
|
|
233
210
|
}
|
|
234
211
|
|
|
235
|
-
//
|
|
236
|
-
// Runs entirely in this Node process - no Python round trip. Each
|
|
237
|
-
// enrolled user gets their own Rustpotter instance (built from the .rpw
|
|
238
|
-
// reference file this same process produces via /build_wakeword, called
|
|
239
|
-
// by EnrollmentManager._commit_enrollment once all samples are
|
|
240
|
-
// confirmed), cached here and fed audio directly as it streams in from
|
|
241
|
-
// Discord.
|
|
212
|
+
// Rustpotter wake-word detection runs entirely in-process here, no Python round trip.
|
|
242
213
|
const WAKE_REF_DIR = path.join(os.homedir(), '.gemini', 'linkgravity', 'wake_refs');
|
|
243
214
|
|
|
244
215
|
let rustpotterModPromise = null;
|
|
245
216
|
function loadRustpotterModule() {
|
|
246
217
|
if (!rustpotterModPromise) {
|
|
247
218
|
rustpotterModPromise = (async () => {
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
// is a bundler-only convention Node doesn't read) - the
|
|
251
|
-
// actual entry file has to be named explicitly.
|
|
252
|
-
//
|
|
253
|
-
// Using the full "rustpotter-web" package here (not the
|
|
254
|
-
// "-slim" variant this used to be) because it's the only one
|
|
255
|
-
// of the two that also exposes WakewordRefCreator - the
|
|
256
|
-
// builder API used by /build_wakeword below to create a new
|
|
257
|
-
// .rpw reference directly from wav samples, in-process, with
|
|
258
|
-
// no external binary. The wasm binary is ~6% bigger
|
|
259
|
-
// (810KB vs 761KB) which is irrelevant for a Node backend
|
|
260
|
-
// (this distinction only matters for browser bundle size,
|
|
261
|
-
// which is the slim variant's actual purpose).
|
|
219
|
+
// Node's ESM loader needs the explicit entry file; "rustpotter-web" (not "-slim")
|
|
220
|
+
// is used because it also exposes WakewordRefCreator, used by /build_wakeword below.
|
|
262
221
|
const mod = await import('rustpotter-web/rustpotter_wasm.js');
|
|
263
222
|
const wasmPath = require.resolve('rustpotter-web/rustpotter_wasm_bg.wasm');
|
|
264
223
|
mod.initSync(fs.readFileSync(wasmPath));
|
|
@@ -288,64 +247,22 @@ async function getDetectorForUser(userId) {
|
|
|
288
247
|
config.setSampleRate(48000);
|
|
289
248
|
config.setSampleFormat(mod.SampleFormat.i16);
|
|
290
249
|
config.setChannels(1);
|
|
291
|
-
//
|
|
292
|
-
// meaningful cutoff (not near-zero) for rustpotter's internal
|
|
293
|
-
// confirm-after-N-more-frames logic to ever finalize a detection.
|
|
250
|
+
// Must stay a real cutoff (not near-zero) for rustpotter's confirm-after-N-frames logic to finalize.
|
|
294
251
|
config.setThreshold(WAKE_MATCH_THRESHOLD);
|
|
295
252
|
config.setAveragedThreshold(0);
|
|
296
|
-
//
|
|
297
|
-
// the leading candidate, even for just one internal frame. Raised
|
|
298
|
-
// to 4 (was 3) so a candidate has to keep winning for a few frames
|
|
299
|
-
// running before it's trusted - this matters more now that
|
|
300
|
-
// scoreMode is back to Max (see below), which is more permissive
|
|
301
|
-
// per-frame than Median was, so this is the main compensating knob
|
|
302
|
-
// against one-off spurious spikes from unrelated speech.
|
|
253
|
+
// Raised from default 1 so a candidate has to keep winning for a few frames before it's trusted.
|
|
303
254
|
config.setMinScores(4);
|
|
304
|
-
// Max
|
|
305
|
-
// Median
|
|
306
|
-
// the middle-ranked sample to also score well, which in practice
|
|
307
|
-
// means all 5 enrollment recordings need fairly consistent
|
|
308
|
-
// tone/pace/delivery - real speech isn't that consistent, so Median
|
|
309
|
-
// made real matches with naturally varied enrollment recordings
|
|
310
|
-
// score worse, not just false positives. Max lets a genuinely
|
|
311
|
-
// varied set of 5 recordings (calm, rushed, questioning, etc.) each
|
|
312
|
-
// cover a different real-world delivery, at the cost of also being
|
|
313
|
-
// easier to trip with something that merely resembles ONE of the 5
|
|
314
|
-
// by chance - minScores(4) above and the STT jamo cross-check in
|
|
315
|
-
// VoiceCog.handle_stt_input (tightened for short wake words
|
|
316
|
-
// specifically) are what compensate for that on the other two axes
|
|
317
|
-
// instead. Still needs field-tuning against real usage logs - see
|
|
318
|
-
// README's known-issues section.
|
|
255
|
+
// Max (best of the 5 enrolled samples) beats Median here - real speech isn't consistent enough
|
|
256
|
+
// for Median's "middle sample must also score well" requirement; minScores(4) compensates.
|
|
319
257
|
config.setScoreMode(mod.ScoreMode.max);
|
|
320
258
|
|
|
321
259
|
const rustpotter = mod.Rustpotter.new(config);
|
|
322
260
|
rustpotter.addWakeword(rpwFile, fs.readFileSync(path.join(userDir, rpwFile)));
|
|
323
261
|
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
// low score off the real one: rustpotter's own scoring
|
|
329
|
-
// (wakeword.run_detection in the Rust source) filters out any
|
|
330
|
-
// frame that doesn't already clear `threshold` before it's even
|
|
331
|
-
// tracked internally, so there is no bound API that exposes a
|
|
332
|
-
// sub-threshold score - process*() returns undefined for those,
|
|
333
|
-
// full stop. Naively lowering the REAL detector's threshold to see
|
|
334
|
-
// more doesn't work either (that's exactly the bug from the
|
|
335
|
-
// local-wake migration: near-zero threshold means background noise
|
|
336
|
-
// clears it on almost every frame, which keeps resetting
|
|
337
|
-
// detection_countdown before it can ever reach 0, so confirmation
|
|
338
|
-
// never completes for anyone, real wake word included).
|
|
339
|
-
//
|
|
340
|
-
// This instance sidesteps that by setting eager+minScores(1), so it
|
|
341
|
-
// finalizes and hands back a real score the very first time ANY
|
|
342
|
-
// frame clears its own (near-zero) threshold, instead of waiting on
|
|
343
|
-
// the countdown - the exact same escape hatch that would break
|
|
344
|
-
// gating on the real detector is safe to lean on here because nothing
|
|
345
|
-
// downstream ever treats this instance's output as a wake trigger.
|
|
346
|
-
// scoreMode is kept identical to the real detector so the number
|
|
347
|
-
// itself is a genuine, comparable similarity score - just captured
|
|
348
|
-
// more eagerly, not computed differently.
|
|
262
|
+
// Diagnostic-only twin, fed the same audio, purely so "no match" logs show a real closeness
|
|
263
|
+
// score - the real detector's own threshold hides sub-threshold scores entirely, and lowering
|
|
264
|
+
// its threshold isn't safe (near-zero means noise keeps resetting the confirm countdown).
|
|
265
|
+
// Never gates wake behavior; only entry.rustpotter above does.
|
|
349
266
|
const diagConfig = mod.RustpotterConfig.new();
|
|
350
267
|
diagConfig.setSampleRate(48000);
|
|
351
268
|
diagConfig.setSampleFormat(mod.SampleFormat.i16);
|
|
@@ -375,16 +292,8 @@ async function getDetectorForUser(userId) {
|
|
|
375
292
|
return entry;
|
|
376
293
|
}
|
|
377
294
|
|
|
378
|
-
// Feeds
|
|
379
|
-
//
|
|
380
|
-
// re-snapshotting a growing buffer, since Rustpotter's internal window
|
|
381
|
-
// expects a genuinely continuous stream. Returns a RustpotterDetection
|
|
382
|
-
// if any complete frame in this chunk triggered one, else null.
|
|
383
|
-
//
|
|
384
|
-
// Frames are fed back-to-back with NO external overlap - rustpotter
|
|
385
|
-
// internally extracts multiple overlapping 10ms-shifted MFCCs from each
|
|
386
|
-
// ~30ms buffer passed in (confirmed against its Rust source), so the
|
|
387
|
-
// caller just needs to keep the stream continuous, not overlap it.
|
|
295
|
+
// Feeds a PCM chunk to a detector frame-aligned via residual carryover (Rustpotter needs a
|
|
296
|
+
// genuinely continuous stream). Returns a detection if a complete frame in this chunk triggered one.
|
|
388
297
|
function feedPCMToDetector(entry, chunk) {
|
|
389
298
|
const incoming = new Int16Array(chunk.buffer, chunk.byteOffset, chunk.length / 2);
|
|
390
299
|
let combined = incoming;
|
|
@@ -430,13 +339,7 @@ function setupReceiver(connection, guildId) {
|
|
|
430
339
|
new prism.opus.Decoder({ rate: 48000, channels: 2, frameSize: 960 }),
|
|
431
340
|
);
|
|
432
341
|
|
|
433
|
-
//
|
|
434
|
-
// DAVE re-encrypt, whatever) throws inside prism-media's
|
|
435
|
-
// decoder. Streams turn a thrown _transform error into an
|
|
436
|
-
// 'error' event - with no listener here, Node's default for an
|
|
437
|
-
// unhandled 'error' event is to crash the ENTIRE process, not
|
|
438
|
-
// just this one user's utterance. Handling it here keeps voice
|
|
439
|
-
// alive for everyone else (and for this user's next utterance).
|
|
342
|
+
// Without a listener, an unhandled 'error' event here crashes the ENTIRE process on one bad packet.
|
|
440
343
|
opusStream.on('error', (err) => {
|
|
441
344
|
console.error(`[Voice] Opus stream error for ${userId}:`, err.message);
|
|
442
345
|
forceEndStream();
|
|
@@ -466,14 +369,7 @@ function setupReceiver(connection, guildId) {
|
|
|
466
369
|
|
|
467
370
|
const maxDurationTimer = setTimeout(forceEndStream, 30000);
|
|
468
371
|
|
|
469
|
-
//
|
|
470
|
-
// Runs entirely inside this Node process now - no HTTP round
|
|
471
|
-
// trip, no Python involvement. Detector instances are cached per
|
|
472
|
-
// Discord user (see getDetectorForUser) and fed every PCM chunk
|
|
473
|
-
// as it streams in below, frame-aligned via feedPCMToDetector's
|
|
474
|
-
// residual carryover - not via periodic re-snapshotting like the
|
|
475
|
-
// old /wake_check design, since Rustpotter expects each sample
|
|
476
|
-
// fed exactly once, in order.
|
|
372
|
+
// Rustpotter runs in-process here, no Python round trip - detectors cached per user.
|
|
477
373
|
let wakeConfirmed = false;
|
|
478
374
|
let matchedWakeWord = null;
|
|
479
375
|
let detectorEntry = null;
|
|
@@ -498,13 +394,8 @@ function setupReceiver(connection, guildId) {
|
|
|
498
394
|
);
|
|
499
395
|
}
|
|
500
396
|
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
// 60s) - NOT for every VAD-detected sound like the old version,
|
|
504
|
-
// which is what made it expensive before. STT here reuses the
|
|
505
|
-
// same googleSTT() call that runs at utterance end anyway, just
|
|
506
|
-
// invoked earlier/more often on the growing buffer for live
|
|
507
|
-
// feedback.
|
|
397
|
+
// Only runs during the active/awake window, not on every VAD sound - reuses the same
|
|
398
|
+
// googleSTT() call utterance-end uses anyway, just invoked earlier for live feedback.
|
|
508
399
|
const PARTIAL_INTERVAL_MS = 1500;
|
|
509
400
|
const PARTIAL_MIN_NEW_BYTES = 24000;
|
|
510
401
|
let lastPartialLength = 0;
|
|
@@ -700,20 +591,8 @@ function setupReceiver(connection, guildId) {
|
|
|
700
591
|
return;
|
|
701
592
|
}
|
|
702
593
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
// wake word (this is also what makes the live "listening"
|
|
706
|
-
// indicator above meaningful - same window).
|
|
707
|
-
// - wakeConfirmed: our in-process Rustpotter detector matched this
|
|
708
|
-
// user's voice against their enrolled samples.
|
|
709
|
-
// There is deliberately no "unenrolled users always get
|
|
710
|
-
// transcribed" fallback anymore - that existed only to feed
|
|
711
|
-
// the Python side's old text-similarity wake-word matching,
|
|
712
|
-
// which has been removed (it was exactly the always-on
|
|
713
|
-
// recognition overhead this Rustpotter migration was meant to
|
|
714
|
-
// get rid of). An unenrolled user simply can't wake the bot
|
|
715
|
-
// by voice until they run /sound.
|
|
716
|
-
const shouldTranscribe = isGuildActive(guildId) || wakeConfirmed;
|
|
594
|
+
const shouldTranscribe =
|
|
595
|
+
isGuildActive(guildId) || wakeConfirmed || wakeWordOptedOut.has(userId);
|
|
717
596
|
|
|
718
597
|
if (!shouldTranscribe) {
|
|
719
598
|
if (partialSent) {
|
|
@@ -800,19 +679,7 @@ app.post('/leave', (req, res) => {
|
|
|
800
679
|
}
|
|
801
680
|
connection.destroy();
|
|
802
681
|
|
|
803
|
-
//
|
|
804
|
-
// /leave untouched. Most critically: if isPlaying was still true
|
|
805
|
-
// (e.g. TTS got cut off mid-playback by the destroy() above, or
|
|
806
|
-
// was never cleanly resolved), the NEXT /join's audio would hit
|
|
807
|
-
// `if (isPlaying.get(guildId)) return;` at the very top of the PCM
|
|
808
|
-
// data handler and get silently dropped forever - STT and wake
|
|
809
|
-
// detection both stop working, with no error, until the whole bot
|
|
810
|
-
// restarts. Same idea for a stale `players` entry: playNextInQueue
|
|
811
|
-
// reuses whatever's cached instead of creating a fresh one, so a
|
|
812
|
-
// leftover player from the destroyed connection could end up
|
|
813
|
-
// "subscribed" to nothing and never reach Idle, which is exactly
|
|
814
|
-
// what a permanently-on "speaking" indicator on the next join looks
|
|
815
|
-
// like from the outside.
|
|
682
|
+
// Without this, a stale isPlaying/players entry silently breaks STT/wake detection on the next /join.
|
|
816
683
|
connections.delete(guild_id);
|
|
817
684
|
players.delete(guild_id);
|
|
818
685
|
audioQueues.delete(guild_id);
|
|
@@ -823,12 +690,8 @@ app.post('/leave', (req, res) => {
|
|
|
823
690
|
res.json({ success: true });
|
|
824
691
|
});
|
|
825
692
|
|
|
826
|
-
//
|
|
827
|
-
//
|
|
828
|
-
// awake" window) or not (e.g. wake-word enrollment sample playback -
|
|
829
|
-
// see EnrollmentManager._play_audio's suppress_active_window). Set
|
|
830
|
-
// whenever an item is shifted off the queue to play; read once the
|
|
831
|
-
// queue drains and playback is fully idle again.
|
|
693
|
+
// Whether the audio that just finished playing should extend the "stay awake" window
|
|
694
|
+
// (false for wake-word enrollment sample playback - see suppress_active_window).
|
|
832
695
|
const suppressNotifyMap = new Map();
|
|
833
696
|
|
|
834
697
|
async function notifyTtsFinished(guild_id) {
|
|
@@ -911,25 +774,13 @@ app.post('/play', express.raw({ type: 'application/octet-stream', limit: '20mb'
|
|
|
911
774
|
|
|
912
775
|
app.post('/interrupt', (req, res) => {
|
|
913
776
|
const { guild_id } = req.body;
|
|
914
|
-
//
|
|
915
|
-
// just gives Python a way to trigger it directly, for the case where
|
|
916
|
-
// a new recognized utterance should cut off whatever's currently
|
|
917
|
-
// playing regardless of how loud it was (see VoiceCog.handle_stt_input,
|
|
918
|
-
// which calls this before starting a new turn whenever a previous
|
|
919
|
-
// one was still in flight).
|
|
777
|
+
// Lets Python trigger the same cutoff the VAD loud-voice check uses, regardless of volume.
|
|
920
778
|
interruptTTS(guild_id);
|
|
921
779
|
res.json({ success: true });
|
|
922
780
|
});
|
|
923
781
|
|
|
924
782
|
app.post('/invalidate_detector', (req, res) => {
|
|
925
|
-
//
|
|
926
|
-
// NEW .rpw is successfully built. Without this, detectorCache (keyed
|
|
927
|
-
// only by user_id, loaded once and cached forever) keeps serving
|
|
928
|
-
// whatever detector - built from an OLDER recording, possibly for a
|
|
929
|
-
// completely different word - was cached the first time this user
|
|
930
|
-
// was ever checked, no matter how many times they re-enroll. That's
|
|
931
|
-
// enough on its own to make every wake attempt score exactly 0
|
|
932
|
-
// forever: it's not comparing against the word that was just said.
|
|
783
|
+
// Without this, detectorCache keeps serving the OLD .rpw after a user re-enrolls.
|
|
933
784
|
const { user_id } = req.body;
|
|
934
785
|
const deleted = detectorCache.delete(user_id);
|
|
935
786
|
console.log(`[Wake] Invalidated cached detector for ${user_id} (was cached: ${deleted})`);
|
|
@@ -937,17 +788,8 @@ app.post('/invalidate_detector', (req, res) => {
|
|
|
937
788
|
});
|
|
938
789
|
|
|
939
790
|
app.post('/build_wakeword', async (req, res) => {
|
|
940
|
-
// Builds a .rpw
|
|
941
|
-
//
|
|
942
|
-
// WakewordRefCreator - see EnrollmentManager._build_rustpotter_reference
|
|
943
|
-
// in cogs/voice/enrollment.py, which used to shell out to a
|
|
944
|
-
// separately-downloaded rustpotter-cli binary for this exact step.
|
|
945
|
-
// That meant an extra install-time download (GitHub Releases API,
|
|
946
|
-
// OS/arch guessing, no checksum verification) just to run a build
|
|
947
|
-
// command whose only real job was calling the same builder API this
|
|
948
|
-
// now calls directly. Nothing else about .rpw files changes: the
|
|
949
|
-
// hot-path detector (getDetectorForUser) still just loads the bytes
|
|
950
|
-
// this returns, the same as it always did.
|
|
791
|
+
// Builds a .rpw reference in-process via rustpotter-web's WakewordRefCreator, instead of
|
|
792
|
+
// shelling out to a separately-downloaded rustpotter-cli binary like this used to.
|
|
951
793
|
try {
|
|
952
794
|
const { name, samples } = req.body;
|
|
953
795
|
if (!name || !Array.isArray(samples) || samples.length === 0) {
|
|
@@ -1008,6 +850,14 @@ app.post('/set_active', (req, res) => {
|
|
|
1008
850
|
res.json({ success: true });
|
|
1009
851
|
});
|
|
1010
852
|
|
|
853
|
+
app.post('/set_wake_word_required', (req, res) => {
|
|
854
|
+
const { user_id, required } = req.body;
|
|
855
|
+
if (!user_id) return res.status(400).json({ error: 'user_id required' });
|
|
856
|
+
if (required) wakeWordOptedOut.delete(user_id);
|
|
857
|
+
else wakeWordOptedOut.add(user_id);
|
|
858
|
+
res.json({ success: true });
|
|
859
|
+
});
|
|
860
|
+
|
|
1011
861
|
process.on('unhandledRejection', (reason) => {
|
|
1012
862
|
console.error('Unhandled promise rejection (voice service stays alive):', reason);
|
|
1013
863
|
});
|