accelr8 3.1.1 → 3.3.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/app-VQ33JVDI.js +793 -0
- package/dist/chunks/chunk-HY6ULYGH.js +559 -0
- package/dist/chunks/{chunk-AY3UXM6A.js → chunk-TW4QNNNT.js} +1 -1
- package/dist/chunks/client-6SYGGJ7L.js +4035 -0
- package/dist/chunks/{server-EFKIKFUG.js → server-VWQ2NYRN.js} +62 -14
- package/dist/cli.js +17 -4
- package/package.json +5 -3
- package/dist/chunks/client-J62VQCBE.js +0 -4331
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/voicelog.ts
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
var logPath = null;
|
|
8
|
+
var enabled = true;
|
|
9
|
+
function file() {
|
|
10
|
+
if (!logPath) {
|
|
11
|
+
const dir = path.join(os.homedir(), ".accelr8");
|
|
12
|
+
try {
|
|
13
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
16
|
+
logPath = path.join(dir, "voice.log");
|
|
17
|
+
}
|
|
18
|
+
return logPath;
|
|
19
|
+
}
|
|
20
|
+
function stamp(nowMs) {
|
|
21
|
+
const d = new Date(nowMs);
|
|
22
|
+
const p = (n, w = 2) => String(n).padStart(w, "0");
|
|
23
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}.${p(d.getMilliseconds(), 3)}`;
|
|
24
|
+
}
|
|
25
|
+
function vlog(nowMs, stage, msg, data) {
|
|
26
|
+
if (!enabled) return;
|
|
27
|
+
try {
|
|
28
|
+
const extra = data ? " " + Object.entries(data).map(([k, v]) => `${k}=${String(v).slice(0, 80)}`).join(" ") : "";
|
|
29
|
+
fs.appendFileSync(file(), `${stamp(nowMs)} [${stage}] ${msg}${extra}
|
|
30
|
+
`);
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function vlogSession(nowMs, header) {
|
|
35
|
+
vlog(nowMs, "session", "\u2500\u2500\u2500\u2500 voice session \u2500\u2500\u2500\u2500", header);
|
|
36
|
+
}
|
|
37
|
+
function voiceLogPath() {
|
|
38
|
+
return file();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/voice.ts
|
|
42
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
43
|
+
import fs2 from "node:fs";
|
|
44
|
+
import path2 from "node:path";
|
|
45
|
+
var SAMPLE_RATE = 16e3;
|
|
46
|
+
var CHUNK_BYTES = 1280;
|
|
47
|
+
var KILL_GRACE_MS = 300;
|
|
48
|
+
var SPEAKER_IDLE_MS = 2e3;
|
|
49
|
+
var PREBUFFER_BYTES = 4800;
|
|
50
|
+
var DRAIN_KILL_MS = 2e3;
|
|
51
|
+
var MAX_PLAYERS = 8;
|
|
52
|
+
var STDERR_TAIL = 400;
|
|
53
|
+
var VERIFY_MIN_BYTES = SAMPLE_RATE * 2 / 10;
|
|
54
|
+
function playbackArgs(ffplayPath) {
|
|
55
|
+
const ch = ffplayChannelFlag(ffplayPath);
|
|
56
|
+
return [
|
|
57
|
+
"-hide_banner",
|
|
58
|
+
"-loglevel",
|
|
59
|
+
"error",
|
|
60
|
+
"-nodisp",
|
|
61
|
+
"-sync",
|
|
62
|
+
"audio",
|
|
63
|
+
"-infbuf",
|
|
64
|
+
"-f",
|
|
65
|
+
"s16le",
|
|
66
|
+
"-ar",
|
|
67
|
+
String(SAMPLE_RATE),
|
|
68
|
+
...ch,
|
|
69
|
+
"-i",
|
|
70
|
+
"-"
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
var chFlagCache = null;
|
|
74
|
+
function ffplayChannelFlag(ffplayPath) {
|
|
75
|
+
if (chFlagCache) return chFlagCache;
|
|
76
|
+
let modern = true;
|
|
77
|
+
try {
|
|
78
|
+
const out = spawnSync(ffplayPath, ["-hide_banner", "-version"], { encoding: "utf8", timeout: 3e3 });
|
|
79
|
+
const m = ((out.stdout || "") + (out.stderr || "")).match(/version\s+n?(\d+)\.(\d+)/i);
|
|
80
|
+
if (m) {
|
|
81
|
+
const major = +m[1], minor = +m[2];
|
|
82
|
+
modern = major > 5 || major === 5 && minor >= 1;
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
chFlagCache = modern ? ["-ch_layout", "mono"] : ["-ac", "1"];
|
|
87
|
+
return chFlagCache;
|
|
88
|
+
}
|
|
89
|
+
function detectVoiceTools() {
|
|
90
|
+
const ffmpegPath = which("ffmpeg");
|
|
91
|
+
const ffplayPath = which("ffplay");
|
|
92
|
+
const out = { capture: !!ffmpegPath, playback: !!ffplayPath };
|
|
93
|
+
if (ffmpegPath) out.ffmpegPath = ffmpegPath;
|
|
94
|
+
if (ffplayPath) out.ffplayPath = ffplayPath;
|
|
95
|
+
if (!ffmpegPath || !ffplayPath) {
|
|
96
|
+
const missing = [ffmpegPath ? "" : "ffmpeg", ffplayPath ? "" : "ffplay"].filter(Boolean).join(" + ");
|
|
97
|
+
out.reason = `${missing} not found on PATH (voice needs ffmpeg; brew/apt install ffmpeg)`;
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
function which(bin) {
|
|
102
|
+
const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
|
|
103
|
+
for (const dir of (process.env.PATH ?? "").split(path2.delimiter).filter(Boolean)) {
|
|
104
|
+
for (const ext of exts) {
|
|
105
|
+
const candidate = path2.join(dir, bin + ext);
|
|
106
|
+
try {
|
|
107
|
+
if (!fs2.existsSync(candidate)) continue;
|
|
108
|
+
fs2.accessSync(candidate, fs2.constants.X_OK);
|
|
109
|
+
if (fs2.statSync(candidate).isFile()) return candidate;
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return void 0;
|
|
115
|
+
}
|
|
116
|
+
function captureArgs(platform, device) {
|
|
117
|
+
const base = ["-hide_banner", "-loglevel", "error"];
|
|
118
|
+
const output = ["-ac", "1", "-ar", String(SAMPLE_RATE), "-f", "s16le", "-"];
|
|
119
|
+
if (platform === "darwin") return [...base, "-f", "avfoundation", "-i", ":" + (device ?? "default"), ...output];
|
|
120
|
+
if (platform === "win32") return [...base, "-f", "dshow", "-i", "audio=" + (device ?? "default"), ...output];
|
|
121
|
+
return [...base, "-f", "alsa", "-i", device ?? "default", ...output];
|
|
122
|
+
}
|
|
123
|
+
function chunkPcm(buf, chunkBytes) {
|
|
124
|
+
if (!Number.isInteger(chunkBytes) || chunkBytes <= 0) throw new RangeError("chunkBytes must be a positive integer");
|
|
125
|
+
const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
126
|
+
const out = [];
|
|
127
|
+
for (let i = 0; i < b.length; i += chunkBytes) out.push(b.subarray(i, i + chunkBytes).toString("base64"));
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
var VoiceCapture = class {
|
|
131
|
+
constructor(tools) {
|
|
132
|
+
this.tools = tools;
|
|
133
|
+
}
|
|
134
|
+
tools;
|
|
135
|
+
session = null;
|
|
136
|
+
get active() {
|
|
137
|
+
return this.session !== null;
|
|
138
|
+
}
|
|
139
|
+
/** Spawn ffmpeg mic->PCM and stream ~40ms base64 chunks to onChunk.
|
|
140
|
+
* No-op while a capture is already active. */
|
|
141
|
+
start(opts) {
|
|
142
|
+
if (this.session) return;
|
|
143
|
+
if (!this.tools.capture || !this.tools.ffmpegPath) {
|
|
144
|
+
const why = this.tools.reason ?? "ffmpeg not found on PATH";
|
|
145
|
+
queueMicrotask(() => opts.onError(why));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
this.session = this.spawnSession(opts, captureArgs(process.platform, opts.device), "primary");
|
|
149
|
+
}
|
|
150
|
+
/** Flush the sub-chunk remainder, then SIGINT -> SIGKILL after 300ms. */
|
|
151
|
+
stop() {
|
|
152
|
+
const s = this.session;
|
|
153
|
+
if (!s) return;
|
|
154
|
+
this.session = null;
|
|
155
|
+
s.stopped = true;
|
|
156
|
+
if (s.buf.length) {
|
|
157
|
+
s.onChunk(s.buf.toString("base64"));
|
|
158
|
+
s.buf = Buffer.alloc(0);
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
s.child.kill("SIGINT");
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
const t = setTimeout(() => {
|
|
165
|
+
try {
|
|
166
|
+
s.child.kill("SIGKILL");
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
}, KILL_GRACE_MS);
|
|
170
|
+
t.unref();
|
|
171
|
+
}
|
|
172
|
+
spawnSession(opts, args, backend) {
|
|
173
|
+
let child;
|
|
174
|
+
try {
|
|
175
|
+
child = spawn(this.tools.ffmpegPath, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
176
|
+
} catch (e) {
|
|
177
|
+
vlog(Date.now(), "capture", "ffmpeg spawn threw", { backend, err: errMsg(e) });
|
|
178
|
+
queueMicrotask(() => opts.onError("mic capture failed to start: " + errMsg(e)));
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
vlog(Date.now(), "capture", "ffmpeg spawned", { backend, device: opts.device ?? "default", args: args.join(" ") });
|
|
182
|
+
const s = { child, onChunk: opts.onChunk, buf: Buffer.alloc(0), stderrTail: "", produced: false, stopped: false };
|
|
183
|
+
child.stdout?.on("data", (chunk) => {
|
|
184
|
+
if (s.stopped) return;
|
|
185
|
+
if (!s.produced) vlog(Date.now(), "capture", "first audio bytes from mic", { backend, bytes: chunk.length });
|
|
186
|
+
s.produced = true;
|
|
187
|
+
s.buf = s.buf.length ? Buffer.concat([s.buf, chunk]) : chunk;
|
|
188
|
+
const whole = Math.floor(s.buf.length / CHUNK_BYTES) * CHUNK_BYTES;
|
|
189
|
+
if (whole === 0) return;
|
|
190
|
+
for (const b64 of chunkPcm(s.buf.subarray(0, whole), CHUNK_BYTES)) opts.onChunk(b64);
|
|
191
|
+
s.buf = s.buf.subarray(whole);
|
|
192
|
+
});
|
|
193
|
+
child.stderr?.setEncoding("utf8");
|
|
194
|
+
child.stderr?.on("data", (c) => {
|
|
195
|
+
s.stderrTail = (s.stderrTail + c).slice(-STDERR_TAIL);
|
|
196
|
+
});
|
|
197
|
+
child.on("error", (e) => {
|
|
198
|
+
if (s.stopped || this.session !== s) return;
|
|
199
|
+
this.session = null;
|
|
200
|
+
vlog(Date.now(), "capture", "ffmpeg error", { backend, code: e.code, produced: s.produced, stderr: s.stderrTail.trim() });
|
|
201
|
+
opts.onError(e.code === "ENOENT" ? "ffmpeg not found" : "mic capture error: " + errMsg(e));
|
|
202
|
+
});
|
|
203
|
+
child.on("close", (code) => {
|
|
204
|
+
if (s.stopped || this.session !== s) return;
|
|
205
|
+
vlog(Date.now(), "capture", "ffmpeg exited", { backend, code, producedAudio: s.produced, stderr: s.stderrTail.trim() });
|
|
206
|
+
if (backend === "primary" && process.platform === "linux" && !s.produced && code !== 0) {
|
|
207
|
+
vlog(Date.now(), "capture", "alsa failed, retrying on pulse");
|
|
208
|
+
const pulse = captureArgs("linux", opts.device).map((a) => a === "alsa" ? "pulse" : a);
|
|
209
|
+
this.session = this.spawnSession(opts, pulse, "pulse");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
this.session = null;
|
|
213
|
+
const tail = s.stderrTail.trim();
|
|
214
|
+
opts.onError(code === 0 ? "mic capture ended unexpectedly" : "mic capture failed: " + (tail || `ffmpeg exited with code ${code}`));
|
|
215
|
+
});
|
|
216
|
+
return s;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
var VoicePlayback = class {
|
|
220
|
+
constructor(tools) {
|
|
221
|
+
this.tools = tools;
|
|
222
|
+
}
|
|
223
|
+
tools;
|
|
224
|
+
players = /* @__PURE__ */ new Map();
|
|
225
|
+
/** ids with an active player */
|
|
226
|
+
get speakers() {
|
|
227
|
+
return [...this.players.keys()];
|
|
228
|
+
}
|
|
229
|
+
/** Feed a received chunk; lazily spawns one ffplay per speaker. Chunks
|
|
230
|
+
* are dropped silently when playback is unavailable, the speaker cap
|
|
231
|
+
* is hit, or a player cannot be (re)spawned. */
|
|
232
|
+
feed(speakerId, b64) {
|
|
233
|
+
if (!this.tools.playback || !this.tools.ffplayPath) return;
|
|
234
|
+
const bytes = Buffer.from(b64, "base64");
|
|
235
|
+
if (bytes.length === 0) return;
|
|
236
|
+
let p = this.players.get(speakerId);
|
|
237
|
+
if (p && !p.alive) {
|
|
238
|
+
this.reap(speakerId, p);
|
|
239
|
+
p = void 0;
|
|
240
|
+
}
|
|
241
|
+
if (!p) {
|
|
242
|
+
if (this.players.size >= MAX_PLAYERS) return;
|
|
243
|
+
const spawned = this.spawnPlayer(speakerId);
|
|
244
|
+
if (!spawned) return;
|
|
245
|
+
p = spawned;
|
|
246
|
+
}
|
|
247
|
+
if (!p.flowing) {
|
|
248
|
+
p.pre.push(bytes);
|
|
249
|
+
if (p.pre.reduce((n, b) => n + b.length, 0) >= PREBUFFER_BYTES) {
|
|
250
|
+
const cushion = Buffer.concat(p.pre);
|
|
251
|
+
p.pre = [];
|
|
252
|
+
p.flowing = true;
|
|
253
|
+
const np2 = this.writeOrRespawn(speakerId, p, cushion);
|
|
254
|
+
if (np2) this.touch(speakerId, np2);
|
|
255
|
+
} else {
|
|
256
|
+
this.touch(speakerId, p);
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const np = this.writeOrRespawn(speakerId, p, bytes);
|
|
261
|
+
if (np) this.touch(speakerId, np);
|
|
262
|
+
}
|
|
263
|
+
/** Write to a player; on EPIPE (it died) respawn once and write there. Returns
|
|
264
|
+
* the live player, or null if it couldn't be (re)spawned. */
|
|
265
|
+
writeOrRespawn(speakerId, p, buf) {
|
|
266
|
+
if (this.write(p, buf)) return p;
|
|
267
|
+
this.reap(speakerId, p);
|
|
268
|
+
const fresh = this.players.size < MAX_PLAYERS ? this.spawnPlayer(speakerId) : null;
|
|
269
|
+
if (!fresh || !this.write(fresh, buf)) return null;
|
|
270
|
+
fresh.flowing = true;
|
|
271
|
+
return fresh;
|
|
272
|
+
}
|
|
273
|
+
/** Graceful close for one speaker (voiceStop): end stdin so ffplay drains
|
|
274
|
+
* its buffered audio, then a SIGKILL backstop after 2s (we dropped -autoexit,
|
|
275
|
+
* so the backstop is what actually reaps the idle player). */
|
|
276
|
+
end(speakerId) {
|
|
277
|
+
const p = this.players.get(speakerId);
|
|
278
|
+
if (!p) return;
|
|
279
|
+
this.players.delete(speakerId);
|
|
280
|
+
if (p.idleTimer) {
|
|
281
|
+
clearTimeout(p.idleTimer);
|
|
282
|
+
p.idleTimer = null;
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
p.child.stdin?.end();
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
const t = setTimeout(() => {
|
|
289
|
+
try {
|
|
290
|
+
p.child.kill("SIGKILL");
|
|
291
|
+
} catch {
|
|
292
|
+
}
|
|
293
|
+
}, DRAIN_KILL_MS);
|
|
294
|
+
t.unref();
|
|
295
|
+
}
|
|
296
|
+
/** Hard cleanup on quit: kill every player immediately. */
|
|
297
|
+
stopAll() {
|
|
298
|
+
for (const [, p] of this.players) {
|
|
299
|
+
if (p.idleTimer) {
|
|
300
|
+
clearTimeout(p.idleTimer);
|
|
301
|
+
p.idleTimer = null;
|
|
302
|
+
}
|
|
303
|
+
p.alive = false;
|
|
304
|
+
try {
|
|
305
|
+
p.child.kill("SIGKILL");
|
|
306
|
+
} catch {
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
this.players.clear();
|
|
310
|
+
}
|
|
311
|
+
spawnPlayer(speakerId) {
|
|
312
|
+
const args = playbackArgs(this.tools.ffplayPath);
|
|
313
|
+
let child;
|
|
314
|
+
try {
|
|
315
|
+
child = spawn(this.tools.ffplayPath, args, { stdio: ["pipe", "ignore", "pipe"] });
|
|
316
|
+
} catch (e) {
|
|
317
|
+
vlog(Date.now(), "playback", "ffplay spawn threw", { speaker: speakerId, err: errMsg(e) });
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
vlog(Date.now(), "playback", "ffplay spawned", { speaker: speakerId, args: args.join(" ") });
|
|
321
|
+
const p = { child, alive: true, idleTimer: null, pre: [], flowing: false };
|
|
322
|
+
let stderrTail = "";
|
|
323
|
+
child.stderr?.setEncoding("utf8");
|
|
324
|
+
child.stderr?.on("data", (c) => {
|
|
325
|
+
stderrTail = (stderrTail + c).slice(-STDERR_TAIL);
|
|
326
|
+
});
|
|
327
|
+
child.stdin?.on("error", () => {
|
|
328
|
+
p.alive = false;
|
|
329
|
+
});
|
|
330
|
+
child.on("error", (e) => {
|
|
331
|
+
p.alive = false;
|
|
332
|
+
vlog(Date.now(), "playback", "ffplay error", { speaker: speakerId, code: e.code, msg: errMsg(e) });
|
|
333
|
+
if (this.players.get(speakerId) === p) this.reap(speakerId, p);
|
|
334
|
+
});
|
|
335
|
+
child.on("close", (code) => {
|
|
336
|
+
p.alive = false;
|
|
337
|
+
vlog(Date.now(), "playback", "ffplay exited", { speaker: speakerId, code, stderr: stderrTail.trim() });
|
|
338
|
+
if (this.players.get(speakerId) === p) {
|
|
339
|
+
if (p.idleTimer) {
|
|
340
|
+
clearTimeout(p.idleTimer);
|
|
341
|
+
p.idleTimer = null;
|
|
342
|
+
}
|
|
343
|
+
this.players.delete(speakerId);
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
this.players.set(speakerId, p);
|
|
347
|
+
return p;
|
|
348
|
+
}
|
|
349
|
+
write(p, bytes) {
|
|
350
|
+
const sin = p.child.stdin;
|
|
351
|
+
if (!p.alive || !sin || sin.destroyed || !sin.writable) return false;
|
|
352
|
+
try {
|
|
353
|
+
sin.write(bytes);
|
|
354
|
+
return true;
|
|
355
|
+
} catch {
|
|
356
|
+
p.alive = false;
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
touch(speakerId, p) {
|
|
361
|
+
if (p.idleTimer) clearTimeout(p.idleTimer);
|
|
362
|
+
p.idleTimer = setTimeout(() => this.end(speakerId), SPEAKER_IDLE_MS);
|
|
363
|
+
p.idleTimer.unref();
|
|
364
|
+
}
|
|
365
|
+
reap(speakerId, p) {
|
|
366
|
+
if (p.idleTimer) {
|
|
367
|
+
clearTimeout(p.idleTimer);
|
|
368
|
+
p.idleTimer = null;
|
|
369
|
+
}
|
|
370
|
+
p.alive = false;
|
|
371
|
+
try {
|
|
372
|
+
p.child.kill("SIGKILL");
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
375
|
+
if (this.players.get(speakerId) === p) this.players.delete(speakerId);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
function listMicDevices() {
|
|
379
|
+
try {
|
|
380
|
+
if (process.platform === "darwin") {
|
|
381
|
+
const text = runForOutput(["-hide_banner", "-f", "avfoundation", "-list_devices", "true", "-i", ""]);
|
|
382
|
+
const out = [];
|
|
383
|
+
let inAudio = false;
|
|
384
|
+
for (const line of text.split("\n")) {
|
|
385
|
+
if (/audio devices/i.test(line)) {
|
|
386
|
+
inAudio = true;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (/video devices/i.test(line)) {
|
|
390
|
+
inAudio = false;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (!inAudio) continue;
|
|
394
|
+
const m = line.match(/\[(\d+)\]\s+(.+?)\s*$/);
|
|
395
|
+
if (m) out.push({ index: m[1], name: m[2] });
|
|
396
|
+
}
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
if (process.platform === "linux") {
|
|
400
|
+
const text = runForOutput(["-hide_banner", "-sources", "pulse"]);
|
|
401
|
+
const out = [];
|
|
402
|
+
for (const line of text.split("\n")) {
|
|
403
|
+
if (/auto-detected/i.test(line)) continue;
|
|
404
|
+
const m = line.match(/^\s*\*?\s*(\S+)\s+\[(.+)\]\s*$/);
|
|
405
|
+
if (m) out.push({ index: m[1], name: m[2] });
|
|
406
|
+
}
|
|
407
|
+
return out;
|
|
408
|
+
}
|
|
409
|
+
if (process.platform === "win32") {
|
|
410
|
+
const text = runForOutput(["-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "dummy"]);
|
|
411
|
+
const out = [];
|
|
412
|
+
for (const line of text.split("\n")) {
|
|
413
|
+
const m = line.match(/"(.+)"\s+\(audio\)/);
|
|
414
|
+
if (m) out.push({ index: m[1], name: m[1] });
|
|
415
|
+
}
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
return [];
|
|
419
|
+
} catch {
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
function runForOutput(args) {
|
|
424
|
+
const res = spawnSync("ffmpeg", args, { encoding: "utf8", timeout: 5e3 });
|
|
425
|
+
return (res.stderr ?? "") + (res.stdout ?? "");
|
|
426
|
+
}
|
|
427
|
+
function errMsg(e) {
|
|
428
|
+
return e instanceof Error ? e.message : String(e);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/geo.ts
|
|
432
|
+
function localTz() {
|
|
433
|
+
try {
|
|
434
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "";
|
|
435
|
+
} catch {
|
|
436
|
+
return "";
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
var TZ = {
|
|
440
|
+
// north america
|
|
441
|
+
"America/New_York": [40.7, -74],
|
|
442
|
+
"America/Detroit": [42.3, -83],
|
|
443
|
+
"America/Toronto": [43.7, -79.4],
|
|
444
|
+
"America/Chicago": [41.9, -87.6],
|
|
445
|
+
"America/Winnipeg": [49.9, -97.1],
|
|
446
|
+
"America/Mexico_City": [19.4, -99.1],
|
|
447
|
+
"America/Denver": [39.7, -104.99],
|
|
448
|
+
"America/Edmonton": [53.5, -113.5],
|
|
449
|
+
"America/Phoenix": [33.4, -112.1],
|
|
450
|
+
"America/Los_Angeles": [34.05, -118.24],
|
|
451
|
+
"America/Vancouver": [49.3, -123.1],
|
|
452
|
+
"America/Anchorage": [61.2, -149.9],
|
|
453
|
+
"Pacific/Honolulu": [21.3, -157.8],
|
|
454
|
+
"America/Halifax": [44.6, -63.6],
|
|
455
|
+
"America/St_Johns": [47.6, -52.7],
|
|
456
|
+
// south + central america
|
|
457
|
+
"America/Sao_Paulo": [-23.5, -46.6],
|
|
458
|
+
"America/Argentina/Buenos_Aires": [-34.6, -58.4],
|
|
459
|
+
"America/Santiago": [-33.5, -70.7],
|
|
460
|
+
"America/Lima": [-12, -77],
|
|
461
|
+
"America/Bogota": [4.7, -74.1],
|
|
462
|
+
"America/Caracas": [10.5, -66.9],
|
|
463
|
+
"America/Guatemala": [14.6, -90.5],
|
|
464
|
+
// europe
|
|
465
|
+
"Europe/London": [51.5, -0.13],
|
|
466
|
+
"Europe/Dublin": [53.3, -6.3],
|
|
467
|
+
"Europe/Lisbon": [38.7, -9.1],
|
|
468
|
+
"Europe/Madrid": [40.4, -3.7],
|
|
469
|
+
"Europe/Paris": [48.86, 2.35],
|
|
470
|
+
"Europe/Amsterdam": [52.4, 4.9],
|
|
471
|
+
"Europe/Brussels": [50.8, 4.4],
|
|
472
|
+
"Europe/Zurich": [47.4, 8.5],
|
|
473
|
+
"Europe/Berlin": [52.5, 13.4],
|
|
474
|
+
"Europe/Rome": [41.9, 12.5],
|
|
475
|
+
"Europe/Vienna": [48.2, 16.4],
|
|
476
|
+
"Europe/Prague": [50.1, 14.4],
|
|
477
|
+
"Europe/Copenhagen": [55.7, 12.6],
|
|
478
|
+
"Europe/Stockholm": [59.3, 18.1],
|
|
479
|
+
"Europe/Oslo": [59.9, 10.7],
|
|
480
|
+
"Europe/Warsaw": [52.2, 21],
|
|
481
|
+
"Europe/Athens": [38, 23.7],
|
|
482
|
+
"Europe/Bucharest": [44.4, 26.1],
|
|
483
|
+
"Europe/Kyiv": [50.5, 30.5],
|
|
484
|
+
"Europe/Kiev": [50.5, 30.5],
|
|
485
|
+
"Europe/Istanbul": [41, 28.98],
|
|
486
|
+
"Europe/Moscow": [55.75, 37.6],
|
|
487
|
+
"Europe/Helsinki": [60.2, 24.9],
|
|
488
|
+
// africa + middle east
|
|
489
|
+
"Africa/Casablanca": [33.6, -7.6],
|
|
490
|
+
"Africa/Lagos": [6.5, 3.4],
|
|
491
|
+
"Africa/Cairo": [30, 31.2],
|
|
492
|
+
"Africa/Nairobi": [-1.3, 36.8],
|
|
493
|
+
"Africa/Johannesburg": [-26.2, 28],
|
|
494
|
+
"Asia/Jerusalem": [31.8, 35.2],
|
|
495
|
+
"Asia/Dubai": [25.2, 55.3],
|
|
496
|
+
"Asia/Tehran": [35.7, 51.4],
|
|
497
|
+
"Asia/Riyadh": [24.7, 46.7],
|
|
498
|
+
"Asia/Baghdad": [33.3, 44.4],
|
|
499
|
+
// asia
|
|
500
|
+
"Asia/Karachi": [24.9, 67],
|
|
501
|
+
"Asia/Kolkata": [22.6, 88.4],
|
|
502
|
+
"Asia/Calcutta": [22.6, 88.4],
|
|
503
|
+
"Asia/Colombo": [6.9, 79.9],
|
|
504
|
+
"Asia/Dhaka": [23.8, 90.4],
|
|
505
|
+
"Asia/Bangkok": [13.8, 100.5],
|
|
506
|
+
"Asia/Jakarta": [-6.2, 106.8],
|
|
507
|
+
"Asia/Singapore": [1.35, 103.8],
|
|
508
|
+
"Asia/Kuala_Lumpur": [3.1, 101.7],
|
|
509
|
+
"Asia/Manila": [14.6, 121],
|
|
510
|
+
"Asia/Hong_Kong": [22.3, 114.2],
|
|
511
|
+
"Asia/Shanghai": [31.2, 121.5],
|
|
512
|
+
"Asia/Taipei": [25, 121.5],
|
|
513
|
+
"Asia/Seoul": [37.6, 127],
|
|
514
|
+
"Asia/Tokyo": [35.7, 139.7],
|
|
515
|
+
// oceania
|
|
516
|
+
"Australia/Perth": [-31.95, 115.9],
|
|
517
|
+
"Australia/Adelaide": [-34.9, 138.6],
|
|
518
|
+
"Australia/Brisbane": [-27.5, 153],
|
|
519
|
+
"Australia/Sydney": [-33.87, 151.2],
|
|
520
|
+
"Australia/Melbourne": [-37.8, 145],
|
|
521
|
+
"Pacific/Auckland": [-36.85, 174.76]
|
|
522
|
+
};
|
|
523
|
+
var REGION = {
|
|
524
|
+
America: [15, -90],
|
|
525
|
+
Europe: [50, 15],
|
|
526
|
+
Africa: [3, 22],
|
|
527
|
+
Asia: [35, 95],
|
|
528
|
+
Australia: [-25, 135],
|
|
529
|
+
Pacific: [-10, -160],
|
|
530
|
+
Atlantic: [35, -35],
|
|
531
|
+
Indian: [-15, 75],
|
|
532
|
+
Antarctica: [-80, 0]
|
|
533
|
+
};
|
|
534
|
+
function tzToCoords(tz) {
|
|
535
|
+
if (!tz) return null;
|
|
536
|
+
const exact = TZ[tz];
|
|
537
|
+
if (exact) return { lat: exact[0], lon: exact[1] };
|
|
538
|
+
const region = REGION[tz.split("/")[0]];
|
|
539
|
+
if (region) return { lat: region[0], lon: region[1] };
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
function tzCity(tz) {
|
|
543
|
+
if (!tz) return "";
|
|
544
|
+
const parts = tz.split("/");
|
|
545
|
+
return (parts[parts.length - 1] || tz).replace(/_/g, " ");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export {
|
|
549
|
+
vlog,
|
|
550
|
+
vlogSession,
|
|
551
|
+
voiceLogPath,
|
|
552
|
+
detectVoiceTools,
|
|
553
|
+
VoiceCapture,
|
|
554
|
+
VoicePlayback,
|
|
555
|
+
listMicDevices,
|
|
556
|
+
localTz,
|
|
557
|
+
tzToCoords,
|
|
558
|
+
tzCity
|
|
559
|
+
};
|