@gonzih/cc-tg 0.9.11 → 0.9.12
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.d.ts +1 -0
- package/dist/bot.js +110 -2
- package/package.json +1 -1
package/dist/bot.d.ts
CHANGED
package/dist/bot.js
CHANGED
|
@@ -31,6 +31,7 @@ const BOT_COMMANDS = [
|
|
|
31
31
|
{ command: "cost", description: "Show session token usage and cost" },
|
|
32
32
|
{ command: "skills", description: "List available Claude skills with descriptions" },
|
|
33
33
|
{ command: "cron", description: "Manage cron jobs — add/list/edit/remove/clear" },
|
|
34
|
+
{ command: "voice_retry", description: "Retry failed voice message transcriptions" },
|
|
34
35
|
];
|
|
35
36
|
const FLUSH_DELAY_MS = 800; // debounce streaming chunks into one Telegram message
|
|
36
37
|
const TYPING_INTERVAL_MS = 4000; // re-send typing action before Telegram's 5s expiry
|
|
@@ -380,6 +381,11 @@ export class CcTgBot {
|
|
|
380
381
|
await this.replyToChat(chatId, listSkills(), threadId);
|
|
381
382
|
return;
|
|
382
383
|
}
|
|
384
|
+
// /voice_retry — retry failed voice message transcriptions
|
|
385
|
+
if (text === "/voice_retry") {
|
|
386
|
+
await this.handleVoiceRetry(chatId, threadId);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
383
389
|
const session = this.getOrCreateSession(chatId, threadId, threadName);
|
|
384
390
|
try {
|
|
385
391
|
const enriched = await enrichPromptWithUrls(text);
|
|
@@ -418,10 +424,24 @@ export class CcTgBot {
|
|
|
418
424
|
return;
|
|
419
425
|
console.log(`[voice:${chatId}] received voice message, transcribing...`);
|
|
420
426
|
this.bot.sendChatAction(chatId, "typing", threadId !== undefined ? { message_thread_id: threadId } : undefined).catch(() => { });
|
|
427
|
+
// Store in Redis before transcription so we can retry on failure
|
|
428
|
+
const pendingEntry = JSON.stringify({
|
|
429
|
+
file_id: fileId,
|
|
430
|
+
chat_id: chatId,
|
|
431
|
+
message_id: msg.message_id,
|
|
432
|
+
timestamp: Date.now(),
|
|
433
|
+
});
|
|
434
|
+
if (this.redis) {
|
|
435
|
+
await this.redis.rpush("voice:pending", pendingEntry).catch((err) => console.warn("[voice] redis rpush voice:pending failed:", err.message));
|
|
436
|
+
}
|
|
421
437
|
try {
|
|
422
438
|
const fileLink = await this.bot.getFileLink(fileId);
|
|
423
439
|
const transcript = await transcribeVoice(fileLink);
|
|
424
440
|
console.log(`[voice:${chatId}] transcribed: ${transcript}`);
|
|
441
|
+
// Remove from pending on success
|
|
442
|
+
if (this.redis) {
|
|
443
|
+
await this.redis.lrem("voice:pending", 0, pendingEntry).catch((err) => console.warn("[voice] redis lrem voice:pending failed:", err.message));
|
|
444
|
+
}
|
|
425
445
|
if (!transcript || transcript === "[empty transcription]") {
|
|
426
446
|
await this.replyToChat(chatId, "Could not transcribe voice message.", threadId);
|
|
427
447
|
return;
|
|
@@ -441,9 +461,97 @@ export class CcTgBot {
|
|
|
441
461
|
}
|
|
442
462
|
}
|
|
443
463
|
catch (err) {
|
|
444
|
-
|
|
445
|
-
|
|
464
|
+
const errMsg = err.message;
|
|
465
|
+
console.error(`[voice:${chatId}] error:`, errMsg);
|
|
466
|
+
// Push to voice:failed on failure (entry stays in voice:pending for retry)
|
|
467
|
+
if (this.redis) {
|
|
468
|
+
const failedEntry = JSON.stringify({
|
|
469
|
+
file_id: fileId,
|
|
470
|
+
chat_id: chatId,
|
|
471
|
+
message_id: msg.message_id,
|
|
472
|
+
timestamp: Date.now(),
|
|
473
|
+
error: errMsg,
|
|
474
|
+
failed_at: Date.now(),
|
|
475
|
+
});
|
|
476
|
+
this.redis.rpush("voice:failed", failedEntry)
|
|
477
|
+
.then(() => this.redis.expire("voice:failed", 48 * 60 * 60))
|
|
478
|
+
.catch((redisErr) => console.warn("[voice] redis write voice:failed failed:", redisErr.message));
|
|
479
|
+
}
|
|
480
|
+
// User-friendly error messages
|
|
481
|
+
let userMsg;
|
|
482
|
+
if (errMsg.includes("whisper-cpp not found") || errMsg.includes("whisper not found")) {
|
|
483
|
+
userMsg = "Voice transcription unavailable — whisper-cpp not installed";
|
|
484
|
+
}
|
|
485
|
+
else if (errMsg.includes("No whisper model found")) {
|
|
486
|
+
userMsg = "Voice transcription unavailable — no whisper model found";
|
|
487
|
+
}
|
|
488
|
+
else if (errMsg.includes("HTTP") && errMsg.includes("downloading")) {
|
|
489
|
+
userMsg = "Could not download voice file from Telegram";
|
|
490
|
+
}
|
|
491
|
+
else {
|
|
492
|
+
userMsg = `Voice transcription failed: ${errMsg}`;
|
|
493
|
+
}
|
|
494
|
+
await this.replyToChat(chatId, userMsg, threadId);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
async handleVoiceRetry(chatId, threadId) {
|
|
498
|
+
if (!this.redis) {
|
|
499
|
+
await this.replyToChat(chatId, "Redis not configured — voice retry unavailable.", threadId);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const [pendingRaw, failedRaw] = await Promise.all([
|
|
503
|
+
this.redis.lrange("voice:pending", 0, -1).catch(() => []),
|
|
504
|
+
this.redis.lrange("voice:failed", 0, -1).catch(() => []),
|
|
505
|
+
]);
|
|
506
|
+
// Deduplicate by file_id across both lists
|
|
507
|
+
const allEntries = new Map();
|
|
508
|
+
for (const raw of [...pendingRaw, ...failedRaw]) {
|
|
509
|
+
try {
|
|
510
|
+
const entry = JSON.parse(raw);
|
|
511
|
+
if (entry.file_id)
|
|
512
|
+
allEntries.set(entry.file_id, entry);
|
|
513
|
+
}
|
|
514
|
+
catch { /* skip malformed entries */ }
|
|
515
|
+
}
|
|
516
|
+
if (allEntries.size === 0) {
|
|
517
|
+
await this.replyToChat(chatId, "No pending voice messages to retry.", threadId);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
await this.replyToChat(chatId, `Retrying ${allEntries.size} voice message(s)...`, threadId);
|
|
521
|
+
let succeeded = 0;
|
|
522
|
+
let failed = 0;
|
|
523
|
+
const errors = [];
|
|
524
|
+
for (const [fileId, entry] of allEntries) {
|
|
525
|
+
try {
|
|
526
|
+
const fileLink = await this.bot.getFileLink(fileId);
|
|
527
|
+
const transcript = await transcribeVoice(fileLink);
|
|
528
|
+
if (transcript && transcript !== "[empty transcription]") {
|
|
529
|
+
const session = this.getOrCreateSession(entry.chat_id, threadId, undefined);
|
|
530
|
+
session.claude.sendPrompt(transcript);
|
|
531
|
+
this.writeChatMessage("user", "telegram", transcript, entry.chat_id);
|
|
532
|
+
// Remove from both lists
|
|
533
|
+
const matchPending = pendingRaw.find((r) => r.includes(`"${fileId}"`));
|
|
534
|
+
const matchFailed = failedRaw.find((r) => r.includes(`"${fileId}"`));
|
|
535
|
+
if (matchPending)
|
|
536
|
+
await this.redis.lrem("voice:pending", 0, matchPending).catch(() => { });
|
|
537
|
+
if (matchFailed)
|
|
538
|
+
await this.redis.lrem("voice:failed", 0, matchFailed).catch(() => { });
|
|
539
|
+
succeeded++;
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
failed++;
|
|
543
|
+
errors.push(`${fileId}: empty transcription`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
failed++;
|
|
548
|
+
errors.push(`${fileId}: ${err.message}`);
|
|
549
|
+
}
|
|
446
550
|
}
|
|
551
|
+
const lines = [`Voice retry complete: ${succeeded} succeeded, ${failed} failed.`];
|
|
552
|
+
if (errors.length > 0)
|
|
553
|
+
lines.push(...errors.map((e) => `• ${e}`));
|
|
554
|
+
await this.replyToChat(chatId, lines.join("\n"), threadId);
|
|
447
555
|
}
|
|
448
556
|
async handlePhoto(chatId, msg, threadId, threadName) {
|
|
449
557
|
// Pick highest resolution photo
|