shadok-ai 0.16.29 → 0.16.30

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 CHANGED
@@ -191,6 +191,13 @@ Enter to skip; you can add it later from the web UI).
191
191
  `SHADOK_WHISPER_MODEL` picks a bigger one if your recordings need it.
192
192
  Set `SHADOK_WHISPER_DIR` to a directory shared between containers and one copy
193
193
  serves them all.
194
+ - **Dictate a prompt on the web** — a 🎙 button beside *Send* records your
195
+ microphone and transcribes it with the **same local whisper.cpp** the voice
196
+ notes use (nothing leaves the machine). The text drops into the composer
197
+ **editable**, never auto-sent, so a misheard word is fixed before it goes —
198
+ click to start, click to stop, a red timer runs while recording. Needs a
199
+ secure context (localhost or HTTPS) for the browser to grant the mic; the
200
+ first use pays the same one-time whisper install as the voice notes above.
194
201
  - **Videos on Telegram** — share a video (or a GIF, or a round video note) and
195
202
  the agent actually sees it: shadok pulls a handful of stills out of the clip
196
203
  for it to look at, and transcribes the sound when there is speech. Both run
package/dist/server.js CHANGED
@@ -39,6 +39,7 @@ import { getUsage } from "./usage.js";
39
39
  import { loadChannels, loadGroups, saveGroups, upsertChannel, removeChannel, mergeClientChannels, channelWriteAllowed, profilePatch, loadTgGroup, saveTgGroup, isHomeChannel, homeAdoptionTarget, resolveSessionTarget, resolveResumeId, resumeTarget, } from "./channels.js";
40
40
  import { deliverWithRetry } from "./reload.js";
41
41
  import { startTelegram, renameTelegramTopic, closeTelegramTopic, probeToken, isStalePreface, announceLoggedOut, resetLoggedOutNotice, attachmentPrompt, pasteFileName, MEDIA_DIR, } from "./telegram.js";
42
+ import { transcribe, audioClipExtension } from "./whisper.js";
42
43
  import { cancelPairingCode, issuePairingCode, pairingStatus } from "./tg-pairing.js";
43
44
  import { migrateTgBindings } from "./channels.js";
44
45
  import { instanceKey } from "./paths.js";
@@ -1267,6 +1268,50 @@ app.post("/paste", express.raw({ type: () => true, limit: PASTE_LIMIT }), (req,
1267
1268
  res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
1268
1269
  }
1269
1270
  });
1271
+ /**
1272
+ * Speech-to-text for the composer's microphone button. The browser records a
1273
+ * short clip (MediaRecorder → webm/opus) and POSTs the raw bytes here; we run
1274
+ * the SAME local whisper.cpp pipeline the Telegram voice path uses
1275
+ * (`transcribe`) and return the text for the client to drop into the composer.
1276
+ *
1277
+ * The text is NEVER auto-sent — it lands in the editable composer so the user
1278
+ * reads it before acting, because a misheard instruction acted on in silence is
1279
+ * worse than a voice note that was ignored (the reason `heardNotice` exists).
1280
+ *
1281
+ * `requestFromBrowser` like `/paste`: this writes a file and records the mic, a
1282
+ * browser-only capability with no business being reachable by a script on
1283
+ * loopback. The clip is TRANSIENT — written to the OS temp dir and removed in
1284
+ * `finally`, never kept in `MEDIA_DIR`: it is spoken input, not a file to hand
1285
+ * the agent.
1286
+ *
1287
+ * The first call on a fresh instance provisions whisper (ffmpeg / model / build
1288
+ * via `ensureWhisper` inside `transcribe`), which can take minutes; the client
1289
+ * shows a working state and we simply wait. The French whisper notices are kept
1290
+ * server-side — the client maps `kind` to its own English UI copy, so the web
1291
+ * stays English while Telegram keeps its spoken-language notices.
1292
+ */
1293
+ app.post("/transcribe", express.raw({ type: () => true, limit: PASTE_LIMIT }), async (req, res) => {
1294
+ if (!requestFromBrowser(req))
1295
+ return res.status(403).json({ error: "same-origin browser only" });
1296
+ const body = req.body;
1297
+ if (!Buffer.isBuffer(body) || !body.length)
1298
+ return res.status(400).json({ error: "empty body" });
1299
+ const ext = audioClipExtension(String(req.headers["content-type"] ?? ""));
1300
+ const tmp = path.join(os.tmpdir(), `shadok-stt-${randomUUID()}.${ext}`);
1301
+ try {
1302
+ await fs.promises.writeFile(tmp, body);
1303
+ const out = await transcribe(tmp);
1304
+ if (out.ok)
1305
+ return res.json({ ok: true, text: out.text });
1306
+ return res.json({ ok: false, kind: out.kind });
1307
+ }
1308
+ catch (e) {
1309
+ return res.status(500).json({ ok: false, error: e instanceof Error ? e.message : String(e) });
1310
+ }
1311
+ finally {
1312
+ await fs.promises.rm(tmp, { force: true }).catch(() => { });
1313
+ }
1314
+ });
1270
1315
  app.use(express.static(path.join(__dirname, "..", "public")));
1271
1316
  // Markdown parser served locally (history rendering on the client).
1272
1317
  // Resolved through require.resolve, not a hardcoded path: in the npm install