omnius 1.0.418 → 1.0.419
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/dist/index.js +33 -10
- package/dist/scripts/live-whisper.py +39 -15
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -603059,6 +603059,21 @@ async function resolveMicSource() {
|
|
|
603059
603059
|
const best = candidates[0];
|
|
603060
603060
|
return { pulseSource: best.name, channels: best.channels, label: `pulse:${best.name}` };
|
|
603061
603061
|
}
|
|
603062
|
+
async function ensureSourceCaptureVolume(source) {
|
|
603063
|
+
try {
|
|
603064
|
+
await execFileText4("pactl", ["set-source-mute", source, "0"], { timeout: 3e3 });
|
|
603065
|
+
} catch {
|
|
603066
|
+
}
|
|
603067
|
+
try {
|
|
603068
|
+
const raw = await execFileText4("pactl", ["get-source-volume", source], { timeout: 3e3 });
|
|
603069
|
+
const percents = [...raw.matchAll(/(\d+)%/g)].map((m2) => Number(m2[1])).filter((n2) => Number.isFinite(n2));
|
|
603070
|
+
const current = percents.length ? Math.max(...percents) : null;
|
|
603071
|
+
if (current !== null && current < 100) {
|
|
603072
|
+
await execFileText4("pactl", ["set-source-volume", source, "100%"], { timeout: 3e3 });
|
|
603073
|
+
}
|
|
603074
|
+
} catch {
|
|
603075
|
+
}
|
|
603076
|
+
}
|
|
603062
603077
|
function micFfmpegFilter(channels) {
|
|
603063
603078
|
const parts = [];
|
|
603064
603079
|
if ((channels ?? 1) > 2) parts.push("pan=mono|c0=c0");
|
|
@@ -603079,6 +603094,7 @@ async function findMicCaptureCommand() {
|
|
|
603079
603094
|
};
|
|
603080
603095
|
}
|
|
603081
603096
|
if (resolved?.pulseSource && await commandExists2("ffmpeg")) {
|
|
603097
|
+
await ensureSourceCaptureVolume(resolved.pulseSource);
|
|
603082
603098
|
return {
|
|
603083
603099
|
cmd: "ffmpeg",
|
|
603084
603100
|
args: [
|
|
@@ -603687,7 +603703,7 @@ var init_listen = __esm({
|
|
|
603687
603703
|
else if (levelDb < this.micNoiseFloorDb) this.micNoiseFloorDb = 0.3 * levelDb + 0.7 * this.micNoiseFloorDb;
|
|
603688
603704
|
else this.micNoiseFloorDb = 0.02 * levelDb + 0.98 * this.micNoiseFloorDb;
|
|
603689
603705
|
const floor = this.micNoiseFloorDb;
|
|
603690
|
-
const speech = this.micLevelDb > Math.max(-
|
|
603706
|
+
const speech = this.micLevelDb > Math.max(-58, floor + 6);
|
|
603691
603707
|
const changed = speech !== this.micSpeechActive;
|
|
603692
603708
|
this.micSpeechActive = speech;
|
|
603693
603709
|
const now2 = Date.now();
|
|
@@ -604575,7 +604591,7 @@ function readPcm16WavActivity(filePath, bins = 36) {
|
|
|
604575
604591
|
const abs = Math.abs(sample);
|
|
604576
604592
|
peak = Math.max(peak, abs);
|
|
604577
604593
|
sumSquares += sample * sample;
|
|
604578
|
-
if (abs >
|
|
604594
|
+
if (abs > 8e-3) active++;
|
|
604579
604595
|
const b = Math.min(bucketSquares.length - 1, Math.floor(i2 / frames * bucketSquares.length));
|
|
604580
604596
|
bucketSquares[b] += sample * sample;
|
|
604581
604597
|
bucketCounts[b] += 1;
|
|
@@ -605411,16 +605427,22 @@ function formatLiveSensorContextFromSnapshot(snapshot, opts = {}) {
|
|
|
605411
605427
|
if (!snapshot) return "";
|
|
605412
605428
|
const now2 = opts.now ?? Date.now();
|
|
605413
605429
|
const maxAgeMs = opts.maxAgeMs ?? 9e4;
|
|
605430
|
+
const toolCapable = opts.toolCapable !== false;
|
|
605414
605431
|
const lines = [];
|
|
605415
605432
|
const activeVideo = snapshot.config.videoEnabled || Boolean(snapshot.video);
|
|
605416
605433
|
const activeAudio = snapshot.config.audioEnabled || snapshot.config.audioOutputEnabled || Boolean(snapshot.audio);
|
|
605417
605434
|
if (!activeVideo && !activeAudio) return "";
|
|
605418
605435
|
lines.push("<live-sensor-context>");
|
|
605419
605436
|
lines.push("## LIVE SENSOR STATUS");
|
|
605420
|
-
|
|
605421
|
-
|
|
605422
|
-
|
|
605423
|
-
|
|
605437
|
+
if (toolCapable) {
|
|
605438
|
+
lines.push("Live sensor context is operator-enabled ambient input. Treat it as current perceptual evidence for this turn; if it is stale, missing, or insufficient, use camera/audio tools to refresh before answering what you see or hear.");
|
|
605439
|
+
lines.push('Live world-model rule: when the user asks what is around, who is present, what changed, what is heard, or asks a deictic question like "what do you see", treat the live camera/audio fields as first-class context. Be curious and tool-capable: refresh stale sensors, inspect frame paths, query vision/CLIP/visual_memory/audio tools, and combine metadata into a concise answer. Do not rely only on summaries when the task requires current perception.');
|
|
605440
|
+
lines.push("Live speech rule: in low-latency voice/live modes, short spoken replies are appropriate for direct address, identity clarification, or important visual/audio triggers. Do not narrate every frame.");
|
|
605441
|
+
lines.push("Identity rule: never invent names for observed people. If a person is unknown and identity matters, ask the user or the person for their name, then store the association with visual_memory(action='enroll', image='<face crop>', name='<name>') when a face crop is available, or multimodal_memory(action='meet', person_name='<name>') when live voice/name context is available.");
|
|
605442
|
+
} else {
|
|
605443
|
+
lines.push("This is your current live perception: what the camera sees and what is heard right now. Answer questions about sight/sound directly from these fields.");
|
|
605444
|
+
lines.push("If the data below is stale or missing, say you cannot see/hear clearly right now. Never invent observations or names for people. To have the main agent investigate further, relay a request with the voice_to_main tool.");
|
|
605445
|
+
}
|
|
605424
605446
|
lines.push(`Snapshot file: ${liveSnapshotPath(snapshot.repoRoot)}`);
|
|
605425
605447
|
lines.push(`Streams: video=${snapshot.config.videoEnabled ? "on" : "off"} infer=${snapshot.config.inferEnabled ? "on" : "off"} clip=${snapshot.config.clipEnabled ? "on" : "off"} audio=${snapshot.config.audioEnabled ? "on" : "off"} asr=${snapshot.config.asrEnabled ? "on" : "off"} sounds=${snapshot.config.audioAnalysisEnabled ? "on" : "off"} output-monitor=${snapshot.config.audioOutputEnabled ? "on" : "off"}`);
|
|
605426
605448
|
if (snapshot.config.selectedCamera) {
|
|
@@ -605563,8 +605585,8 @@ function formatLiveSensorContextFromSnapshot(snapshot, opts = {}) {
|
|
|
605563
605585
|
... live sensor context truncated ...
|
|
605564
605586
|
</live-sensor-context>` : text2;
|
|
605565
605587
|
}
|
|
605566
|
-
function formatLiveSensorContext(repoRoot) {
|
|
605567
|
-
return formatLiveSensorContextFromSnapshot(readLiveSensorSnapshot(repoRoot));
|
|
605588
|
+
function formatLiveSensorContext(repoRoot, opts = {}) {
|
|
605589
|
+
return formatLiveSensorContextFromSnapshot(readLiveSensorSnapshot(repoRoot), opts);
|
|
605568
605590
|
}
|
|
605569
605591
|
function isLiveSensorSnapshotActive(snapshot) {
|
|
605570
605592
|
const cfg = snapshot?.config;
|
|
@@ -605737,7 +605759,7 @@ async function tryRecordAudioDevice(device, durationSec, outputPath3, channels)
|
|
|
605737
605759
|
}
|
|
605738
605760
|
function hasLiveAudioSignal(activity) {
|
|
605739
605761
|
if (!activity) return false;
|
|
605740
|
-
return activity.peak >= 0.
|
|
605762
|
+
return activity.peak >= 0.012 || activity.rmsDb > -55 || activity.activeRatio >= 0.015;
|
|
605741
605763
|
}
|
|
605742
605764
|
function audioCaptureMethod(device) {
|
|
605743
605765
|
return device.startsWith("pulse:") ? "pulse/ffmpeg" : "arecord";
|
|
@@ -696637,6 +696659,7 @@ var init_voicechat = __esm({
|
|
|
696637
696659
|
SYSTEM_PROMPT2 = `You are a voice assistant having a live spoken conversation. Keep responses extremely brief — 1-2 sentences max. You're speaking aloud, not writing. Be conversational, direct, and helpful. Don't use markdown or formatting — just natural speech.
|
|
696638
696660
|
|
|
696639
696661
|
Rules:
|
|
696662
|
+
- Live perception: each turn you may receive a read-only "Context snapshot" system message containing what the camera currently sees (objects, people, classifications, recent visual events) and what is currently heard (sound scene, recent sounds, transcripts). Treat it as your own live sight and hearing. When the user asks what you see, who is there, or what you hear, answer directly from that snapshot. If the snapshot is missing or stale, say you can't see/hear clearly right now — do not invent observations.
|
|
696640
696663
|
- Never invent environment facts (cwd, OS, specs, repo state). If you need a precise fact from the main agent, request a tool by outputting on a single line EXACTLY one JSON object: {"tool": string, "args": object} and nothing else. Then wait for the tool result before answering.
|
|
696641
696664
|
- You may also request to relay a user task to the main agent by emitting {"tool":"voice_to_main","args":{"message":"...","start":true}}.
|
|
696642
696665
|
- Prefer tools for factual queries; otherwise, answer directly with a short reply.
|
|
@@ -736343,7 +736366,7 @@ Respond concisely and safely. Remember: you are talking to the general public.`;
|
|
|
736343
736366
|
} else {
|
|
736344
736367
|
parts.push("active: no");
|
|
736345
736368
|
}
|
|
736346
|
-
const liveContext = formatLiveSensorContext(repoRoot);
|
|
736369
|
+
const liveContext = formatLiveSensorContext(repoRoot, { toolCapable: false, maxChars: 7e3 });
|
|
736347
736370
|
if (liveContext) parts.push(liveContext);
|
|
736348
736371
|
return parts.join("\n");
|
|
736349
736372
|
}
|
|
@@ -165,6 +165,24 @@ def main():
|
|
|
165
165
|
window_samples = int(args.window_seconds * SAMPLE_RATE)
|
|
166
166
|
last_text = ""
|
|
167
167
|
running = True
|
|
168
|
+
noise_floor = None # EMA of the quietest 0.5s block RMS — adapts to the capture chain
|
|
169
|
+
|
|
170
|
+
def block_energies(samples):
|
|
171
|
+
"""RMS of consecutive 0.5s blocks — lets speech anywhere in the
|
|
172
|
+
window register even when most of the window is silence."""
|
|
173
|
+
block = SAMPLE_RATE // 2
|
|
174
|
+
n = max(1, len(samples) // block)
|
|
175
|
+
return [float(np.sqrt(np.mean(samples[i * block:(i + 1) * block] ** 2))) for i in range(n)]
|
|
176
|
+
|
|
177
|
+
def amplify(samples):
|
|
178
|
+
"""Bring quiet-but-real speech toward full scale. Whisper performs far
|
|
179
|
+
better near normal levels; quiet capture chains (distant speakers,
|
|
180
|
+
low source gain, beamformed arrays) otherwise force users to shout
|
|
181
|
+
into the mic. Gain capped at 40x (~32dB), output clipped to [-1, 1]."""
|
|
182
|
+
peak = float(np.max(np.abs(samples))) if len(samples) else 0.0
|
|
183
|
+
if 0 < peak < 0.5:
|
|
184
|
+
samples = np.clip(samples * min(40.0, 0.9 / max(peak, 1e-4)), -1.0, 1.0)
|
|
185
|
+
return samples
|
|
168
186
|
|
|
169
187
|
def read_stdin():
|
|
170
188
|
"""Read PCM16 from stdin in a background thread."""
|
|
@@ -197,18 +215,25 @@ def main():
|
|
|
197
215
|
# Take the last window_seconds of audio
|
|
198
216
|
window = audio_buf[-window_samples:].copy() if len(audio_buf) > window_samples else audio_buf.copy()
|
|
199
217
|
|
|
200
|
-
#
|
|
201
|
-
#
|
|
202
|
-
#
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
218
|
+
# Adaptive silence gate: whisper hallucinates repetitive garbage
|
|
219
|
+
# on silent input, but absolute thresholds reject real speech on
|
|
220
|
+
# quiet capture chains. Instead track the noise floor (quietest
|
|
221
|
+
# 0.5s block) and require the loudest block to rise above it.
|
|
222
|
+
energies = block_energies(window)
|
|
223
|
+
quiet = min(energies)
|
|
224
|
+
loud = max(energies)
|
|
225
|
+
if noise_floor is None:
|
|
226
|
+
noise_floor = quiet
|
|
227
|
+
elif quiet < noise_floor:
|
|
228
|
+
noise_floor = 0.7 * noise_floor + 0.3 * quiet # drop fast
|
|
229
|
+
else:
|
|
230
|
+
noise_floor = 0.995 * noise_floor + 0.005 * quiet # creep up slowly
|
|
231
|
+
gate = max(1.5e-4, noise_floor * 2.5) # ≥ ~8dB above floor, ~-76dB min
|
|
232
|
+
if loud < gate:
|
|
206
233
|
continue
|
|
207
234
|
|
|
208
|
-
#
|
|
209
|
-
|
|
210
|
-
if peak < 0.3:
|
|
211
|
-
window = window * min(20.0, 0.9 / max(peak, 1e-4))
|
|
235
|
+
# Amplify toward full scale before transcription
|
|
236
|
+
window = amplify(window)
|
|
212
237
|
|
|
213
238
|
# Transcribe
|
|
214
239
|
try:
|
|
@@ -234,12 +259,11 @@ def main():
|
|
|
234
259
|
with buf_lock:
|
|
235
260
|
full_audio = audio_buf.copy()
|
|
236
261
|
|
|
237
|
-
|
|
238
|
-
|
|
262
|
+
final_loud = max(block_energies(full_audio)) if len(full_audio) >= SAMPLE_RATE else 0.0
|
|
263
|
+
final_gate = max(1.5e-4, (noise_floor or 0.0) * 2.5)
|
|
264
|
+
if len(full_audio) >= SAMPLE_RATE and final_loud >= final_gate:
|
|
239
265
|
try:
|
|
240
|
-
|
|
241
|
-
if 0 < final_peak < 0.3:
|
|
242
|
-
full_audio = full_audio * min(20.0, 0.9 / max(final_peak, 1e-4))
|
|
266
|
+
full_audio = amplify(full_audio)
|
|
243
267
|
fp16 = (device == "cuda")
|
|
244
268
|
result = model.transcribe(
|
|
245
269
|
full_audio,
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omnius",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.419",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "omnius",
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.419",
|
|
10
10
|
"bundleDependencies": [
|
|
11
11
|
"image-to-ascii"
|
|
12
12
|
],
|
package/package.json
CHANGED