@gonzih/cc-discord 0.2.38 → 0.2.40
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/bot.js +8 -1
- package/dist/meta-agent-manager.d.ts +0 -15
- package/dist/meta-agent-manager.js +68 -2
- package/dist/notifier.js +0 -5
- package/dist/voice.d.ts +3 -2
- package/dist/voice.js +86 -42
- package/package.json +1 -1
package/dist/bot.js
CHANGED
|
@@ -911,8 +911,15 @@ export class CcDiscordBot {
|
|
|
911
911
|
this.lastActiveChannelId = channelId;
|
|
912
912
|
switch (interaction.commandName) {
|
|
913
913
|
case "reset": {
|
|
914
|
+
// Kill local DM session if any
|
|
914
915
|
this.killSession(channelId);
|
|
915
|
-
|
|
916
|
+
// Kill persistent meta-agent session if this is a routed channel.
|
|
917
|
+
// Unlike /clear, we do NOT delete the JSONL — context is preserved.
|
|
918
|
+
// Next message will respawn with --continue, picking up the same history.
|
|
919
|
+
const resetNs = this.resolveNamespaceForChannel(channelId);
|
|
920
|
+
this.metaAgentManager.killSession(resetNs);
|
|
921
|
+
await interaction.reply(`Session process killed for \`${resetNs}\`. Next message respawns with existing context intact.\n` +
|
|
922
|
+
`Use \`/clear\` to also wipe conversation history.`);
|
|
916
923
|
break;
|
|
917
924
|
}
|
|
918
925
|
case "costs": {
|
|
@@ -65,21 +65,6 @@ export interface MetaAgentManager {
|
|
|
65
65
|
/** Write a raw line to the stdin of the running session, if any. */
|
|
66
66
|
sendToSession: (ns: string, line: string) => void;
|
|
67
67
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Create a MetaAgentManager that maintains one persistent Claude process per namespace.
|
|
70
|
-
*
|
|
71
|
-
* On first message for a namespace:
|
|
72
|
-
* 1. ensureWorkspace + injectMcp
|
|
73
|
-
* 2. Drain any pending Redis queue entries to stdin
|
|
74
|
-
* 3. Write the new message to stdin
|
|
75
|
-
*
|
|
76
|
-
* On subsequent messages: write directly to stdin of the running process.
|
|
77
|
-
*
|
|
78
|
-
* On process exit: remove from sessions map. Next message triggers a respawn.
|
|
79
|
-
*
|
|
80
|
-
* The 3-second poll loop drains any messages that arrived while a session was
|
|
81
|
-
* starting up or temporarily unavailable.
|
|
82
|
-
*/
|
|
83
68
|
export declare function createMetaAgentManager(): MetaAgentManager;
|
|
84
69
|
/**
|
|
85
70
|
* Migrate the old cc-agent meta input key to the new cc-discord key.
|
|
@@ -293,7 +293,7 @@ export function spawnSession(ns, message, token, wire) {
|
|
|
293
293
|
* Stdout is wired to Redis via wireStdoutToRedis.
|
|
294
294
|
* Returns the ChildProcess.
|
|
295
295
|
*/
|
|
296
|
-
function spawnPersistentSession(ns, token, wire, onExit) {
|
|
296
|
+
function spawnPersistentSession(ns, token, wire, onExit, onOutput) {
|
|
297
297
|
const wsPath = workspacePath(ns);
|
|
298
298
|
const claudeBin = resolveClaude();
|
|
299
299
|
const env = buildEnv(token);
|
|
@@ -312,6 +312,9 @@ function spawnPersistentSession(ns, token, wire, onExit) {
|
|
|
312
312
|
], { cwd: wsPath, env, stdio: ["pipe", "pipe", "pipe"] });
|
|
313
313
|
proc.stdin.setDefaultEncoding("utf8");
|
|
314
314
|
wireStdoutToRedis(proc, ns, wire);
|
|
315
|
+
if (onOutput) {
|
|
316
|
+
proc.stdout.on("data", onOutput);
|
|
317
|
+
}
|
|
315
318
|
proc.on("exit", (code) => {
|
|
316
319
|
console.log(`[meta-agent-manager] persistent session exited (ns=${ns}, code=${code})`);
|
|
317
320
|
onExit();
|
|
@@ -337,15 +340,22 @@ function spawnPersistentSession(ns, token, wire, onExit) {
|
|
|
337
340
|
* The 3-second poll loop drains any messages that arrived while a session was
|
|
338
341
|
* starting up or temporarily unavailable.
|
|
339
342
|
*/
|
|
343
|
+
/** Kill a session after this many ms of no stdout (or no new stdin). */
|
|
344
|
+
const SESSION_INACTIVITY_MS = 5 * 60_000; // 5 minutes
|
|
340
345
|
export function createMetaAgentManager() {
|
|
341
346
|
let pollInterval = null;
|
|
347
|
+
let watchdogInterval = null;
|
|
342
348
|
/** One persistent ChildProcess per namespace. */
|
|
343
349
|
const sessions = new Map();
|
|
344
350
|
/** Namespaces currently being set up (workspace clone / first spawn). */
|
|
345
351
|
const startingUp = new Set();
|
|
352
|
+
/** Wire reference needed for watchdog notifications. Set in startPolling. */
|
|
353
|
+
let wireRef = null;
|
|
346
354
|
/**
|
|
347
355
|
* Write a line to the stdin of the persistent session for ns.
|
|
348
356
|
* Silently no-ops if no session exists.
|
|
357
|
+
* Resets the inactivity timer so the watchdog doesn't kill a session
|
|
358
|
+
* that just received a message but hasn't responded yet.
|
|
349
359
|
*/
|
|
350
360
|
function writeToStdin(ns, line) {
|
|
351
361
|
const session = sessions.get(ns);
|
|
@@ -354,11 +364,54 @@ export function createMetaAgentManager() {
|
|
|
354
364
|
try {
|
|
355
365
|
// Interactive mode: plain text line, Claude reads it as user input
|
|
356
366
|
session.proc.stdin.write(`${line}\n`);
|
|
367
|
+
// Reset inactivity timer — the session now has SESSION_INACTIVITY_MS to respond
|
|
368
|
+
session.lastOutputAt = Date.now();
|
|
357
369
|
}
|
|
358
370
|
catch (err) {
|
|
359
371
|
console.warn(`[meta-agent-manager] stdin write failed (ns=${ns}):`, err.message);
|
|
360
372
|
}
|
|
361
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Watchdog: scan all sessions every 60s. If any session has been silent
|
|
376
|
+
* (no stdout and no new stdin) for SESSION_INACTIVITY_MS, kill it and
|
|
377
|
+
* notify Discord so the user can see why the session went quiet.
|
|
378
|
+
*/
|
|
379
|
+
function startWatchdog() {
|
|
380
|
+
if (watchdogInterval)
|
|
381
|
+
return;
|
|
382
|
+
watchdogInterval = setInterval(() => {
|
|
383
|
+
if (!wireRef)
|
|
384
|
+
return;
|
|
385
|
+
const now = Date.now();
|
|
386
|
+
for (const [ns, session] of sessions) {
|
|
387
|
+
const idleMs = now - session.lastOutputAt;
|
|
388
|
+
if (idleMs < SESSION_INACTIVITY_MS)
|
|
389
|
+
continue;
|
|
390
|
+
const idleMin = Math.round(idleMs / 60_000);
|
|
391
|
+
console.warn(`[meta-agent-manager] watchdog: ns=${ns} idle ${idleMin}m, killing stuck session`);
|
|
392
|
+
// Notify Discord before killing so the user knows what happened
|
|
393
|
+
const alertMsg = {
|
|
394
|
+
id: crypto.randomUUID(),
|
|
395
|
+
source: "claude",
|
|
396
|
+
role: "assistant",
|
|
397
|
+
content: `⚠️ Session for **${ns}** was idle for ${idleMin} minutes (likely stuck on a tool call). Killed and removed — send any message to restart.`,
|
|
398
|
+
timestamp: new Date().toISOString(),
|
|
399
|
+
chatId: 0,
|
|
400
|
+
};
|
|
401
|
+
wireRef.discord.publishOutgoing(ns, alertMsg).catch((err) => {
|
|
402
|
+
console.warn(`[meta-agent-manager] watchdog publishOutgoing failed (ns=${ns}):`, err.message);
|
|
403
|
+
});
|
|
404
|
+
try {
|
|
405
|
+
session.proc.stdin.end();
|
|
406
|
+
session.proc.kill("SIGTERM");
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
// ignore kill errors
|
|
410
|
+
}
|
|
411
|
+
sessions.delete(ns);
|
|
412
|
+
}
|
|
413
|
+
}, 60_000);
|
|
414
|
+
}
|
|
362
415
|
/**
|
|
363
416
|
* Drain all pending messages from the Redis input queue into the session's stdin.
|
|
364
417
|
*/
|
|
@@ -405,12 +458,19 @@ export function createMetaAgentManager() {
|
|
|
405
458
|
const wsPath = workspacePath(ns);
|
|
406
459
|
await ensureWorkspace(ns, repoUrl);
|
|
407
460
|
injectMcp(ns, wsPath, token);
|
|
461
|
+
// Placeholder — filled in after proc is known so the onOutput closure can update it
|
|
462
|
+
let session;
|
|
408
463
|
const proc = spawnPersistentSession(ns, token, wire, () => {
|
|
409
464
|
// On exit: remove from map so next message triggers a respawn
|
|
410
465
|
sessions.delete(ns);
|
|
411
466
|
console.log(`[meta-agent-manager] session removed from map (ns=${ns})`);
|
|
467
|
+
}, () => {
|
|
468
|
+
// Reset inactivity timer on any stdout data
|
|
469
|
+
if (session)
|
|
470
|
+
session.lastOutputAt = Date.now();
|
|
412
471
|
});
|
|
413
|
-
|
|
472
|
+
session = { proc, ns, lastOutputAt: Date.now() };
|
|
473
|
+
sessions.set(ns, session);
|
|
414
474
|
// Drain any messages that arrived before this session started
|
|
415
475
|
await drainQueue(ns, wire);
|
|
416
476
|
// Write the triggering message last (after queue drain, in order)
|
|
@@ -443,6 +503,8 @@ export function createMetaAgentManager() {
|
|
|
443
503
|
startPolling(wire, getNamespaces, instanceId) {
|
|
444
504
|
if (pollInterval)
|
|
445
505
|
return; // already running
|
|
506
|
+
wireRef = wire;
|
|
507
|
+
startWatchdog();
|
|
446
508
|
pollInterval = setInterval(() => {
|
|
447
509
|
const namespaces = getNamespaces();
|
|
448
510
|
if (namespaces.length === 0)
|
|
@@ -499,6 +561,10 @@ export function createMetaAgentManager() {
|
|
|
499
561
|
pollInterval = null;
|
|
500
562
|
console.log("[meta-agent-manager] polling stopped");
|
|
501
563
|
}
|
|
564
|
+
if (watchdogInterval) {
|
|
565
|
+
clearInterval(watchdogInterval);
|
|
566
|
+
watchdogInterval = null;
|
|
567
|
+
}
|
|
502
568
|
// Kill all active sessions
|
|
503
569
|
for (const [ns, session] of sessions) {
|
|
504
570
|
try {
|
package/dist/notifier.js
CHANGED
|
@@ -279,11 +279,6 @@ export function startNotifier(bot, notifyChannelId, namespace, redis, handleUser
|
|
|
279
279
|
if (!buf || !buf.text.trim())
|
|
280
280
|
return;
|
|
281
281
|
const text = `← [${ns}] ` + stripAnsi(buf.text.trim());
|
|
282
|
-
if (text.length < 30) {
|
|
283
|
-
buf.text = "";
|
|
284
|
-
buf.timer = null;
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
282
|
buf.text = "";
|
|
288
283
|
buf.timer = null;
|
|
289
284
|
// During an active loop, route meta-agent output to the thread rather than main channel
|
package/dist/voice.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Voice message transcription via whisper.cpp.
|
|
3
|
-
* Flow:
|
|
3
|
+
* Flow: Discord voice memo (OGG/M4A) → ffmpeg convert to 16kHz WAV → whisper-cpp → text
|
|
4
4
|
*/
|
|
5
5
|
/**
|
|
6
|
-
* Transcribe a voice message from a
|
|
6
|
+
* Transcribe a voice message from a Discord CDN URL.
|
|
7
|
+
* Accepts OGG, M4A, MP3, or any audio format ffmpeg can decode.
|
|
7
8
|
* Returns the transcribed text, or throws if whisper/ffmpeg not available.
|
|
8
9
|
*/
|
|
9
10
|
export declare function transcribeVoice(fileUrl: string): Promise<string>;
|
package/dist/voice.js
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Voice message transcription via whisper.cpp.
|
|
3
|
-
* Flow:
|
|
3
|
+
* Flow: Discord voice memo (OGG/M4A) → ffmpeg convert to 16kHz WAV → whisper-cpp → text
|
|
4
4
|
*/
|
|
5
5
|
import { execFile } from "child_process";
|
|
6
6
|
import { promisify } from "util";
|
|
7
|
-
import { existsSync } from "fs";
|
|
8
|
-
import { unlink, readFile } from "fs/promises";
|
|
7
|
+
import { existsSync, createWriteStream } from "fs";
|
|
8
|
+
import { unlink, readFile, access } from "fs/promises";
|
|
9
9
|
import { tmpdir } from "os";
|
|
10
10
|
import { join } from "path";
|
|
11
11
|
import https from "https";
|
|
12
12
|
import http from "http";
|
|
13
|
-
import { createWriteStream } from "fs";
|
|
14
13
|
const execFileAsync = promisify(execFile);
|
|
15
|
-
//
|
|
16
|
-
// Falls back to base.en if small not found
|
|
14
|
+
// Env var overrides allow test injection: WHISPER_BIN, FFMPEG_BIN, WHISPER_MODEL
|
|
17
15
|
const WHISPER_MODELS = [
|
|
16
|
+
process.env.WHISPER_MODEL,
|
|
18
17
|
"/opt/homebrew/share/whisper-cpp/ggml-small.en.bin",
|
|
19
18
|
"/opt/homebrew/share/whisper-cpp/ggml-small.bin",
|
|
20
19
|
"/opt/homebrew/share/whisper-cpp/ggml-base.en.bin",
|
|
21
20
|
"/opt/homebrew/share/whisper-cpp/ggml-base.bin",
|
|
22
|
-
// user-local
|
|
23
21
|
`${process.env.HOME}/.local/share/whisper-cpp/ggml-small.en.bin`,
|
|
24
22
|
`${process.env.HOME}/.local/share/whisper-cpp/ggml-base.en.bin`,
|
|
25
|
-
];
|
|
23
|
+
].filter(Boolean);
|
|
26
24
|
const WHISPER_BIN_CANDIDATES = [
|
|
27
|
-
|
|
25
|
+
process.env.WHISPER_BIN,
|
|
26
|
+
"/opt/homebrew/bin/whisper-cli",
|
|
28
27
|
"/opt/homebrew/bin/whisper-cpp",
|
|
29
28
|
"/usr/local/bin/whisper-cli",
|
|
30
29
|
"/usr/local/bin/whisper-cpp",
|
|
31
30
|
"/opt/homebrew/bin/whisper",
|
|
32
|
-
];
|
|
31
|
+
].filter(Boolean);
|
|
33
32
|
const FFMPEG_CANDIDATES = [
|
|
33
|
+
process.env.FFMPEG_BIN,
|
|
34
34
|
"/opt/homebrew/bin/ffmpeg",
|
|
35
35
|
"/usr/local/bin/ffmpeg",
|
|
36
36
|
"/usr/bin/ffmpeg",
|
|
37
|
-
];
|
|
37
|
+
].filter(Boolean);
|
|
38
38
|
function findBin(candidates) {
|
|
39
39
|
for (const p of candidates) {
|
|
40
40
|
if (existsSync(p))
|
|
@@ -49,15 +49,34 @@ function findModel() {
|
|
|
49
49
|
}
|
|
50
50
|
return null;
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Download a file from a URL to dest.
|
|
54
|
+
* Supports file:// (copies local file), and https/http with redirect following.
|
|
55
|
+
*/
|
|
56
|
+
async function downloadFile(url, dest, redirectsLeft = 5) {
|
|
57
|
+
if (url.startsWith("file://")) {
|
|
58
|
+
const { copyFile } = await import("fs/promises");
|
|
59
|
+
await copyFile(url.slice("file://".length), dest);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
53
62
|
return new Promise((resolve, reject) => {
|
|
54
|
-
|
|
63
|
+
if (redirectsLeft <= 0) {
|
|
64
|
+
reject(new Error(`Too many redirects downloading ${url}`));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
55
67
|
const getter = url.startsWith("https") ? https : http;
|
|
56
68
|
getter.get(url, (res) => {
|
|
69
|
+
if (res.statusCode && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
70
|
+
res.resume();
|
|
71
|
+
downloadFile(res.headers.location, dest, redirectsLeft - 1).then(resolve).catch(reject);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
57
74
|
if (res.statusCode !== 200) {
|
|
75
|
+
res.resume();
|
|
58
76
|
reject(new Error(`HTTP ${res.statusCode} downloading ${url}`));
|
|
59
77
|
return;
|
|
60
78
|
}
|
|
79
|
+
const file = createWriteStream(dest);
|
|
61
80
|
res.pipe(file);
|
|
62
81
|
file.on("finish", () => file.close(() => resolve()));
|
|
63
82
|
file.on("error", reject);
|
|
@@ -65,7 +84,8 @@ function downloadFile(url, dest) {
|
|
|
65
84
|
});
|
|
66
85
|
}
|
|
67
86
|
/**
|
|
68
|
-
* Transcribe a voice message from a
|
|
87
|
+
* Transcribe a voice message from a Discord CDN URL.
|
|
88
|
+
* Accepts OGG, M4A, MP3, or any audio format ffmpeg can decode.
|
|
69
89
|
* Returns the transcribed text, or throws if whisper/ffmpeg not available.
|
|
70
90
|
*/
|
|
71
91
|
export async function transcribeVoice(fileUrl) {
|
|
@@ -78,58 +98,82 @@ export async function transcribeVoice(fileUrl) {
|
|
|
78
98
|
const model = findModel();
|
|
79
99
|
if (!model)
|
|
80
100
|
throw new Error("No whisper model found — run: whisper-cpp-download-ggml-model small.en");
|
|
81
|
-
const tmp = join(tmpdir(), `cc-
|
|
82
|
-
|
|
101
|
+
const tmp = join(tmpdir(), `cc-discord-voice-${Date.now()}`);
|
|
102
|
+
// Preserve original extension so ffmpeg auto-detects format
|
|
103
|
+
const urlPath = new URL(fileUrl).pathname;
|
|
104
|
+
const ext = urlPath.includes(".") ? "." + urlPath.split(".").pop().split("?")[0] : ".ogg";
|
|
105
|
+
const audioPath = `${tmp}${ext}`;
|
|
83
106
|
const wavPath = `${tmp}.wav`;
|
|
84
107
|
try {
|
|
85
|
-
// 1. Download
|
|
86
|
-
await downloadFile(fileUrl,
|
|
87
|
-
// 2. Convert
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
108
|
+
// 1. Download audio from Discord CDN (follows redirects)
|
|
109
|
+
await downloadFile(fileUrl, audioPath);
|
|
110
|
+
// 2. Convert to 16kHz mono WAV (whisper requirement)
|
|
111
|
+
try {
|
|
112
|
+
await execFileAsync(ffmpegBin, [
|
|
113
|
+
"-y", "-i", audioPath,
|
|
114
|
+
"-ar", "16000",
|
|
115
|
+
"-ac", "1",
|
|
116
|
+
"-c:a", "pcm_s16le",
|
|
117
|
+
wavPath,
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
122
|
+
throw new Error(`ffmpeg conversion failed: ${msg}`);
|
|
123
|
+
}
|
|
95
124
|
// 3. Run whisper-cpp
|
|
96
|
-
//
|
|
97
|
-
// -l auto fails with .en models — detect and use -l en instead
|
|
125
|
+
// -l auto fails with .en models — use -l en for those
|
|
98
126
|
const isEnModel = model.includes(".en.");
|
|
99
127
|
const langArgs = isEnModel ? ["-l", "en"] : ["-l", "auto"];
|
|
128
|
+
let whisperStderr = "";
|
|
100
129
|
try {
|
|
101
|
-
await execFileAsync(whisperBin, [
|
|
130
|
+
const result = await execFileAsync(whisperBin, [
|
|
102
131
|
"-m", model,
|
|
103
132
|
"-f", wavPath,
|
|
104
133
|
"--no-timestamps",
|
|
105
134
|
...langArgs,
|
|
106
|
-
"--output-txt",
|
|
135
|
+
"--output-txt",
|
|
107
136
|
]);
|
|
137
|
+
whisperStderr = result.stderr ?? "";
|
|
108
138
|
}
|
|
109
139
|
catch (err) {
|
|
110
|
-
const
|
|
111
|
-
|
|
140
|
+
const e = err;
|
|
141
|
+
whisperStderr = e.stderr ?? "";
|
|
142
|
+
const msg = e.message ?? String(err);
|
|
143
|
+
throw new Error(`whisper-cpp failed: ${msg}\nstderr: ${whisperStderr}`);
|
|
112
144
|
}
|
|
113
|
-
//
|
|
114
|
-
|
|
145
|
+
// 4. Find output file — whisper-cpp versions differ on suffix:
|
|
146
|
+
// some write {wavPath}.txt, others write {baseName}.txt (strip .wav)
|
|
147
|
+
const txtPathFull = `${wavPath}.txt`; // /tmp/foo.wav.txt
|
|
148
|
+
const txtPathStripped = wavPath.replace(/\.wav$/, ".txt"); // /tmp/foo.txt
|
|
115
149
|
let raw = "";
|
|
116
|
-
|
|
117
|
-
|
|
150
|
+
for (const candidate of [txtPathFull, txtPathStripped]) {
|
|
151
|
+
try {
|
|
152
|
+
await access(candidate);
|
|
153
|
+
raw = await readFile(candidate, "utf-8");
|
|
154
|
+
await unlink(candidate).catch(() => { });
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// try next
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!raw && !whisperStderr) {
|
|
162
|
+
throw new Error("whisper-cpp ran but produced no output file (tried .wav.txt and .txt)");
|
|
118
163
|
}
|
|
119
|
-
|
|
120
|
-
|
|
164
|
+
// Use stdout text if file wasn't found but whisper printed to stderr (some versions do)
|
|
165
|
+
if (!raw) {
|
|
166
|
+
raw = whisperStderr;
|
|
121
167
|
}
|
|
122
168
|
const text = raw
|
|
123
169
|
.replace(/\[BLANK_AUDIO\]/gi, "")
|
|
124
|
-
.replace(/\[.*?\]/g, "")
|
|
170
|
+
.replace(/\[.*?\]/g, "")
|
|
125
171
|
.trim();
|
|
126
172
|
return text || "[empty transcription]";
|
|
127
173
|
}
|
|
128
174
|
finally {
|
|
129
|
-
|
|
130
|
-
await unlink(oggPath).catch(() => { });
|
|
175
|
+
await unlink(audioPath).catch(() => { });
|
|
131
176
|
await unlink(wavPath).catch(() => { });
|
|
132
|
-
await unlink(`${wavPath}.txt`).catch(() => { });
|
|
133
177
|
}
|
|
134
178
|
}
|
|
135
179
|
/**
|