accelr8 3.1.1 → 3.2.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/dist/chunks/chunk-Z2VXUT5L.js +64 -0
- package/dist/chunks/client-GZH54N5H.js +4479 -0
- package/dist/chunks/client-KW5GM7FV.js +4479 -0
- package/dist/chunks/{client-J62VQCBE.js → client-QQTXQT4O.js} +102 -11
- package/dist/chunks/server-2ZSEVMOZ.js +1024 -0
- package/dist/chunks/server-QQOOCMUI.js +1024 -0
- package/dist/cli.js +3 -3
- package/package.json +5 -3
|
@@ -1175,8 +1175,48 @@ function microLine(w, engine, frame2, info) {
|
|
|
1175
1175
|
|
|
1176
1176
|
// src/voice.ts
|
|
1177
1177
|
import { spawn, spawnSync } from "node:child_process";
|
|
1178
|
+
import fs2 from "node:fs";
|
|
1179
|
+
import path2 from "node:path";
|
|
1180
|
+
|
|
1181
|
+
// src/voicelog.ts
|
|
1178
1182
|
import fs from "node:fs";
|
|
1183
|
+
import os from "node:os";
|
|
1179
1184
|
import path from "node:path";
|
|
1185
|
+
var logPath = null;
|
|
1186
|
+
var enabled = true;
|
|
1187
|
+
function file() {
|
|
1188
|
+
if (!logPath) {
|
|
1189
|
+
const dir = path.join(os.homedir(), ".accelr8");
|
|
1190
|
+
try {
|
|
1191
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1192
|
+
} catch {
|
|
1193
|
+
}
|
|
1194
|
+
logPath = path.join(dir, "voice.log");
|
|
1195
|
+
}
|
|
1196
|
+
return logPath;
|
|
1197
|
+
}
|
|
1198
|
+
function stamp(nowMs) {
|
|
1199
|
+
const d = new Date(nowMs);
|
|
1200
|
+
const p = (n, w = 2) => String(n).padStart(w, "0");
|
|
1201
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}.${p(d.getMilliseconds(), 3)}`;
|
|
1202
|
+
}
|
|
1203
|
+
function vlog(nowMs, stage, msg, data) {
|
|
1204
|
+
if (!enabled) return;
|
|
1205
|
+
try {
|
|
1206
|
+
const extra = data ? " " + Object.entries(data).map(([k, v]) => `${k}=${String(v).slice(0, 80)}`).join(" ") : "";
|
|
1207
|
+
fs.appendFileSync(file(), `${stamp(nowMs)} [${stage}] ${msg}${extra}
|
|
1208
|
+
`);
|
|
1209
|
+
} catch {
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
function vlogSession(nowMs, header) {
|
|
1213
|
+
vlog(nowMs, "session", "\u2500\u2500\u2500\u2500 voice session \u2500\u2500\u2500\u2500", header);
|
|
1214
|
+
}
|
|
1215
|
+
function voiceLogPath() {
|
|
1216
|
+
return file();
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// src/voice.ts
|
|
1180
1220
|
var SAMPLE_RATE = 16e3;
|
|
1181
1221
|
var CHUNK_BYTES = 1280;
|
|
1182
1222
|
var KILL_GRACE_MS = 300;
|
|
@@ -1235,13 +1275,13 @@ function detectVoiceTools() {
|
|
|
1235
1275
|
}
|
|
1236
1276
|
function which(bin) {
|
|
1237
1277
|
const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
|
|
1238
|
-
for (const dir of (process.env.PATH ?? "").split(
|
|
1278
|
+
for (const dir of (process.env.PATH ?? "").split(path2.delimiter).filter(Boolean)) {
|
|
1239
1279
|
for (const ext of exts) {
|
|
1240
|
-
const candidate =
|
|
1280
|
+
const candidate = path2.join(dir, bin + ext);
|
|
1241
1281
|
try {
|
|
1242
|
-
if (!
|
|
1243
|
-
|
|
1244
|
-
if (
|
|
1282
|
+
if (!fs2.existsSync(candidate)) continue;
|
|
1283
|
+
fs2.accessSync(candidate, fs2.constants.X_OK);
|
|
1284
|
+
if (fs2.statSync(candidate).isFile()) return candidate;
|
|
1245
1285
|
} catch {
|
|
1246
1286
|
}
|
|
1247
1287
|
}
|
|
@@ -1309,12 +1349,15 @@ var VoiceCapture = class {
|
|
|
1309
1349
|
try {
|
|
1310
1350
|
child = spawn(this.tools.ffmpegPath, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1311
1351
|
} catch (e) {
|
|
1352
|
+
vlog(Date.now(), "capture", "ffmpeg spawn threw", { backend, err: errMsg(e) });
|
|
1312
1353
|
queueMicrotask(() => opts.onError("mic capture failed to start: " + errMsg(e)));
|
|
1313
1354
|
return null;
|
|
1314
1355
|
}
|
|
1356
|
+
vlog(Date.now(), "capture", "ffmpeg spawned", { backend, device: opts.device ?? "default", args: args.join(" ") });
|
|
1315
1357
|
const s = { child, onChunk: opts.onChunk, buf: Buffer.alloc(0), stderrTail: "", produced: false, stopped: false };
|
|
1316
1358
|
child.stdout?.on("data", (chunk) => {
|
|
1317
1359
|
if (s.stopped) return;
|
|
1360
|
+
if (!s.produced) vlog(Date.now(), "capture", "first audio bytes from mic", { backend, bytes: chunk.length });
|
|
1318
1361
|
s.produced = true;
|
|
1319
1362
|
s.buf = s.buf.length ? Buffer.concat([s.buf, chunk]) : chunk;
|
|
1320
1363
|
const whole = Math.floor(s.buf.length / CHUNK_BYTES) * CHUNK_BYTES;
|
|
@@ -1329,11 +1372,14 @@ var VoiceCapture = class {
|
|
|
1329
1372
|
child.on("error", (e) => {
|
|
1330
1373
|
if (s.stopped || this.session !== s) return;
|
|
1331
1374
|
this.session = null;
|
|
1375
|
+
vlog(Date.now(), "capture", "ffmpeg error", { backend, code: e.code, produced: s.produced, stderr: s.stderrTail.trim() });
|
|
1332
1376
|
opts.onError(e.code === "ENOENT" ? "ffmpeg not found" : "mic capture error: " + errMsg(e));
|
|
1333
1377
|
});
|
|
1334
1378
|
child.on("close", (code) => {
|
|
1335
1379
|
if (s.stopped || this.session !== s) return;
|
|
1380
|
+
vlog(Date.now(), "capture", "ffmpeg exited", { backend, code, producedAudio: s.produced, stderr: s.stderrTail.trim() });
|
|
1336
1381
|
if (backend === "primary" && process.platform === "linux" && !s.produced && code !== 0) {
|
|
1382
|
+
vlog(Date.now(), "capture", "alsa failed, retrying on pulse");
|
|
1337
1383
|
const pulse = captureArgs("linux", opts.device).map((a) => a === "alsa" ? "pulse" : a);
|
|
1338
1384
|
this.session = this.spawnSession(opts, pulse, "pulse");
|
|
1339
1385
|
return;
|
|
@@ -1438,22 +1484,32 @@ var VoicePlayback = class {
|
|
|
1438
1484
|
this.players.clear();
|
|
1439
1485
|
}
|
|
1440
1486
|
spawnPlayer(speakerId) {
|
|
1487
|
+
const args = playbackArgs(this.tools.ffplayPath);
|
|
1441
1488
|
let child;
|
|
1442
1489
|
try {
|
|
1443
|
-
child = spawn(this.tools.ffplayPath,
|
|
1444
|
-
} catch {
|
|
1490
|
+
child = spawn(this.tools.ffplayPath, args, { stdio: ["pipe", "ignore", "pipe"] });
|
|
1491
|
+
} catch (e) {
|
|
1492
|
+
vlog(Date.now(), "playback", "ffplay spawn threw", { speaker: speakerId, err: errMsg(e) });
|
|
1445
1493
|
return null;
|
|
1446
1494
|
}
|
|
1495
|
+
vlog(Date.now(), "playback", "ffplay spawned", { speaker: speakerId, args: args.join(" ") });
|
|
1447
1496
|
const p = { child, alive: true, idleTimer: null, pre: [], flowing: false };
|
|
1497
|
+
let stderrTail = "";
|
|
1498
|
+
child.stderr?.setEncoding("utf8");
|
|
1499
|
+
child.stderr?.on("data", (c) => {
|
|
1500
|
+
stderrTail = (stderrTail + c).slice(-STDERR_TAIL);
|
|
1501
|
+
});
|
|
1448
1502
|
child.stdin?.on("error", () => {
|
|
1449
1503
|
p.alive = false;
|
|
1450
1504
|
});
|
|
1451
|
-
child.on("error", () => {
|
|
1505
|
+
child.on("error", (e) => {
|
|
1452
1506
|
p.alive = false;
|
|
1507
|
+
vlog(Date.now(), "playback", "ffplay error", { speaker: speakerId, code: e.code, msg: errMsg(e) });
|
|
1453
1508
|
if (this.players.get(speakerId) === p) this.reap(speakerId, p);
|
|
1454
1509
|
});
|
|
1455
|
-
child.on("close", () => {
|
|
1510
|
+
child.on("close", (code) => {
|
|
1456
1511
|
p.alive = false;
|
|
1512
|
+
vlog(Date.now(), "playback", "ffplay exited", { speaker: speakerId, code, stderr: stderrTail.trim() });
|
|
1457
1513
|
if (this.players.get(speakerId) === p) {
|
|
1458
1514
|
if (p.idleTimer) {
|
|
1459
1515
|
clearTimeout(p.idleTimer);
|
|
@@ -1937,6 +1993,8 @@ var voiceCapture = null;
|
|
|
1937
1993
|
var voicePlayback = null;
|
|
1938
1994
|
var voiceDevice = process.env.ACCELR8_MIC || void 0;
|
|
1939
1995
|
var voiceRetries = 0;
|
|
1996
|
+
var voiceSent = 0;
|
|
1997
|
+
var voiceRecv = /* @__PURE__ */ new Map();
|
|
1940
1998
|
function autoPickMic() {
|
|
1941
1999
|
if (process.platform === "darwin") {
|
|
1942
2000
|
const mics = listMicDevices();
|
|
@@ -1958,7 +2016,11 @@ function startCapture() {
|
|
|
1958
2016
|
device: voiceDevice,
|
|
1959
2017
|
onChunk: (b64) => {
|
|
1960
2018
|
voiceRetries = 0;
|
|
1961
|
-
if (voiceTransmit)
|
|
2019
|
+
if (!voiceTransmit) return;
|
|
2020
|
+
voiceSent++;
|
|
2021
|
+
if (voiceSent === 1) vlog(Date.now(), "transmit", "first frame sent to server");
|
|
2022
|
+
else if (voiceSent % 125 === 0) vlog(Date.now(), "transmit", "still sending", { frames: voiceSent });
|
|
2023
|
+
sendMsg({ t: "voiceFrame", data: b64 });
|
|
1962
2024
|
},
|
|
1963
2025
|
onError: (m) => {
|
|
1964
2026
|
if (!voiceTransmit) return;
|
|
@@ -1991,12 +2053,25 @@ function toggleVoice() {
|
|
|
1991
2053
|
voiceCapture = new VoiceCapture(voiceTools);
|
|
1992
2054
|
voiceOn = true;
|
|
1993
2055
|
voiceTransmit = false;
|
|
2056
|
+
voiceSent = 0;
|
|
2057
|
+
voiceRecv.clear();
|
|
1994
2058
|
if (voiceDevice === void 0 && voiceTools.capture) voiceDevice = autoPickMic();
|
|
2059
|
+
vlogSession(Date.now(), {
|
|
2060
|
+
platform: process.platform,
|
|
2061
|
+
version: PROTO,
|
|
2062
|
+
name: me.name,
|
|
2063
|
+
ffmpeg: voiceTools.ffmpegPath ?? "MISSING",
|
|
2064
|
+
ffplay: voiceTools.ffplayPath ?? "MISSING",
|
|
2065
|
+
capture: voiceTools.capture,
|
|
2066
|
+
playback: voiceTools.playback,
|
|
2067
|
+
mic: voiceDevice ?? "default"
|
|
2068
|
+
});
|
|
1995
2069
|
sendMsg({ t: "voiceJoin" });
|
|
1996
2070
|
state.status = voiceTools.capture ? "voice on \xB7 /talk (or ctrl-g) to speak" : "voice on \xB7 listen only (no mic found)";
|
|
1997
2071
|
}
|
|
1998
2072
|
function leaveVoice() {
|
|
1999
2073
|
if (!voiceOn) return;
|
|
2074
|
+
vlog(Date.now(), "session", "leaving voice", { sent: voiceSent, recvSpeakers: voiceRecv.size, recvTotal: [...voiceRecv.values()].reduce((a, b) => a + b, 0) });
|
|
2000
2075
|
voiceCapture?.stop();
|
|
2001
2076
|
voicePlayback?.stopAll();
|
|
2002
2077
|
voiceCapture = null;
|
|
@@ -2452,7 +2527,12 @@ function handleServer(msg) {
|
|
|
2452
2527
|
state.voicePeers = Array.isArray(msg.peers) ? msg.peers : [];
|
|
2453
2528
|
break;
|
|
2454
2529
|
case "voiceFrame":
|
|
2455
|
-
if (voiceOn)
|
|
2530
|
+
if (voiceOn) {
|
|
2531
|
+
const n = (voiceRecv.get(msg.sid) ?? 0) + 1;
|
|
2532
|
+
voiceRecv.set(msg.sid, n);
|
|
2533
|
+
if (n === 1) vlog(Date.now(), "receive", "first frame received from a peer", { sid: msg.sid.slice(0, 8) });
|
|
2534
|
+
voicePlayback?.feed(msg.sid, msg.data);
|
|
2535
|
+
}
|
|
2456
2536
|
break;
|
|
2457
2537
|
case "error": {
|
|
2458
2538
|
if (msg.code === "chatRate" || msg.code === "msgTooLarge") {
|
|
@@ -3947,6 +4027,17 @@ function slashCommand(text) {
|
|
|
3947
4027
|
toggleVoice();
|
|
3948
4028
|
} else if (cmd === "/talk" || cmd === "/mute") {
|
|
3949
4029
|
toggleTalk();
|
|
4030
|
+
} else if (cmd === "/voice debug") {
|
|
4031
|
+
const recvTotal = [...voiceRecv.values()].reduce((a, b) => a + b, 0);
|
|
4032
|
+
sidecarBlock = { title: "voice debug", at: Date.now(), lines: [
|
|
4033
|
+
`on: ${voiceOn} \xB7 live: ${voiceTransmit}`,
|
|
4034
|
+
`tools: ffmpeg ${voiceTools?.ffmpegPath ? "ok" : "MISSING"} \xB7 ffplay ${voiceTools?.ffplayPath ? "ok" : "MISSING"}`,
|
|
4035
|
+
`mic: ${voiceDevice ?? "default"} \xB7 peers: ${state.voicePeers.length}`,
|
|
4036
|
+
`sent: ${voiceSent} \xB7 received: ${recvTotal} from ${voiceRecv.size}`,
|
|
4037
|
+
`log: ${voiceLogPath()}`,
|
|
4038
|
+
"send me that file if voice fails"
|
|
4039
|
+
] };
|
|
4040
|
+
state.status = "voice debug \xB7 see the panel \xB7 log at ~/.accelr8/voice.log";
|
|
3950
4041
|
} else if (cmd === "/voice mics" || cmd === "/mics") {
|
|
3951
4042
|
const mics = listMicDevices();
|
|
3952
4043
|
sidecarBlock = { title: "microphones", at: Date.now(), lines: mics.length ? mics.map((m) => `${m.index} ${m.name}${voiceDevice === m.index ? " \xB7 selected" : ""}`).concat("use: /voice mic <index>") : ["none found (need ffmpeg)"] };
|