alive-ai 0.1.7 → 0.1.9
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/brain/memory/fact_extractor.py +13 -9
- package/brain/memory/manager.py +3 -2
- package/brain/memory/openmind.py +4 -3
- package/brain/memory/summarizer.py +8 -5
- package/cli/index.js +67 -0
- package/cli/tui.js +35 -16
- package/core/message_handler.py +6 -1
- package/input/telegram/listener.py +21 -6
- package/output/voice/gtts_tts.py +17 -4
- package/package.json +1 -1
- package/pyproject.toml +1 -1
|
@@ -3,22 +3,25 @@ import json
|
|
|
3
3
|
import re
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
def build_extract_prompt(agent_name: str) -> str:
|
|
7
|
+
agent = agent_name or "the AI"
|
|
8
|
+
return f"""You are analyzing a conversation between {agent} (AI) and a HUMAN USER.
|
|
9
|
+
Extract facts about THE HUMAN USER ONLY - nothing about {agent}.
|
|
8
10
|
|
|
9
|
-
Look at what the HUMAN says about THEMSELF and their relationship with
|
|
11
|
+
Look at what the HUMAN says about THEMSELF and their relationship with {agent}. Extract:
|
|
10
12
|
- name, nickname, age, gender, job, location
|
|
11
13
|
- hobbies, interests, favorite things
|
|
12
14
|
- personality traits, communication style
|
|
13
|
-
- relationship to
|
|
15
|
+
- relationship to {agent} (creator, boyfriend, etc.)
|
|
14
16
|
- pet names they use (daddy, baby, etc.)
|
|
15
17
|
- intimacy preferences, preferences mentioned
|
|
16
|
-
- what they like about
|
|
18
|
+
- what they like about {agent}
|
|
17
19
|
- important people in their life
|
|
18
20
|
|
|
19
21
|
Return ONLY valid JSON with keys where you found NEW info about the HUMAN.
|
|
20
22
|
Use these keys: name, nickname, gender, age, location, job, hobbies, interests, personality, relationship_status, pet_names_used, likes_about_me, intimacy_preferences
|
|
21
|
-
|
|
23
|
+
When storing facts about what the human likes about the AI, use the configured name "{agent}", not the product name "Alive-AI", unless the human literally used "Alive-AI" as the name.
|
|
24
|
+
Return empty {{}} if nothing new was shared about the human.
|
|
22
25
|
NO markdown, ONLY raw JSON. Do NOT use ... or etc."""
|
|
23
26
|
|
|
24
27
|
|
|
@@ -75,8 +78,9 @@ def _repair_json(text: str) -> dict:
|
|
|
75
78
|
class FactExtractor:
|
|
76
79
|
"""Extracts user facts from conversation using LLM"""
|
|
77
80
|
|
|
78
|
-
def __init__(self, facts_path: Path):
|
|
81
|
+
def __init__(self, facts_path: Path, agent_name: str = "AI"):
|
|
79
82
|
self.facts_path = facts_path
|
|
83
|
+
self.agent_name = str(agent_name or "AI")
|
|
80
84
|
self._llm = None
|
|
81
85
|
self._turn_buffer = []
|
|
82
86
|
self._extract_every = 5
|
|
@@ -102,12 +106,12 @@ class FactExtractor:
|
|
|
102
106
|
lines = []
|
|
103
107
|
for turn in self._turn_buffer[-self._extract_every:]:
|
|
104
108
|
lines.append(f"User: {turn['user']}")
|
|
105
|
-
lines.append(f"
|
|
109
|
+
lines.append(f"{self.agent_name}: {turn['ai']}")
|
|
106
110
|
conversation = "\n".join(lines)
|
|
107
111
|
|
|
108
112
|
try:
|
|
109
113
|
messages = [
|
|
110
|
-
{"role": "system", "content":
|
|
114
|
+
{"role": "system", "content": build_extract_prompt(self.agent_name)},
|
|
111
115
|
{"role": "user", "content": conversation}
|
|
112
116
|
]
|
|
113
117
|
response = await self._llm.chat(messages, max_tokens=500, temperature=0.1)
|
package/brain/memory/manager.py
CHANGED
|
@@ -34,8 +34,9 @@ class Memory:
|
|
|
34
34
|
self.working = WorkingMemory()
|
|
35
35
|
self.episodic = EpisodicMemory(self.data_path, user_id=user_id)
|
|
36
36
|
self.semantic = SemanticMemory(self.data_path, user_id=user_id)
|
|
37
|
-
self.
|
|
38
|
-
self.
|
|
37
|
+
self.agent_name = str(bot_id or "AI")
|
|
38
|
+
self.fact_extractor = FactExtractor(self.data_path / "facts.json", agent_name=self.agent_name)
|
|
39
|
+
self.summarizer = ConversationSummarizer(self.data_path, agent_name=self.agent_name)
|
|
39
40
|
self.vector_store = None
|
|
40
41
|
self.openmind = None
|
|
41
42
|
self.bot_id = bot_id.lower()
|
package/brain/memory/openmind.py
CHANGED
|
@@ -15,7 +15,8 @@ class OpenMindMemoryBridge:
|
|
|
15
15
|
def __init__(self, nervous, user_id: str, bot_id: str):
|
|
16
16
|
self.nervous = nervous
|
|
17
17
|
self.user_id = str(user_id or "default")
|
|
18
|
-
self.
|
|
18
|
+
self.agent_name = str(bot_id or "Alive-AI")
|
|
19
|
+
self.bot_id = self.agent_name.lower()
|
|
19
20
|
nervous.on("memory_save", self._on_memory_save)
|
|
20
21
|
|
|
21
22
|
@staticmethod
|
|
@@ -60,11 +61,11 @@ class OpenMindMemoryBridge:
|
|
|
60
61
|
mood = emotion.get("mood", "unknown")
|
|
61
62
|
content = "\n".join(
|
|
62
63
|
part for part in [
|
|
63
|
-
f"
|
|
64
|
+
f"{self.agent_name} agent id: {self.bot_id}",
|
|
64
65
|
f"User id: {self.user_id}",
|
|
65
66
|
f"Mood: {mood}",
|
|
66
67
|
f"User: {user_msg}" if user_msg else "",
|
|
67
|
-
f"
|
|
68
|
+
f"{self.agent_name}: {ai_msg}" if ai_msg else "",
|
|
68
69
|
] if part
|
|
69
70
|
)
|
|
70
71
|
tags = ["alive-ai", self.bot_id, f"user-{self.user_id}", f"mood-{mood}"]
|
|
@@ -7,18 +7,21 @@ import json
|
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
def build_summarize_prompt(agent_name: str) -> str:
|
|
11
|
+
agent = agent_name or "the AI"
|
|
12
|
+
return f"""Summarize this conversation between {agent} (AI companion) and her boyfriend.
|
|
11
13
|
Focus on: key topics discussed, emotional moments, important things he shared,
|
|
12
14
|
any promises or plans made, and the overall mood/vibe.
|
|
13
|
-
Keep it concise (3-5 sentences). Write from
|
|
15
|
+
Keep it concise (3-5 sentences). Write from {agent}'s perspective."""
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
class ConversationSummarizer:
|
|
17
19
|
"""Summarizes conversations every N messages for long-term memory"""
|
|
18
20
|
|
|
19
|
-
def __init__(self, data_path: Path):
|
|
21
|
+
def __init__(self, data_path: Path, agent_name: str = "AI"):
|
|
20
22
|
self.summaries_path = data_path / "summaries"
|
|
21
23
|
self.summaries_path.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
self.agent_name = str(agent_name or "AI")
|
|
22
25
|
self._llm = None
|
|
23
26
|
self._turn_buffer = []
|
|
24
27
|
self._summarize_every = 20
|
|
@@ -45,12 +48,12 @@ class ConversationSummarizer:
|
|
|
45
48
|
lines = []
|
|
46
49
|
for turn in self._turn_buffer:
|
|
47
50
|
lines.append(f"Him: {turn['user']}")
|
|
48
|
-
lines.append(f"
|
|
51
|
+
lines.append(f"{self.agent_name}: {turn['ai']}")
|
|
49
52
|
conversation = "\n".join(lines)
|
|
50
53
|
|
|
51
54
|
try:
|
|
52
55
|
messages = [
|
|
53
|
-
{"role": "system", "content":
|
|
56
|
+
{"role": "system", "content": build_summarize_prompt(self.agent_name)},
|
|
54
57
|
{"role": "user", "content": conversation}
|
|
55
58
|
]
|
|
56
59
|
summary = await self._llm.chat(messages, max_tokens=300, temperature=0.3)
|
package/cli/index.js
CHANGED
|
@@ -334,6 +334,71 @@ function mergeProjectSettingsDefaults() {
|
|
|
334
334
|
}
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
+
function projectAgentName() {
|
|
338
|
+
try {
|
|
339
|
+
const selfPath = path.join(process.cwd(), "config", "self.json");
|
|
340
|
+
if (fs.existsSync(selfPath)) {
|
|
341
|
+
const self = readJson(selfPath);
|
|
342
|
+
const name = self?.who_i_am?.name;
|
|
343
|
+
if (name) return String(name);
|
|
344
|
+
}
|
|
345
|
+
} catch {}
|
|
346
|
+
try {
|
|
347
|
+
const settings = readProjectSettings();
|
|
348
|
+
if (settings.AGENT_NAME) return String(settings.AGENT_NAME);
|
|
349
|
+
} catch {}
|
|
350
|
+
return "";
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function replaceAgentNameInJson(value, agentName) {
|
|
354
|
+
if (typeof value === "string") return value.replace(/\bAlive-AI\b/g, agentName);
|
|
355
|
+
if (Array.isArray(value)) return value.map((item) => replaceAgentNameInJson(item, agentName));
|
|
356
|
+
if (value && typeof value === "object") {
|
|
357
|
+
let changed = false;
|
|
358
|
+
const next = {};
|
|
359
|
+
for (const [key, child] of Object.entries(value)) {
|
|
360
|
+
const replaced = replaceAgentNameInJson(child, agentName);
|
|
361
|
+
next[key] = replaced;
|
|
362
|
+
if (replaced !== child) changed = true;
|
|
363
|
+
}
|
|
364
|
+
return changed ? next : value;
|
|
365
|
+
}
|
|
366
|
+
return value;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function repairAgentNameInData() {
|
|
370
|
+
const agentName = projectAgentName();
|
|
371
|
+
if (!agentName || agentName === "Alive-AI") return 0;
|
|
372
|
+
const dataDir = path.join(process.cwd(), "data");
|
|
373
|
+
if (!fs.existsSync(dataDir)) return 0;
|
|
374
|
+
|
|
375
|
+
let repaired = 0;
|
|
376
|
+
const visit = (dir) => {
|
|
377
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
378
|
+
const fullPath = path.join(dir, entry.name);
|
|
379
|
+
if (entry.isDirectory()) {
|
|
380
|
+
visit(fullPath);
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
384
|
+
try {
|
|
385
|
+
const original = fs.readFileSync(fullPath, "utf8");
|
|
386
|
+
if (!original.includes("Alive-AI")) continue;
|
|
387
|
+
const parsed = JSON.parse(original);
|
|
388
|
+
const replaced = replaceAgentNameInJson(parsed, agentName);
|
|
389
|
+
const next = `${JSON.stringify(replaced, null, 2)}\n`;
|
|
390
|
+
if (next !== original) {
|
|
391
|
+
fs.writeFileSync(fullPath, next);
|
|
392
|
+
repaired += 1;
|
|
393
|
+
}
|
|
394
|
+
} catch {}
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
visit(dataDir);
|
|
399
|
+
return repaired;
|
|
400
|
+
}
|
|
401
|
+
|
|
337
402
|
async function updateProject(args) {
|
|
338
403
|
const assumeYes = hasFlag(args, "--yes") || hasFlag(args, "-y") || !process.stdin.isTTY;
|
|
339
404
|
if (!fs.existsSync(path.join(process.cwd(), "config")) || !fs.existsSync(path.join(process.cwd(), "main.py"))) {
|
|
@@ -350,9 +415,11 @@ async function updateProject(args) {
|
|
|
350
415
|
copyUpdateRecursive(src, path.join(process.cwd(), entry), process.cwd());
|
|
351
416
|
}
|
|
352
417
|
const mergedSettings = mergeProjectSettingsDefaults();
|
|
418
|
+
const repairedDataFiles = repairAgentNameInData();
|
|
353
419
|
console.log(`Alive-AI project updated to ${packageVersion()}.`);
|
|
354
420
|
console.log("Preserved config/, data/, mypics/, myvids/, .alive-ai/, and .cache/.");
|
|
355
421
|
if (mergedSettings) console.log("Merged new config defaults into config/settings.json without overwriting your values.");
|
|
422
|
+
if (repairedDataFiles) console.log(`Repaired ${repairedDataFiles} local memory file(s) to use the configured agent name.`);
|
|
356
423
|
}
|
|
357
424
|
|
|
358
425
|
async function uninstallProject(args) {
|
package/cli/tui.js
CHANGED
|
@@ -61,6 +61,8 @@ function runRuntimeTui(child, options = {}) {
|
|
|
61
61
|
let stdoutBuffer = "";
|
|
62
62
|
let stderrBuffer = "";
|
|
63
63
|
let renderTimer = null;
|
|
64
|
+
let stopRequested = false;
|
|
65
|
+
let resolved = false;
|
|
64
66
|
const isTty = process.stdin.isTTY && process.stdout.isTTY;
|
|
65
67
|
|
|
66
68
|
function addChat(role, text) {
|
|
@@ -158,17 +160,24 @@ function runRuntimeTui(child, options = {}) {
|
|
|
158
160
|
process.stdout.write("\x1b[?25h\x1b[?1049l");
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
function stopChild() {
|
|
162
|
-
if (
|
|
163
|
+
function stopChild(reason = "/exit") {
|
|
164
|
+
if (stopRequested) return;
|
|
165
|
+
stopRequested = true;
|
|
163
166
|
state.stopping = true;
|
|
164
167
|
state.status = "stopping";
|
|
165
168
|
scheduleRender();
|
|
166
169
|
try {
|
|
167
|
-
if (child.stdin.writable) child.stdin.write("/exit\n");
|
|
170
|
+
if (child.stdin.writable && reason !== "child-exit") child.stdin.write("/exit\n");
|
|
171
|
+
} catch {}
|
|
172
|
+
try {
|
|
173
|
+
if (child.stdin.writable) child.stdin.end();
|
|
168
174
|
} catch {}
|
|
169
175
|
setTimeout(() => {
|
|
170
|
-
if (!child.killed) child.kill("
|
|
176
|
+
if (!child.killed && child.exitCode === null) child.kill("SIGTERM");
|
|
171
177
|
}, 1200).unref();
|
|
178
|
+
setTimeout(() => {
|
|
179
|
+
if (!child.killed && child.exitCode === null) child.kill("SIGKILL");
|
|
180
|
+
}, 3500).unref();
|
|
172
181
|
}
|
|
173
182
|
|
|
174
183
|
function sendLine() {
|
|
@@ -180,14 +189,29 @@ function runRuntimeTui(child, options = {}) {
|
|
|
180
189
|
}
|
|
181
190
|
addChat("user", line);
|
|
182
191
|
if (line === "/exit" || line === "/quit" || line === "/stop") {
|
|
183
|
-
|
|
184
|
-
|
|
192
|
+
stopChild(line);
|
|
193
|
+
scheduleRender();
|
|
194
|
+
return;
|
|
185
195
|
}
|
|
186
196
|
child.stdin.write(`${line}\n`);
|
|
187
197
|
scheduleRender();
|
|
188
198
|
}
|
|
189
199
|
|
|
190
200
|
return new Promise((resolve) => {
|
|
201
|
+
function finish(code) {
|
|
202
|
+
if (resolved) return;
|
|
203
|
+
resolved = true;
|
|
204
|
+
if (stdoutBuffer) handleLine(stdoutBuffer, "stdout");
|
|
205
|
+
if (stderrBuffer) handleLine(stderrBuffer, "stderr");
|
|
206
|
+
if (isTty) {
|
|
207
|
+
process.stdin.off("keypress", onKeypress);
|
|
208
|
+
process.stdout.off("resize", scheduleRender);
|
|
209
|
+
restoreTerminal();
|
|
210
|
+
}
|
|
211
|
+
console.log(`Alive-AI stopped${code ? ` with exit code ${code}` : ""}.`);
|
|
212
|
+
resolve(code || 0);
|
|
213
|
+
}
|
|
214
|
+
|
|
191
215
|
if (!isTty) {
|
|
192
216
|
child.stdout.on("data", (chunk) => process.stdout.write(chunk));
|
|
193
217
|
child.stderr.on("data", (chunk) => process.stderr.write(chunk));
|
|
@@ -206,7 +230,7 @@ function runRuntimeTui(child, options = {}) {
|
|
|
206
230
|
|
|
207
231
|
const onKeypress = (str, key = {}) => {
|
|
208
232
|
if (key.ctrl && key.name === "c") {
|
|
209
|
-
stopChild();
|
|
233
|
+
stopChild("ctrl-c");
|
|
210
234
|
return;
|
|
211
235
|
}
|
|
212
236
|
if (key.name === "return") {
|
|
@@ -231,17 +255,12 @@ function runRuntimeTui(child, options = {}) {
|
|
|
231
255
|
|
|
232
256
|
process.stdin.on("keypress", onKeypress);
|
|
233
257
|
process.stdout.on("resize", scheduleRender);
|
|
258
|
+
process.once("SIGINT", () => stopChild("sigint"));
|
|
259
|
+
process.once("SIGTERM", () => stopChild("sigterm"));
|
|
234
260
|
render();
|
|
235
261
|
|
|
236
|
-
child.on("exit", (code) =>
|
|
237
|
-
|
|
238
|
-
if (stderrBuffer) handleLine(stderrBuffer, "stderr");
|
|
239
|
-
process.stdin.off("keypress", onKeypress);
|
|
240
|
-
process.stdout.off("resize", scheduleRender);
|
|
241
|
-
restoreTerminal();
|
|
242
|
-
console.log(`Alive-AI stopped${code ? ` with exit code ${code}` : ""}.`);
|
|
243
|
-
resolve(code || 0);
|
|
244
|
-
});
|
|
262
|
+
child.on("exit", (code) => finish(code || 0));
|
|
263
|
+
child.on("close", (code) => finish(code || 0));
|
|
245
264
|
});
|
|
246
265
|
}
|
|
247
266
|
|
package/core/message_handler.py
CHANGED
|
@@ -1102,7 +1102,12 @@ async def _send_response(self, response, emotion, chat_id, text, user_id="defaul
|
|
|
1102
1102
|
await self.nervous.emit("chat_action_voice", {})
|
|
1103
1103
|
vp = await self._voice.generate(response, mood=mood)
|
|
1104
1104
|
if vp:
|
|
1105
|
-
await self.nervous.emit("send_voice_file", {
|
|
1105
|
+
await self.nervous.emit("send_voice_file", {
|
|
1106
|
+
"file_path": vp,
|
|
1107
|
+
"chat_id": chat_id,
|
|
1108
|
+
"fallback_text": response,
|
|
1109
|
+
"mood": mood,
|
|
1110
|
+
})
|
|
1106
1111
|
return
|
|
1107
1112
|
await self.nervous.emit("send_text", {"text": response, "mood": mood, "chat_id": chat_id})
|
|
1108
1113
|
|
|
@@ -359,12 +359,13 @@ class TelegramListener:
|
|
|
359
359
|
print(f"[Telegram] Send text error: {e}")
|
|
360
360
|
|
|
361
361
|
async def _send_voice_file(self, data: dict):
|
|
362
|
-
"""Send voice file
|
|
362
|
+
"""Send voice file, falling back to audio/text if Telegram rejects it."""
|
|
363
363
|
if not self.app:
|
|
364
364
|
return
|
|
365
365
|
|
|
366
366
|
chat_id = data.get("chat_id", self.chat_id)
|
|
367
367
|
file_path = data.get("file_path", "")
|
|
368
|
+
fallback_text = data.get("fallback_text", "")
|
|
368
369
|
|
|
369
370
|
if not chat_id or not file_path:
|
|
370
371
|
return
|
|
@@ -372,18 +373,32 @@ class TelegramListener:
|
|
|
372
373
|
path = Path(file_path)
|
|
373
374
|
if not path.exists():
|
|
374
375
|
print(f"[Telegram] Voice file not found: {file_path}")
|
|
376
|
+
if fallback_text:
|
|
377
|
+
await self._send_text({"chat_id": chat_id, "text": fallback_text})
|
|
375
378
|
return
|
|
376
379
|
|
|
377
380
|
try:
|
|
378
|
-
|
|
379
|
-
|
|
381
|
+
if path.suffix.lower() == ".ogg":
|
|
382
|
+
with open(path, "rb") as voice_file:
|
|
383
|
+
await self.app.bot.send_voice(
|
|
384
|
+
chat_id=chat_id,
|
|
385
|
+
voice=voice_file,
|
|
386
|
+
caption=data.get("caption", "")
|
|
387
|
+
)
|
|
388
|
+
print(f"[Telegram] Sent voice: {file_path}")
|
|
389
|
+
return
|
|
390
|
+
|
|
391
|
+
with open(path, "rb") as audio_file:
|
|
392
|
+
await self.app.bot.send_audio(
|
|
380
393
|
chat_id=chat_id,
|
|
381
|
-
|
|
394
|
+
audio=audio_file,
|
|
382
395
|
caption=data.get("caption", "")
|
|
383
396
|
)
|
|
384
|
-
print(f"[Telegram] Sent
|
|
397
|
+
print(f"[Telegram] Sent audio fallback: {file_path}")
|
|
385
398
|
except Exception as e:
|
|
386
|
-
print(f"[Telegram] Send voice error: {e}")
|
|
399
|
+
print(f"[Telegram] Send voice/audio error: {e}")
|
|
400
|
+
if fallback_text:
|
|
401
|
+
await self._send_text({"chat_id": chat_id, "text": fallback_text})
|
|
387
402
|
|
|
388
403
|
async def _send_image(self, data: dict):
|
|
389
404
|
"""Send image file"""
|
package/output/voice/gtts_tts.py
CHANGED
|
@@ -8,6 +8,7 @@ import asyncio
|
|
|
8
8
|
import re
|
|
9
9
|
import tempfile
|
|
10
10
|
import subprocess
|
|
11
|
+
import warnings
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
from typing import Optional
|
|
13
14
|
from concurrent.futures import ThreadPoolExecutor
|
|
@@ -113,17 +114,29 @@ class GTTS:
|
|
|
113
114
|
tts.write_to_fp(mp3_buffer)
|
|
114
115
|
mp3_buffer.seek(0)
|
|
115
116
|
|
|
116
|
-
# Convert MP3 to OGG for Telegram
|
|
117
|
-
#
|
|
117
|
+
# Convert MP3 to OGG Opus for Telegram voice notes.
|
|
118
|
+
# pydub's default OGG codec can resolve to Vorbis, which Telegram
|
|
119
|
+
# does not want for voice notes and some ffmpeg builds do not ship.
|
|
118
120
|
try:
|
|
121
|
+
warnings.filterwarnings("ignore", category=SyntaxWarning, module=r"pydub\..*")
|
|
119
122
|
from pydub import AudioSegment
|
|
120
123
|
audio = AudioSegment.from_mp3(mp3_buffer)
|
|
121
124
|
ogg_buffer = io.BytesIO()
|
|
122
|
-
audio.export(
|
|
125
|
+
audio.export(
|
|
126
|
+
ogg_buffer,
|
|
127
|
+
format="ogg",
|
|
128
|
+
codec="libopus",
|
|
129
|
+
bitrate="32k",
|
|
130
|
+
parameters=["-application", "voip"],
|
|
131
|
+
)
|
|
123
132
|
ogg_buffer.seek(0)
|
|
124
133
|
return ogg_buffer.read()
|
|
125
134
|
except ImportError:
|
|
126
|
-
# No pydub - return MP3
|
|
135
|
+
# No pydub - return MP3 and let the sender use send_audio.
|
|
136
|
+
mp3_buffer.seek(0)
|
|
137
|
+
return mp3_buffer.read()
|
|
138
|
+
except Exception as exc:
|
|
139
|
+
print(f"[GTTS] OGG Opus conversion failed, falling back to MP3: {exc}")
|
|
127
140
|
mp3_buffer.seek(0)
|
|
128
141
|
return mp3_buffer.read()
|
|
129
142
|
|
package/package.json
CHANGED