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 +7 -0
- package/dist/server.js +45 -0
- package/dist/server.js.map +1 -1
- package/dist/whisper.d.ts +8 -0
- package/dist/whisper.js +24 -0
- package/dist/whisper.js.map +1 -1
- package/package.json +1 -1
- package/public/index.html +163 -0
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
|