@grinev/opencode-telegram-bot 0.18.0 → 0.19.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/.env.example +22 -1
- package/README.md +20 -5
- package/dist/app/start-bot-app.js +4 -0
- package/dist/attach/service.js +6 -1
- package/dist/bot/commands/definitions.js +1 -0
- package/dist/bot/commands/mcps.js +394 -0
- package/dist/bot/commands/opencode-start.js +2 -10
- package/dist/bot/index.js +6 -2
- package/dist/config.js +25 -6
- package/dist/i18n/de.js +19 -0
- package/dist/i18n/en.js +19 -0
- package/dist/i18n/es.js +19 -0
- package/dist/i18n/fr.js +19 -0
- package/dist/i18n/ru.js +19 -0
- package/dist/i18n/zh.js +19 -0
- package/dist/opencode/auto-restart.js +92 -0
- package/dist/opencode/process.js +18 -1
- package/dist/pinned/manager.js +21 -0
- package/dist/summary/aggregator.js +35 -0
- package/dist/summary/formatter.js +2 -2
- package/dist/summary/markdown-to-telegram-v2.js +99 -0
- package/dist/summary/subagent-formatter.js +23 -2
- package/dist/telegram/render/inline-renderer.js +18 -0
- package/dist/tts/client.js +94 -19
- package/package.json +2 -2
package/dist/tts/client.js
CHANGED
|
@@ -1,22 +1,80 @@
|
|
|
1
1
|
import { config } from "../config.js";
|
|
2
2
|
import { logger } from "../utils/logger.js";
|
|
3
|
+
import textToSpeech from "@google-cloud/text-to-speech";
|
|
3
4
|
const TTS_REQUEST_TIMEOUT_MS = 60_000;
|
|
4
5
|
export function isTtsConfigured() {
|
|
6
|
+
if (config.tts.provider === "google") {
|
|
7
|
+
return Boolean(process.env.GOOGLE_APPLICATION_CREDENTIALS);
|
|
8
|
+
}
|
|
5
9
|
return Boolean(config.tts.apiUrl && config.tts.apiKey);
|
|
6
10
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Removes markdown syntax that TTS engines would read aloud
|
|
13
|
+
* (asterisks, backticks, heading markers, etc.).
|
|
14
|
+
*/
|
|
15
|
+
export function stripMarkdownForSpeech(text) {
|
|
16
|
+
let clean = text;
|
|
17
|
+
// fenced code blocks → inline content
|
|
18
|
+
clean = clean.replace(/```[\s\S]*?```/g, (match) => {
|
|
19
|
+
const inner = match.replace(/^```\w*\n?/, "").replace(/\n?```$/, "");
|
|
20
|
+
return inner.replace(/\n/g, " ");
|
|
21
|
+
});
|
|
22
|
+
clean = clean.replace(/`([^`]+)`/g, "$1");
|
|
23
|
+
clean = clean.replace(/\*\*\*(.+?)\*\*\*/g, "$1");
|
|
24
|
+
clean = clean.replace(/\*\*(.+?)\*\*/g, "$1");
|
|
25
|
+
clean = clean.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, "$1");
|
|
26
|
+
clean = clean.replace(/~~(.+?)~~/g, "$1");
|
|
27
|
+
clean = clean.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
28
|
+
clean = clean.replace(/^#{1,6}\s+/gm, "");
|
|
29
|
+
clean = clean.replace(/^>\s?/gm, "");
|
|
30
|
+
clean = clean.replace(/^[-*]\s+/gm, "");
|
|
31
|
+
clean = clean.replace(/^\d+\.\s+/gm, "");
|
|
32
|
+
clean = clean.replace(/^[-*_]{3,}\s*$/gm, "");
|
|
33
|
+
clean = clean.replace(/<\/?[A-Za-z][^>]*>/g, "");
|
|
34
|
+
clean = clean.replace(/[ \t]+/g, " ");
|
|
35
|
+
clean = clean.replace(/\n{3,}/g, "\n\n");
|
|
36
|
+
return clean.trim();
|
|
37
|
+
}
|
|
38
|
+
/** Extracts "ll-CC" from Google voice names like "de-DE-Neural2-B". */
|
|
39
|
+
export function extractLanguageCode(voiceName) {
|
|
40
|
+
const match = voiceName.match(/^([a-z]{2,3}-[A-Z]{2})/);
|
|
41
|
+
return match ? match[1] : "en-US";
|
|
42
|
+
}
|
|
43
|
+
// --- Provider implementations ---
|
|
44
|
+
let googleClient = null;
|
|
45
|
+
function getGoogleClient() {
|
|
46
|
+
if (!googleClient) {
|
|
47
|
+
googleClient = new textToSpeech.TextToSpeechClient();
|
|
10
48
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
49
|
+
return googleClient;
|
|
50
|
+
}
|
|
51
|
+
/** @internal Reset Google client singleton (for tests only). */
|
|
52
|
+
export function _resetGoogleClient() {
|
|
53
|
+
googleClient = null;
|
|
54
|
+
}
|
|
55
|
+
async function synthesizeWithGoogle(text) {
|
|
56
|
+
const client = getGoogleClient();
|
|
57
|
+
const voiceName = config.tts.voice || "en-US-Studio-O";
|
|
58
|
+
const languageCode = extractLanguageCode(voiceName);
|
|
59
|
+
logger.debug(`[TTS] Google Cloud TTS: voice=${voiceName}, languageCode=${languageCode}, chars=${text.length}`);
|
|
60
|
+
const [response] = await client.synthesizeSpeech({
|
|
61
|
+
input: { text },
|
|
62
|
+
voice: { languageCode, name: voiceName },
|
|
63
|
+
audioConfig: { audioEncoding: "MP3" },
|
|
64
|
+
}, { timeout: TTS_REQUEST_TIMEOUT_MS });
|
|
65
|
+
const raw = response.audioContent;
|
|
66
|
+
const buffer = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
67
|
+
if (buffer.length === 0) {
|
|
68
|
+
throw new Error("Google TTS API returned an empty audio response");
|
|
14
69
|
}
|
|
70
|
+
return { buffer, filename: "assistant-reply.mp3", mimeType: "audio/mpeg" };
|
|
71
|
+
}
|
|
72
|
+
async function synthesizeWithOpenAi(text) {
|
|
73
|
+
const url = `${config.tts.apiUrl}/audio/speech`;
|
|
74
|
+
logger.debug(`[TTS] OpenAI-compatible: url=${url}, model=${config.tts.model}, voice=${config.tts.voice}, chars=${text.length}`);
|
|
15
75
|
const controller = new AbortController();
|
|
16
76
|
const timeout = setTimeout(() => controller.abort(), TTS_REQUEST_TIMEOUT_MS);
|
|
17
77
|
try {
|
|
18
|
-
const url = `${config.tts.apiUrl}/audio/speech`;
|
|
19
|
-
logger.debug(`[TTS] Sending speech synthesis request: url=${url}, model=${config.tts.model}, voice=${config.tts.voice}, chars=${input.length}`);
|
|
20
78
|
const response = await fetch(url, {
|
|
21
79
|
method: "POST",
|
|
22
80
|
headers: {
|
|
@@ -26,7 +84,7 @@ export async function synthesizeSpeech(text) {
|
|
|
26
84
|
body: JSON.stringify({
|
|
27
85
|
model: config.tts.model,
|
|
28
86
|
voice: config.tts.voice,
|
|
29
|
-
input,
|
|
87
|
+
input: text,
|
|
30
88
|
response_format: "mp3",
|
|
31
89
|
}),
|
|
32
90
|
signal: controller.signal,
|
|
@@ -35,17 +93,37 @@ export async function synthesizeSpeech(text) {
|
|
|
35
93
|
const errorBody = await response.text().catch(() => "");
|
|
36
94
|
throw new Error(`TTS API returned HTTP ${response.status}: ${errorBody || response.statusText}`);
|
|
37
95
|
}
|
|
38
|
-
const
|
|
39
|
-
const buffer = Buffer.from(arrayBuffer);
|
|
96
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
40
97
|
if (buffer.length === 0) {
|
|
41
98
|
throw new Error("TTS API returned an empty audio response");
|
|
42
99
|
}
|
|
43
100
|
logger.debug(`[TTS] Generated speech audio: ${buffer.length} bytes`);
|
|
44
|
-
return {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
101
|
+
return { buffer, filename: "assistant-reply.mp3", mimeType: "audio/mpeg" };
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
clearTimeout(timeout);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// --- Public API ---
|
|
108
|
+
function getNotConfiguredMessage() {
|
|
109
|
+
return config.tts.provider === "google"
|
|
110
|
+
? "TTS is not configured: set GOOGLE_APPLICATION_CREDENTIALS for Google Cloud TTS"
|
|
111
|
+
: "TTS is not configured: set TTS_API_URL and TTS_API_KEY";
|
|
112
|
+
}
|
|
113
|
+
export async function synthesizeSpeech(text) {
|
|
114
|
+
if (!isTtsConfigured()) {
|
|
115
|
+
throw new Error(getNotConfiguredMessage());
|
|
116
|
+
}
|
|
117
|
+
const raw = text.trim();
|
|
118
|
+
if (!raw) {
|
|
119
|
+
throw new Error("TTS input text is empty");
|
|
120
|
+
}
|
|
121
|
+
const input = stripMarkdownForSpeech(raw);
|
|
122
|
+
try {
|
|
123
|
+
if (config.tts.provider === "google") {
|
|
124
|
+
return await synthesizeWithGoogle(input);
|
|
125
|
+
}
|
|
126
|
+
return await synthesizeWithOpenAi(input);
|
|
49
127
|
}
|
|
50
128
|
catch (err) {
|
|
51
129
|
if (err instanceof DOMException && err.name === "AbortError") {
|
|
@@ -53,7 +131,4 @@ export async function synthesizeSpeech(text) {
|
|
|
53
131
|
}
|
|
54
132
|
throw err;
|
|
55
133
|
}
|
|
56
|
-
finally {
|
|
57
|
-
clearTimeout(timeout);
|
|
58
|
-
}
|
|
59
134
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grinev/opencode-telegram-bot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Telegram bot client for OpenCode to run and monitor coding tasks from chat.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"homepage": "https://github.com/grinev/opencode-telegram-bot#readme",
|
|
53
53
|
"dependencies": {
|
|
54
|
+
"@google-cloud/text-to-speech": "^6.4.0",
|
|
54
55
|
"@grammyjs/menu": "^1.3.1",
|
|
55
56
|
"@opencode-ai/sdk": "^1.1.21",
|
|
56
57
|
"better-sqlite3": "^12.6.2",
|
|
@@ -61,7 +62,6 @@
|
|
|
61
62
|
"remark-gfm": "^4.0.1",
|
|
62
63
|
"remark-parse": "^11.0.0",
|
|
63
64
|
"socks-proxy-agent": "^8.0.5",
|
|
64
|
-
"telegram-markdown-v2": "^0.0.4",
|
|
65
65
|
"unified": "^11.0.5"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|