linkgravity 1.5.1 → 1.5.3
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 +79 -12
- package/package.json +2 -2
- package/src/cogs/general_cog.py +1 -1
- package/src/cogs/voice_cog.py +25 -21
- package/src/core/agy_runner.py +58 -45
- package/src/main_slack.py +1 -2
- package/src/main_telegram.py +5 -2
- package/voice-service/audioUtils.js +31 -0
- package/voice-service/config.js +24 -0
- package/voice-service/index.js +12 -11
- package/voice-service/logger.js +16 -0
- package/voice-service/receiver.js +310 -0
- package/voice-service/routes.js +182 -0
- package/voice-service/state.js +42 -0
- package/voice-service/stt.js +71 -0
- package/voice-service/tts.js +81 -0
- package/voice-service/wakeword.js +114 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { detectorCache } = require('./state');
|
|
5
|
+
|
|
6
|
+
// Rustpotter wake-word detection runs entirely in-process here, no Python round trip.
|
|
7
|
+
const WAKE_REF_DIR = path.join(os.homedir(), '.gemini', 'linkgravity', 'wake_refs');
|
|
8
|
+
|
|
9
|
+
let rustpotterModPromise = null;
|
|
10
|
+
function loadRustpotterModule() {
|
|
11
|
+
if (!rustpotterModPromise) {
|
|
12
|
+
rustpotterModPromise = (async () => {
|
|
13
|
+
// "rustpotter-web" (not "-slim") - also exposes WakewordRefCreator for /build_wakeword.
|
|
14
|
+
const mod = await import('rustpotter-web/rustpotter_wasm.js');
|
|
15
|
+
const wasmPath = require.resolve('rustpotter-web/rustpotter_wasm_bg.wasm');
|
|
16
|
+
mod.initSync(fs.readFileSync(wasmPath));
|
|
17
|
+
return mod;
|
|
18
|
+
})();
|
|
19
|
+
}
|
|
20
|
+
return rustpotterModPromise;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Wake-word confirm cutoff - must stay well above ~0.05 (rustpotter's countdown never finalizes if noise/silence clears it too); 0.4 chosen after live use kept narrowly missing genuine hits just under 0.5.
|
|
24
|
+
const WAKE_MATCH_THRESHOLD = 0.4;
|
|
25
|
+
|
|
26
|
+
async function getDetectorForUser(userId) {
|
|
27
|
+
if (detectorCache.has(userId)) return detectorCache.get(userId);
|
|
28
|
+
|
|
29
|
+
const userDir = path.join(WAKE_REF_DIR, userId);
|
|
30
|
+
if (!fs.existsSync(userDir)) return null; // not enrolled
|
|
31
|
+
|
|
32
|
+
const rpwFile = fs.readdirSync(userDir).find((f) => f.endsWith('.rpw'));
|
|
33
|
+
if (!rpwFile) return null; // samples exist but .rpw build hasn't happened/failed - see _commit_enrollment
|
|
34
|
+
|
|
35
|
+
const mod = await loadRustpotterModule();
|
|
36
|
+
const config = mod.RustpotterConfig.new();
|
|
37
|
+
config.setSampleRate(48000);
|
|
38
|
+
config.setSampleFormat(mod.SampleFormat.i16);
|
|
39
|
+
config.setChannels(1);
|
|
40
|
+
config.setThreshold(WAKE_MATCH_THRESHOLD);
|
|
41
|
+
config.setAveragedThreshold(0);
|
|
42
|
+
// Live logs showed genuine attempts peaking above threshold but not sustaining 4 positive-scoring
|
|
43
|
+
// frames; lowered from 4. STT-side prefix-similarity check is the backstop against false wakes.
|
|
44
|
+
config.setMinScores(2);
|
|
45
|
+
// Max (best of the 5 enrolled samples) beats Median here - real speech isn't consistent enough
|
|
46
|
+
// for Median's "middle sample must also score well" requirement; minScores compensates.
|
|
47
|
+
config.setScoreMode(mod.ScoreMode.max);
|
|
48
|
+
// Enrollment and live-call volume rarely match (distance, speaking softly); without this, that
|
|
49
|
+
// mismatch alone can push a genuine match below threshold.
|
|
50
|
+
config.setGainNormalizerEnabled(true);
|
|
51
|
+
|
|
52
|
+
const rustpotter = mod.Rustpotter.new(config);
|
|
53
|
+
rustpotter.addWakeword(rpwFile, fs.readFileSync(path.join(userDir, rpwFile)));
|
|
54
|
+
|
|
55
|
+
// Diagnostic-only twin (same audio) so "no match" logs show a closeness score - never gates wake behavior.
|
|
56
|
+
const diagConfig = mod.RustpotterConfig.new();
|
|
57
|
+
diagConfig.setSampleRate(48000);
|
|
58
|
+
diagConfig.setSampleFormat(mod.SampleFormat.i16);
|
|
59
|
+
diagConfig.setChannels(1);
|
|
60
|
+
diagConfig.setThreshold(0.01);
|
|
61
|
+
diagConfig.setAveragedThreshold(0);
|
|
62
|
+
diagConfig.setMinScores(1);
|
|
63
|
+
diagConfig.setEager(true);
|
|
64
|
+
diagConfig.setScoreMode(mod.ScoreMode.max);
|
|
65
|
+
diagConfig.setGainNormalizerEnabled(true);
|
|
66
|
+
const diagRustpotter = mod.Rustpotter.new(diagConfig);
|
|
67
|
+
diagRustpotter.addWakeword(rpwFile, fs.readFileSync(path.join(userDir, rpwFile)));
|
|
68
|
+
|
|
69
|
+
const entry = {
|
|
70
|
+
rustpotter,
|
|
71
|
+
samplesPerFrame: rustpotter.getSamplesPerFrame(),
|
|
72
|
+
residual: new Int16Array(0),
|
|
73
|
+
diag: {
|
|
74
|
+
rustpotter: diagRustpotter,
|
|
75
|
+
samplesPerFrame: diagRustpotter.getSamplesPerFrame(),
|
|
76
|
+
residual: new Int16Array(0),
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
console.log(
|
|
80
|
+
`[Wake] Loaded detector for ${userId} from ${rpwFile}: samplesPerFrame=${entry.samplesPerFrame}`,
|
|
81
|
+
);
|
|
82
|
+
detectorCache.set(userId, entry);
|
|
83
|
+
return entry;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Frame-aligns via residual carryover (Rustpotter needs a continuous stream); returns a detection if any.
|
|
87
|
+
function feedPCMToDetector(entry, chunk) {
|
|
88
|
+
const incoming = new Int16Array(chunk.buffer, chunk.byteOffset, chunk.length / 2);
|
|
89
|
+
let combined = incoming;
|
|
90
|
+
if (entry.residual.length) {
|
|
91
|
+
combined = new Int16Array(entry.residual.length + incoming.length);
|
|
92
|
+
combined.set(entry.residual, 0);
|
|
93
|
+
combined.set(incoming, entry.residual.length);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let offset = 0;
|
|
97
|
+
let detection = null;
|
|
98
|
+
while (combined.length - offset >= entry.samplesPerFrame) {
|
|
99
|
+
const frame = combined.subarray(offset, offset + entry.samplesPerFrame);
|
|
100
|
+
const result = entry.rustpotter.processI16(frame);
|
|
101
|
+
if (result) detection = result;
|
|
102
|
+
offset += entry.samplesPerFrame;
|
|
103
|
+
}
|
|
104
|
+
entry.residual = combined.subarray(offset);
|
|
105
|
+
return detection;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
module.exports = {
|
|
109
|
+
WAKE_REF_DIR,
|
|
110
|
+
WAKE_MATCH_THRESHOLD,
|
|
111
|
+
loadRustpotterModule,
|
|
112
|
+
getDetectorForUser,
|
|
113
|
+
feedPCMToDetector,
|
|
114
|
+
};
|