@gonzih/cc-tg 0.9.15 → 0.9.16
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/README.md +36 -0
- package/package.json +4 -3
- package/dist/bot.d.ts +0 -54
- package/dist/bot.js +0 -1222
- package/dist/claude.d.ts +0 -54
- package/dist/claude.js +0 -208
- package/dist/cron.d.ts +0 -33
- package/dist/cron.js +0 -127
- package/dist/formatter.d.ts +0 -23
- package/dist/formatter.js +0 -86
- package/dist/index.d.ts +0 -17
- package/dist/index.js +0 -123
- package/dist/usage-limit.d.ts +0 -7
- package/dist/usage-limit.js +0 -30
- package/dist/voice.d.ts +0 -13
- package/dist/voice.js +0 -140
package/dist/usage-limit.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
export function detectUsageLimit(text) {
|
|
2
|
-
const lower = text.toLowerCase();
|
|
3
|
-
if (lower.includes('extra usage') ||
|
|
4
|
-
lower.includes('usage has been disabled') ||
|
|
5
|
-
lower.includes('billing_error') ||
|
|
6
|
-
lower.includes('usage limit reached') ||
|
|
7
|
-
lower.includes('your usage limit')) {
|
|
8
|
-
const wake = nextHourBoundary() + 5 * 60 * 1000;
|
|
9
|
-
return {
|
|
10
|
-
detected: true,
|
|
11
|
-
reason: 'usage_exhausted',
|
|
12
|
-
retryAfterMs: wake - Date.now(),
|
|
13
|
-
humanMessage: `⏸ Claude usage limit reached. Will auto-resume at ${new Date(wake).toUTCString()}. I'll message you when it's back.`,
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
if (lower.includes('currently overloaded') || lower.includes('overloaded with requests')) {
|
|
17
|
-
return {
|
|
18
|
-
detected: true,
|
|
19
|
-
reason: 'rate_limit',
|
|
20
|
-
retryAfterMs: 2 * 60 * 1000,
|
|
21
|
-
humanMessage: `⏸ Rate limited. Retrying in 2 minutes...`,
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
return { detected: false, reason: 'rate_limit', retryAfterMs: 0, humanMessage: '' };
|
|
25
|
-
}
|
|
26
|
-
function nextHourBoundary() {
|
|
27
|
-
const d = new Date();
|
|
28
|
-
d.setHours(d.getHours() + 1, 0, 0, 0);
|
|
29
|
-
return d.getTime();
|
|
30
|
-
}
|
package/dist/voice.d.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Voice message transcription via whisper.cpp.
|
|
3
|
-
* Flow: Telegram OGG → ffmpeg convert to 16kHz WAV → whisper-cpp → text
|
|
4
|
-
*/
|
|
5
|
-
/**
|
|
6
|
-
* Transcribe a voice message from a Telegram file URL.
|
|
7
|
-
* Returns the transcribed text, or throws if whisper/ffmpeg not available.
|
|
8
|
-
*/
|
|
9
|
-
export declare function transcribeVoice(fileUrl: string): Promise<string>;
|
|
10
|
-
/**
|
|
11
|
-
* Check if voice transcription is available on this system.
|
|
12
|
-
*/
|
|
13
|
-
export declare function isVoiceAvailable(): boolean;
|
package/dist/voice.js
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Voice message transcription via whisper.cpp.
|
|
3
|
-
* Flow: Telegram OGG → ffmpeg convert to 16kHz WAV → whisper-cpp → text
|
|
4
|
-
*/
|
|
5
|
-
import { execFile } from "child_process";
|
|
6
|
-
import { promisify } from "util";
|
|
7
|
-
import { existsSync } from "fs";
|
|
8
|
-
import { unlink } from "fs/promises";
|
|
9
|
-
import { tmpdir } from "os";
|
|
10
|
-
import { join } from "path";
|
|
11
|
-
import https from "https";
|
|
12
|
-
import http from "http";
|
|
13
|
-
import { createWriteStream } from "fs";
|
|
14
|
-
const execFileAsync = promisify(execFile);
|
|
15
|
-
// Whisper model — small.en is fast and accurate enough for commands
|
|
16
|
-
// Falls back to base.en if small not found
|
|
17
|
-
const WHISPER_MODELS = [
|
|
18
|
-
"/opt/homebrew/share/whisper-cpp/ggml-small.en.bin",
|
|
19
|
-
"/opt/homebrew/share/whisper-cpp/ggml-small.bin",
|
|
20
|
-
"/opt/homebrew/share/whisper-cpp/ggml-base.en.bin",
|
|
21
|
-
"/opt/homebrew/share/whisper-cpp/ggml-base.bin",
|
|
22
|
-
// user-local
|
|
23
|
-
`${process.env.HOME}/.local/share/whisper-cpp/ggml-small.en.bin`,
|
|
24
|
-
`${process.env.HOME}/.local/share/whisper-cpp/ggml-base.en.bin`,
|
|
25
|
-
];
|
|
26
|
-
const WHISPER_BIN_CANDIDATES = [
|
|
27
|
-
"/opt/homebrew/bin/whisper-cli", // whisper-cpp brew formula installs as whisper-cli
|
|
28
|
-
"/opt/homebrew/bin/whisper-cpp",
|
|
29
|
-
"/usr/local/bin/whisper-cli",
|
|
30
|
-
"/usr/local/bin/whisper-cpp",
|
|
31
|
-
"/opt/homebrew/bin/whisper",
|
|
32
|
-
];
|
|
33
|
-
const FFMPEG_CANDIDATES = [
|
|
34
|
-
"/opt/homebrew/bin/ffmpeg",
|
|
35
|
-
"/usr/local/bin/ffmpeg",
|
|
36
|
-
"/usr/bin/ffmpeg",
|
|
37
|
-
];
|
|
38
|
-
function findBin(candidates) {
|
|
39
|
-
for (const p of candidates) {
|
|
40
|
-
if (existsSync(p))
|
|
41
|
-
return p;
|
|
42
|
-
}
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
function findModel() {
|
|
46
|
-
for (const p of WHISPER_MODELS) {
|
|
47
|
-
if (existsSync(p))
|
|
48
|
-
return p;
|
|
49
|
-
}
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
function downloadFile(url, dest) {
|
|
53
|
-
return new Promise((resolve, reject) => {
|
|
54
|
-
const file = createWriteStream(dest);
|
|
55
|
-
const getter = url.startsWith("https") ? https : http;
|
|
56
|
-
getter.get(url, (res) => {
|
|
57
|
-
if (res.statusCode !== 200) {
|
|
58
|
-
reject(new Error(`HTTP ${res.statusCode} downloading ${url}`));
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
res.pipe(file);
|
|
62
|
-
file.on("finish", () => file.close(() => resolve()));
|
|
63
|
-
file.on("error", reject);
|
|
64
|
-
}).on("error", reject);
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Transcribe a voice message from a Telegram file URL.
|
|
69
|
-
* Returns the transcribed text, or throws if whisper/ffmpeg not available.
|
|
70
|
-
*/
|
|
71
|
-
export async function transcribeVoice(fileUrl) {
|
|
72
|
-
const whisperBin = findBin(WHISPER_BIN_CANDIDATES);
|
|
73
|
-
if (!whisperBin)
|
|
74
|
-
throw new Error("whisper-cpp not found — install with: brew install whisper-cpp");
|
|
75
|
-
const ffmpegBin = findBin(FFMPEG_CANDIDATES);
|
|
76
|
-
if (!ffmpegBin)
|
|
77
|
-
throw new Error("ffmpeg not found — install with: brew install ffmpeg");
|
|
78
|
-
const model = findModel();
|
|
79
|
-
if (!model)
|
|
80
|
-
throw new Error("No whisper model found — run: whisper-cpp-download-ggml-model small.en");
|
|
81
|
-
const tmp = join(tmpdir(), `cc-tg-voice-${Date.now()}`);
|
|
82
|
-
const oggPath = `${tmp}.ogg`;
|
|
83
|
-
const wavPath = `${tmp}.wav`;
|
|
84
|
-
try {
|
|
85
|
-
// 1. Download OGG from Telegram
|
|
86
|
-
await downloadFile(fileUrl, oggPath);
|
|
87
|
-
// 2. Convert OGG → 16kHz mono WAV (whisper requirement)
|
|
88
|
-
await execFileAsync(ffmpegBin, [
|
|
89
|
-
"-y", "-i", oggPath,
|
|
90
|
-
"-ar", "16000",
|
|
91
|
-
"-ac", "1",
|
|
92
|
-
"-c:a", "pcm_s16le",
|
|
93
|
-
wavPath,
|
|
94
|
-
]);
|
|
95
|
-
// 3. Run whisper-cpp (with one retry on signal-kill)
|
|
96
|
-
const whisperArgs = [
|
|
97
|
-
"-m", model,
|
|
98
|
-
"-f", wavPath,
|
|
99
|
-
"--no-timestamps",
|
|
100
|
-
"-l", "auto",
|
|
101
|
-
"--output-txt",
|
|
102
|
-
];
|
|
103
|
-
let stdout = "";
|
|
104
|
-
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
105
|
-
try {
|
|
106
|
-
const result = await execFileAsync(whisperBin, whisperArgs, { timeout: 60000 });
|
|
107
|
-
stdout = result.stdout;
|
|
108
|
-
break;
|
|
109
|
-
}
|
|
110
|
-
catch (e) {
|
|
111
|
-
// If killed by signal and we have another attempt, retry
|
|
112
|
-
if (attempt < 2 && (e.signal || !e.message)) {
|
|
113
|
-
console.warn(`[voice] whisper attempt ${attempt} failed (${e.signal || 'no message'}), retrying...`);
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
throw e;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
// whisper outputs to stdout — strip leading/trailing whitespace and [BLANK_AUDIO] artifacts
|
|
120
|
-
const text = stdout
|
|
121
|
-
.replace(/\[BLANK_AUDIO\]/gi, "")
|
|
122
|
-
.replace(/\[.*?\]/g, "") // remove timestamp artifacts
|
|
123
|
-
.trim();
|
|
124
|
-
return text || "[empty transcription]";
|
|
125
|
-
}
|
|
126
|
-
finally {
|
|
127
|
-
// Cleanup temp files
|
|
128
|
-
await unlink(oggPath).catch(() => { });
|
|
129
|
-
await unlink(wavPath).catch(() => { });
|
|
130
|
-
await unlink(`${wavPath}.txt`).catch(() => { });
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Check if voice transcription is available on this system.
|
|
135
|
-
*/
|
|
136
|
-
export function isVoiceAvailable() {
|
|
137
|
-
return (findBin(WHISPER_BIN_CANDIDATES) !== null &&
|
|
138
|
-
findBin(FFMPEG_CANDIDATES) !== null &&
|
|
139
|
-
findModel() !== null);
|
|
140
|
-
}
|