@inetafrica/open-claudia 2.6.43 → 2.6.45
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/bot-agent.js +4 -20
- package/channels/telegram/adapter.js +47 -15
- package/core/config.js +4 -0
- package/core/media.js +48 -5
- package/core/runner.js +3 -3
- package/package.json +1 -1
package/bot-agent.js
CHANGED
|
@@ -767,25 +767,9 @@ function transcribeAudio(oggPath) {
|
|
|
767
767
|
}
|
|
768
768
|
|
|
769
769
|
// ── Text-to-Speech ────────────────────────────────────────────────
|
|
770
|
+
// Shared with direct mode: ElevenLabs natural voice, falling back to `say`.
|
|
770
771
|
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
function textToVoice(text) {
|
|
774
|
-
if (!TTS_CMD || !FFMPEG) return null;
|
|
775
|
-
try {
|
|
776
|
-
const clean = text.replace(/[*_`#>\[\]()]/g, "").replace(/\n{2,}/g, ". ").replace(/\n/g, " ").trim();
|
|
777
|
-
if (!clean) return null;
|
|
778
|
-
const aiffPath = path.join(TEMP_DIR, `tts-${Date.now()}.aiff`);
|
|
779
|
-
const oggPath = aiffPath.replace(".aiff", ".ogg");
|
|
780
|
-
execSync(`${TTS_CMD} ${JSON.stringify(clean)} -o "${aiffPath}"`, { timeout: 30000 });
|
|
781
|
-
execSync(`"${FFMPEG}" -i "${aiffPath}" -c:a libopus -y "${oggPath}" 2>/dev/null`, { timeout: 30000 });
|
|
782
|
-
try { fs.unlinkSync(aiffPath); } catch (e) {}
|
|
783
|
-
return oggPath;
|
|
784
|
-
} catch (e) {
|
|
785
|
-
console.error("TTS error:", e.message);
|
|
786
|
-
return null;
|
|
787
|
-
}
|
|
788
|
-
}
|
|
772
|
+
const { textToVoice } = require("./core/media");
|
|
789
773
|
|
|
790
774
|
async function sendVoice(oggPath) {
|
|
791
775
|
try {
|
|
@@ -1421,9 +1405,9 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1421
1405
|
if (code !== 0 && code !== null) await send(`Exit code: ${code}`);
|
|
1422
1406
|
|
|
1423
1407
|
// Send voice reply if input was a voice note
|
|
1424
|
-
if (lastInputWasVoice
|
|
1408
|
+
if (lastInputWasVoice) {
|
|
1425
1409
|
lastInputWasVoice = false;
|
|
1426
|
-
const voicePath = textToVoice(finalText);
|
|
1410
|
+
const voicePath = await textToVoice(finalText);
|
|
1427
1411
|
if (voicePath) await sendVoice(voicePath);
|
|
1428
1412
|
}
|
|
1429
1413
|
} catch (e) {
|
|
@@ -19,11 +19,16 @@ class TelegramAdapter {
|
|
|
19
19
|
this.token = token;
|
|
20
20
|
this.ownerChatId = ownerChatId;
|
|
21
21
|
this.chatIds = chatIds || [];
|
|
22
|
+
// Own the HTTP(S) agent so we can drop dead keep-alive sockets on reconnect.
|
|
23
|
+
this._agent = new https.Agent({ keepAlive: true });
|
|
22
24
|
this.bot = new TelegramBot(token, {
|
|
23
25
|
polling: { autoStart: false, params: { timeout: 30 } },
|
|
26
|
+
request: { agent: this._agent },
|
|
24
27
|
});
|
|
25
28
|
this._listeners = { message: new Set(), action: new Set() };
|
|
26
29
|
this._reconnectTimer = null;
|
|
30
|
+
this._recoveryTimer = null;
|
|
31
|
+
this._reconnectAttempts = 0;
|
|
27
32
|
this._wireInbound();
|
|
28
33
|
}
|
|
29
34
|
|
|
@@ -45,9 +50,49 @@ class TelegramAdapter {
|
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
async stop() {
|
|
53
|
+
if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null; }
|
|
54
|
+
if (this._recoveryTimer) { clearTimeout(this._recoveryTimer); this._recoveryTimer = null; }
|
|
48
55
|
try { await this.bot.stopPolling(); } catch (e) {}
|
|
49
56
|
}
|
|
50
57
|
|
|
58
|
+
// Recover from a wedged poll loop. The library reuses one keep-alive socket;
|
|
59
|
+
// after a sleep/wake or network blip that socket dies and every poll then
|
|
60
|
+
// ETIMEDOUTs forever — restarting polling alone reuses the same dead socket.
|
|
61
|
+
// So we destroy the agent (forcing a fresh connection), back off, and if we
|
|
62
|
+
// still can't recover after several tries we exit so launchd (KeepAlive)
|
|
63
|
+
// relaunches a fully clean process.
|
|
64
|
+
_scheduleReconnect() {
|
|
65
|
+
if (this._reconnectTimer) return; // one cycle in flight; swallow the flood
|
|
66
|
+
if (this._recoveryTimer) { clearTimeout(this._recoveryTimer); this._recoveryTimer = null; }
|
|
67
|
+
this._reconnectAttempts += 1;
|
|
68
|
+
const MAX = 6;
|
|
69
|
+
if (this._reconnectAttempts > MAX) {
|
|
70
|
+
console.error(`Polling unrecoverable after ${MAX} attempts — exiting for supervisor restart.`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
const delay = Math.min(30000, 1000 * 2 ** this._reconnectAttempts); // 2s,4s,8s,16s,30s,30s
|
|
74
|
+
console.log(`Network lost. Reconnecting in ${Math.round(delay / 1000)}s (attempt ${this._reconnectAttempts}/${MAX})...`);
|
|
75
|
+
this._reconnectTimer = setTimeout(async () => {
|
|
76
|
+
this._reconnectTimer = null;
|
|
77
|
+
try {
|
|
78
|
+
try { await this.bot.stopPolling(); } catch (e) {}
|
|
79
|
+
try { this._agent.destroy(); } catch (e) {} // drop the dead pooled socket
|
|
80
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
81
|
+
await this.bot.startPolling();
|
|
82
|
+
console.log("Polling restarted.");
|
|
83
|
+
// No fresh error within 30s ⇒ genuinely recovered; reset the counter.
|
|
84
|
+
this._recoveryTimer = setTimeout(() => {
|
|
85
|
+
this._recoveryTimer = null;
|
|
86
|
+
if (this._reconnectAttempts) console.log("Connection healthy again.");
|
|
87
|
+
this._reconnectAttempts = 0;
|
|
88
|
+
}, 30000);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
console.error("Reconnect attempt failed:", e.message);
|
|
91
|
+
this._scheduleReconnect(); // back off and retry; don't die on first miss
|
|
92
|
+
}
|
|
93
|
+
}, delay);
|
|
94
|
+
}
|
|
95
|
+
|
|
51
96
|
_wireInbound() {
|
|
52
97
|
this.bot.on("polling_error", (err) => {
|
|
53
98
|
const msg = err.message || "";
|
|
@@ -56,21 +101,8 @@ class TelegramAdapter {
|
|
|
56
101
|
console.error("Another instance is polling. Exiting.");
|
|
57
102
|
process.exit(1);
|
|
58
103
|
}
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
console.log("Network lost. Reconnecting in 10s...");
|
|
62
|
-
this._reconnectTimer = setTimeout(async () => {
|
|
63
|
-
this._reconnectTimer = null;
|
|
64
|
-
try {
|
|
65
|
-
await this.bot.stopPolling();
|
|
66
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
67
|
-
await this.bot.startPolling();
|
|
68
|
-
console.log("Reconnected.");
|
|
69
|
-
} catch (e) {
|
|
70
|
-
console.error("Reconnect failed:", e.message);
|
|
71
|
-
process.exit(1);
|
|
72
|
-
}
|
|
73
|
-
}, 10000);
|
|
104
|
+
if (/ETIMEDOUT|ECONNRESET|ENOTFOUND|ENETUNREACH|EAI_AGAIN|EFATAL|socket hang up/i.test(msg)) {
|
|
105
|
+
this._scheduleReconnect();
|
|
74
106
|
}
|
|
75
107
|
});
|
|
76
108
|
|
package/core/config.js
CHANGED
|
@@ -99,6 +99,9 @@ const TRANSCRIPTS_DIR = config.TRANSCRIPTS_DIR || process.env.TRANSCRIPTS_DIR ||
|
|
|
99
99
|
const WHISPER_CLI = config.WHISPER_CLI || "";
|
|
100
100
|
const WHISPER_MODEL = config.WHISPER_MODEL || "";
|
|
101
101
|
const FFMPEG = config.FFMPEG || "";
|
|
102
|
+
const ELEVENLABS_API_KEY = config.ELEVENLABS_API_KEY || process.env.ELEVENLABS_API_KEY || "";
|
|
103
|
+
const ELEVENLABS_VOICE_ID = config.ELEVENLABS_VOICE_ID || process.env.ELEVENLABS_VOICE_ID || "EXAVITQu4vr4xnSDxMaL";
|
|
104
|
+
const ELEVENLABS_MODEL = config.ELEVENLABS_MODEL || process.env.ELEVENLABS_MODEL || "eleven_v3";
|
|
102
105
|
const SOUL_FILE = config.SOUL_FILE || path.join(CONFIG_DIR, "soul.md");
|
|
103
106
|
const CRONS_FILE = config.CRONS_FILE || path.join(CONFIG_DIR, "crons.json");
|
|
104
107
|
const JOBS_FILE = config.JOBS_FILE || path.join(CONFIG_DIR, "jobs.json");
|
|
@@ -233,6 +236,7 @@ module.exports = {
|
|
|
233
236
|
TRANSCRIPT_MAX_ENTRY_CHARS,
|
|
234
237
|
TRANSCRIPTS_DIR,
|
|
235
238
|
WHISPER_CLI, WHISPER_MODEL, FFMPEG,
|
|
239
|
+
ELEVENLABS_API_KEY, ELEVENLABS_VOICE_ID, ELEVENLABS_MODEL,
|
|
236
240
|
SOUL_FILE, CRONS_FILE, JOBS_FILE, TASKS_DIR, VAULT_FILE, AUTH_FILE, IDENTITIES_FILE,
|
|
237
241
|
PEOPLE_FILE, INTROS_FILE, AUDIT_FILE,
|
|
238
242
|
STATE_FILE, SESSIONS_FILE,
|
package/core/media.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
const fs = require("fs");
|
|
5
5
|
const path = require("path");
|
|
6
6
|
const { execSync } = require("child_process");
|
|
7
|
-
const { WHISPER_CLI, WHISPER_MODEL, FFMPEG, TEMP_DIR } = require("./config");
|
|
7
|
+
const { WHISPER_CLI, WHISPER_MODEL, FFMPEG, TEMP_DIR, ELEVENLABS_API_KEY, ELEVENLABS_VOICE_ID, ELEVENLABS_MODEL } = require("./config");
|
|
8
8
|
|
|
9
9
|
const TTS_CMD = process.platform === "darwin" ? "say" : null;
|
|
10
10
|
|
|
@@ -19,11 +19,14 @@ function transcribeAudio(oggPath) {
|
|
|
19
19
|
.join(" ").trim();
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
function
|
|
22
|
+
function cleanForTTS(text) {
|
|
23
|
+
return text.replace(/[*_`#>\[\]()]/g, "").replace(/\n{2,}/g, ". ").replace(/\n/g, " ").trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// macOS `say` fallback. Synchronous. Returns ogg path or null.
|
|
27
|
+
function sayToVoice(clean) {
|
|
23
28
|
if (!TTS_CMD || !FFMPEG) return null;
|
|
24
29
|
try {
|
|
25
|
-
const clean = text.replace(/[*_`#>\[\]()]/g, "").replace(/\n{2,}/g, ". ").replace(/\n/g, " ").trim();
|
|
26
|
-
if (!clean) return null;
|
|
27
30
|
const aiffPath = path.join(TEMP_DIR, `tts-${Date.now()}.aiff`);
|
|
28
31
|
const oggPath = aiffPath.replace(".aiff", ".ogg");
|
|
29
32
|
execSync(`${TTS_CMD} ${JSON.stringify(clean)} -o "${aiffPath}"`, { timeout: 30000 });
|
|
@@ -31,9 +34,49 @@ function textToVoice(text) {
|
|
|
31
34
|
try { fs.unlinkSync(aiffPath); } catch (e) {}
|
|
32
35
|
return oggPath;
|
|
33
36
|
} catch (e) {
|
|
34
|
-
console.error("TTS error:", e.message);
|
|
37
|
+
console.error("say TTS error:", e.message);
|
|
35
38
|
return null;
|
|
36
39
|
}
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
// Natural TTS via ElevenLabs. Returns ogg path or null on any failure.
|
|
43
|
+
async function elevenLabsToVoice(clean) {
|
|
44
|
+
if (!ELEVENLABS_API_KEY || !FFMPEG) return null;
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${ELEVENLABS_VOICE_ID}`, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: { "xi-api-key": ELEVENLABS_API_KEY, "Content-Type": "application/json" },
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
text: clean,
|
|
51
|
+
model_id: ELEVENLABS_MODEL,
|
|
52
|
+
voice_settings: { stability: 0.5, similarity_boost: 0.85, style: 0.5, use_speaker_boost: true },
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
const body = await res.text().catch(() => "");
|
|
57
|
+
console.error(`ElevenLabs TTS failed: ${res.status} ${body}`.slice(0, 300));
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
61
|
+
const mp3Path = path.join(TEMP_DIR, `tts-${Date.now()}.mp3`);
|
|
62
|
+
const oggPath = mp3Path.replace(".mp3", ".ogg");
|
|
63
|
+
fs.writeFileSync(mp3Path, buf);
|
|
64
|
+
execSync(`"${FFMPEG}" -i "${mp3Path}" -c:a libopus -y "${oggPath}" 2>/dev/null`, { timeout: 30000 });
|
|
65
|
+
try { fs.unlinkSync(mp3Path); } catch (e) {}
|
|
66
|
+
return oggPath;
|
|
67
|
+
} catch (e) {
|
|
68
|
+
console.error("ElevenLabs TTS error:", e.message);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Natural voice via ElevenLabs, falling back to macOS `say` only on no-key/error.
|
|
74
|
+
async function textToVoice(text) {
|
|
75
|
+
const clean = cleanForTTS(text);
|
|
76
|
+
if (!clean) return null;
|
|
77
|
+
const eleven = await elevenLabsToVoice(clean);
|
|
78
|
+
if (eleven) return eleven;
|
|
79
|
+
return sayToVoice(clean);
|
|
80
|
+
}
|
|
81
|
+
|
|
39
82
|
module.exports = { transcribeAudio, textToVoice, TTS_CMD };
|
package/core/runner.js
CHANGED
|
@@ -16,7 +16,7 @@ const { chatContext, currentChannelId, currentAdapter } = require("./context");
|
|
|
16
16
|
const { buildSystemPrompt, promptWithDynamicContext } = require("./system-prompt");
|
|
17
17
|
const { redactSensitive } = require("./redact");
|
|
18
18
|
const { send, editMessage, sendVoice, splitMessage } = require("./io");
|
|
19
|
-
const { textToVoice
|
|
19
|
+
const { textToVoice } = require("./media");
|
|
20
20
|
const { killProcessTree } = require("./process-tree");
|
|
21
21
|
const {
|
|
22
22
|
appendProjectTranscript, transcriptProjectInfo,
|
|
@@ -1193,9 +1193,9 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1193
1193
|
}
|
|
1194
1194
|
if (code !== 0 && code !== null) await send(`Exit code: ${code}`);
|
|
1195
1195
|
|
|
1196
|
-
if (state.lastInputWasVoice
|
|
1196
|
+
if (state.lastInputWasVoice) {
|
|
1197
1197
|
state.lastInputWasVoice = false;
|
|
1198
|
-
const voicePath = textToVoice(finalText);
|
|
1198
|
+
const voicePath = await textToVoice(finalText);
|
|
1199
1199
|
if (voicePath) await sendVoice(voicePath);
|
|
1200
1200
|
}
|
|
1201
1201
|
} catch (e) {
|
package/package.json
CHANGED