niahere 0.5.13 → 0.5.14
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/package.json +1 -1
- package/src/channels/telegram.ts +48 -0
package/package.json
CHANGED
package/src/channels/telegram.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { classifyMime, validateAttachment, prepareImage } from "../utils/attachm
|
|
|
13
13
|
import { getNiaHome } from "../utils/paths";
|
|
14
14
|
import { ChatSessions, chainLock } from "./common/chat-session";
|
|
15
15
|
import { shouldSuppressReply } from "./common/reply";
|
|
16
|
+
import { transcribeAudio } from "./twilio/transcribe";
|
|
16
17
|
|
|
17
18
|
function safeExtension(filename?: string): string {
|
|
18
19
|
const ext = filename?.split(".").pop();
|
|
@@ -51,10 +52,24 @@ class TelegramChannel implements Channel {
|
|
|
51
52
|
bot.on("message:text", (ctx) => this.handleText(ctx));
|
|
52
53
|
bot.on("message:photo", (ctx) => this.handlePhoto(ctx));
|
|
53
54
|
bot.on("message:document", (ctx) => this.handleDocument(ctx));
|
|
55
|
+
bot.on(["message:voice", "message:audio"], (ctx) => this.handleVoice(ctx));
|
|
56
|
+
|
|
57
|
+
// Without a handler grammY rethrows, and an error thrown while polling
|
|
58
|
+
// takes the whole loop down rather than the one update that caused it.
|
|
59
|
+
bot.catch((err) => log.error({ err: err.error, chatId: err.ctx.chatId }, "telegram handler failed"));
|
|
54
60
|
|
|
55
61
|
bot.start({ onStart: () => log.info("telegram bot polling started") });
|
|
56
62
|
}
|
|
57
63
|
|
|
64
|
+
/**
|
|
65
|
+
* grammY stops polling for good if the loop crashes, and the process carries
|
|
66
|
+
* on regardless. `isRunning()` is its own account of whether that has
|
|
67
|
+
* happened, so ask it rather than inferring from the outside.
|
|
68
|
+
*/
|
|
69
|
+
healthy(): boolean {
|
|
70
|
+
return !this.bot || this.bot.isRunning();
|
|
71
|
+
}
|
|
72
|
+
|
|
58
73
|
async stop(): Promise<void> {
|
|
59
74
|
this.chats.closeAll();
|
|
60
75
|
if (this.bot) {
|
|
@@ -168,6 +183,39 @@ class TelegramChannel implements Channel {
|
|
|
168
183
|
});
|
|
169
184
|
}
|
|
170
185
|
|
|
186
|
+
private async handleVoice(ctx: Context): Promise<void> {
|
|
187
|
+
const media = ctx.message?.voice ?? ctx.message?.audio;
|
|
188
|
+
if (!ctx.chatId || !media || !this.gate(ctx)) return;
|
|
189
|
+
this.registerOutbound(ctx.chatId);
|
|
190
|
+
const state = await this.chats.get(ctx.chatId);
|
|
191
|
+
const chatId = ctx.chatId;
|
|
192
|
+
this.withLock(chatId, async () => {
|
|
193
|
+
try {
|
|
194
|
+
const apiKey = getConfig().channels.phone.openai_api_key;
|
|
195
|
+
if (!apiKey) {
|
|
196
|
+
await ctx.reply("Can't transcribe voice notes — channels.phone.openai_api_key isn't set.");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const data = await this.downloadFile(media.file_id);
|
|
200
|
+
const error = validateAttachment(data);
|
|
201
|
+
if (error) {
|
|
202
|
+
await ctx.reply(error);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const transcript = await transcribeAudio({ apiKey, data, mime: media.mime_type || "audio/ogg" });
|
|
206
|
+
if (!transcript) {
|
|
207
|
+
await ctx.reply("Couldn't make out anything in that voice note.");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const caption = ctx.message!.caption;
|
|
211
|
+
await this.processMessage(ctx, state, caption ? `${caption}\n\n${transcript}` : transcript);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
log.error({ err, chatId }, "failed to process voice note");
|
|
214
|
+
await ignore(ctx.reply(`Failed to transcribe that voice note — ${errMsg(err)}`), "reply voice failure");
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
171
219
|
// --- Core message loop ---
|
|
172
220
|
|
|
173
221
|
private async processMessage(
|