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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "niahere",
3
- "version": "0.5.13",
3
+ "version": "0.5.14",
4
4
  "description": "A personal AI assistant daemon — chat, scheduled jobs, persona system, extensible via skills.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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(