@rubytech/taskmaster 1.2.1 → 1.4.0
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/dist/agents/auth-profiles/oauth.js +24 -0
- package/dist/agents/auth-profiles/profiles.js +37 -0
- package/dist/agents/auth-profiles.js +1 -1
- package/dist/agents/pi-tools.policy.js +4 -0
- package/dist/agents/taskmaster-tools.js +14 -0
- package/dist/agents/tool-policy.js +5 -2
- package/dist/agents/tools/apikeys-tool.js +16 -5
- package/dist/agents/tools/contact-create-tool.js +59 -0
- package/dist/agents/tools/contact-delete-tool.js +48 -0
- package/dist/agents/tools/contact-update-tool.js +17 -2
- package/dist/agents/tools/file-delete-tool.js +137 -0
- package/dist/agents/tools/file-list-tool.js +127 -0
- package/dist/agents/tools/message-history-tool.js +2 -3
- package/dist/auto-reply/media-note.js +11 -0
- package/dist/auto-reply/reply/commands-tts.js +7 -2
- package/dist/auto-reply/reply/get-reply.js +4 -0
- package/dist/build-info.json +3 -3
- package/dist/cli/provision-seed.js +1 -2
- package/dist/commands/doctor-config-flow.js +13 -0
- package/dist/config/agent-tools-reconcile.js +53 -0
- package/dist/config/defaults.js +10 -1
- package/dist/config/legacy.migrations.part-3.js +26 -0
- package/dist/config/zod-schema.core.js +9 -1
- package/dist/config/zod-schema.js +1 -0
- package/dist/control-ui/assets/{index-N8du4fwV.js → index-BDETQp97.js} +692 -600
- package/dist/control-ui/assets/index-BDETQp97.js.map +1 -0
- package/dist/control-ui/assets/index-CPawOl_z.css +1 -0
- package/dist/control-ui/index.html +2 -2
- package/dist/gateway/chat-sanitize.js +5 -1
- package/dist/gateway/config-reload.js +1 -0
- package/dist/gateway/media-http.js +28 -0
- package/dist/gateway/server/tls.js +2 -2
- package/dist/gateway/server-http.js +34 -4
- package/dist/gateway/server-methods/apikeys.js +56 -4
- package/dist/gateway/server-methods/chat.js +64 -25
- package/dist/gateway/server-methods/tts.js +11 -2
- package/dist/gateway/server.impl.js +38 -5
- package/dist/infra/tls/gateway.js +19 -3
- package/dist/media-understanding/apply.js +35 -0
- package/dist/media-understanding/providers/deepgram/audio.js +1 -1
- package/dist/media-understanding/providers/google/audio.js +1 -1
- package/dist/media-understanding/providers/google/video.js +1 -1
- package/dist/media-understanding/providers/index.js +2 -0
- package/dist/media-understanding/providers/openai/audio.js +1 -1
- package/dist/media-understanding/providers/sherpa-onnx/index.js +10 -0
- package/dist/media-understanding/runner.js +61 -72
- package/dist/media-understanding/sherpa-onnx-local.js +223 -0
- package/dist/memory/audit.js +9 -0
- package/dist/memory/manager.js +1 -1
- package/dist/records/records-manager.js +10 -0
- package/dist/tts/tts.js +98 -10
- package/dist/web/auto-reply/monitor/process-message.js +45 -17
- package/dist/web/inbound/monitor.js +9 -1
- package/extensions/diagnostics-otel/node_modules/.bin/acorn +0 -0
- package/extensions/googlechat/node_modules/.bin/taskmaster +2 -2
- package/extensions/googlechat/package.json +2 -2
- package/extensions/line/node_modules/.bin/taskmaster +2 -2
- package/extensions/line/package.json +1 -1
- package/extensions/matrix/node_modules/.bin/markdown-it +0 -0
- package/extensions/matrix/node_modules/.bin/taskmaster +2 -2
- package/extensions/matrix/package.json +1 -1
- package/extensions/memory-lancedb/node_modules/.bin/arrow2csv +0 -0
- package/extensions/memory-lancedb/node_modules/.bin/openai +0 -0
- package/extensions/msteams/node_modules/.bin/taskmaster +2 -2
- package/extensions/msteams/package.json +1 -1
- package/extensions/nostr/node_modules/.bin/taskmaster +2 -2
- package/extensions/nostr/node_modules/.bin/tsc +0 -0
- package/extensions/nostr/node_modules/.bin/tsserver +0 -0
- package/extensions/nostr/package.json +1 -1
- package/extensions/zalo/node_modules/.bin/taskmaster +2 -2
- package/extensions/zalo/package.json +1 -1
- package/extensions/zalouser/node_modules/.bin/taskmaster +2 -2
- package/extensions/zalouser/package.json +1 -1
- package/package.json +56 -65
- package/scripts/install.sh +0 -0
- package/scripts/postinstall.js +76 -0
- package/skills/business-assistant/references/crm.md +32 -8
- package/taskmaster-docs/USER-GUIDE.md +111 -6
- package/templates/.DS_Store +0 -0
- package/templates/beagle/agents/admin/AGENTS.md +4 -2
- package/templates/customer/.DS_Store +0 -0
- package/templates/customer/agents/.DS_Store +0 -0
- package/templates/maxy/.DS_Store +0 -0
- package/templates/maxy/.gitignore +1 -0
- package/templates/maxy/agents/.DS_Store +0 -0
- package/templates/maxy/agents/admin/.DS_Store +0 -0
- package/templates/maxy/memory/.DS_Store +0 -0
- package/templates/maxy/skills/.DS_Store +0 -0
- package/templates/taskmaster/.gitignore +1 -0
- package/templates/taskmaster/agents/admin/AGENTS.md +1 -0
- package/dist/control-ui/assets/index-DtQHRIVD.css +0 -1
- package/dist/control-ui/assets/index-N8du4fwV.js.map +0 -1
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { findModelInCatalog, loadModelCatalog, modelSupportsVision, } from "../agents/model-catalog.js";
|
|
6
6
|
import { applyTemplate } from "../auto-reply/templating.js";
|
|
7
7
|
import { requireApiKey, resolveApiKeyForProvider } from "../agents/model-auth.js";
|
|
8
|
-
import {
|
|
8
|
+
import { createSubsystemLogger } from "../logging/subsystem.js";
|
|
9
9
|
import { runExec } from "../process/exec.js";
|
|
10
10
|
import { MediaAttachmentCache, normalizeAttachments, selectAttachments } from "./attachments.js";
|
|
11
11
|
import { CLI_OUTPUT_MAX_BUFFER, DEFAULT_AUDIO_MODELS, DEFAULT_TIMEOUT_SECONDS, } from "./defaults.js";
|
|
@@ -23,6 +23,7 @@ const DEFAULT_IMAGE_MODELS = {
|
|
|
23
23
|
google: "gemini-3-flash-preview",
|
|
24
24
|
minimax: "MiniMax-VL-01",
|
|
25
25
|
};
|
|
26
|
+
const log = createSubsystemLogger("gateway/media");
|
|
26
27
|
export function buildProviderRegistry(overrides) {
|
|
27
28
|
return buildMediaUnderstandingRegistry(overrides);
|
|
28
29
|
}
|
|
@@ -33,7 +34,6 @@ export function createMediaAttachmentCache(attachments) {
|
|
|
33
34
|
return new MediaAttachmentCache(attachments);
|
|
34
35
|
}
|
|
35
36
|
const binaryCache = new Map();
|
|
36
|
-
const geminiProbeCache = new Map();
|
|
37
37
|
function expandHomeDir(value) {
|
|
38
38
|
if (!value.startsWith("~"))
|
|
39
39
|
return value;
|
|
@@ -181,26 +181,6 @@ function extractSherpaOnnxText(raw) {
|
|
|
181
181
|
}
|
|
182
182
|
return null;
|
|
183
183
|
}
|
|
184
|
-
async function probeGeminiCli() {
|
|
185
|
-
const cached = geminiProbeCache.get("gemini");
|
|
186
|
-
if (cached)
|
|
187
|
-
return cached;
|
|
188
|
-
const resolved = (async () => {
|
|
189
|
-
if (!(await hasBinary("gemini")))
|
|
190
|
-
return false;
|
|
191
|
-
try {
|
|
192
|
-
const { stdout } = await runExec("gemini", ["--output-format", "json", "ok"], {
|
|
193
|
-
timeoutMs: 8000,
|
|
194
|
-
});
|
|
195
|
-
return Boolean(extractGeminiResponse(stdout) ?? stdout.toLowerCase().includes("ok"));
|
|
196
|
-
}
|
|
197
|
-
catch {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
})();
|
|
201
|
-
geminiProbeCache.set("gemini", resolved);
|
|
202
|
-
return resolved;
|
|
203
|
-
}
|
|
204
184
|
async function resolveLocalWhisperCppEntry() {
|
|
205
185
|
if (!(await hasBinary("whisper-cli")))
|
|
206
186
|
return null;
|
|
@@ -234,7 +214,34 @@ async function resolveLocalWhisperEntry() {
|
|
|
234
214
|
],
|
|
235
215
|
};
|
|
236
216
|
}
|
|
237
|
-
|
|
217
|
+
/**
|
|
218
|
+
* Check if sherpa-onnx-node (npm package) is available with model + ffmpeg.
|
|
219
|
+
* Returns a provider entry so the pipeline uses the Node.js API directly
|
|
220
|
+
* (no CLI binary or SHERPA_ONNX_MODEL_DIR env var required).
|
|
221
|
+
*/
|
|
222
|
+
async function resolveSherpaOnnxNodeEntry() {
|
|
223
|
+
try {
|
|
224
|
+
const { isReady } = await import("./sherpa-onnx-local.js");
|
|
225
|
+
if (await isReady()) {
|
|
226
|
+
return { type: "provider", provider: "sherpa-onnx" };
|
|
227
|
+
}
|
|
228
|
+
// Package + ffmpeg available but model not yet downloaded — still viable
|
|
229
|
+
// (the provider will trigger a lazy download on first use)
|
|
230
|
+
const { isAvailable } = await import("./sherpa-onnx-local.js");
|
|
231
|
+
if (await isAvailable()) {
|
|
232
|
+
return { type: "provider", provider: "sherpa-onnx" };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// sherpa-onnx-node not installed — skip
|
|
237
|
+
}
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Fallback: check for sherpa-onnx-offline CLI binary + SHERPA_ONNX_MODEL_DIR env var.
|
|
242
|
+
* This is the legacy detection path for users who installed the binary manually.
|
|
243
|
+
*/
|
|
244
|
+
async function resolveSherpaOnnxCliEntry() {
|
|
238
245
|
if (!(await hasBinary("sherpa-onnx-offline")))
|
|
239
246
|
return null;
|
|
240
247
|
const modelDir = process.env.SHERPA_ONNX_MODEL_DIR?.trim();
|
|
@@ -265,32 +272,19 @@ async function resolveSherpaOnnxEntry() {
|
|
|
265
272
|
};
|
|
266
273
|
}
|
|
267
274
|
async function resolveLocalAudioEntry() {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
275
|
+
// Prefer sherpa-onnx-node (npm, no PATH issues, automatic model management)
|
|
276
|
+
const sherpaNode = await resolveSherpaOnnxNodeEntry();
|
|
277
|
+
if (sherpaNode)
|
|
278
|
+
return sherpaNode;
|
|
279
|
+
// Fallback: CLI binary (legacy/manual installs)
|
|
280
|
+
const sherpaCli = await resolveSherpaOnnxCliEntry();
|
|
281
|
+
if (sherpaCli)
|
|
282
|
+
return sherpaCli;
|
|
271
283
|
const whisperCpp = await resolveLocalWhisperCppEntry();
|
|
272
284
|
if (whisperCpp)
|
|
273
285
|
return whisperCpp;
|
|
274
286
|
return await resolveLocalWhisperEntry();
|
|
275
287
|
}
|
|
276
|
-
async function resolveGeminiCliEntry(_capability) {
|
|
277
|
-
if (!(await probeGeminiCli()))
|
|
278
|
-
return null;
|
|
279
|
-
return {
|
|
280
|
-
type: "cli",
|
|
281
|
-
command: "gemini",
|
|
282
|
-
args: [
|
|
283
|
-
"--output-format",
|
|
284
|
-
"json",
|
|
285
|
-
"--allowed-tools",
|
|
286
|
-
"read_many_files",
|
|
287
|
-
"--include-directories",
|
|
288
|
-
"{{MediaDir}}",
|
|
289
|
-
"{{Prompt}}",
|
|
290
|
-
"Use read_many_files to read {{MediaPath}} and respond with only the text output.",
|
|
291
|
-
],
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
288
|
async function resolveKeyEntry(params) {
|
|
295
289
|
const { cfg, agentDir, providerRegistry, capability } = params;
|
|
296
290
|
const checkProvider = async (providerId, model) => {
|
|
@@ -362,9 +356,6 @@ async function resolveAutoEntries(params) {
|
|
|
362
356
|
if (localAudio)
|
|
363
357
|
return [localAudio];
|
|
364
358
|
}
|
|
365
|
-
const gemini = await resolveGeminiCliEntry(params.capability);
|
|
366
|
-
if (gemini)
|
|
367
|
-
return [gemini];
|
|
368
359
|
const keys = await resolveKeyEntry(params);
|
|
369
360
|
if (keys)
|
|
370
361
|
return [keys];
|
|
@@ -635,14 +626,18 @@ async function runProviderEntry(params) {
|
|
|
635
626
|
maxBytes,
|
|
636
627
|
timeoutMs,
|
|
637
628
|
});
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
629
|
+
// Local providers (e.g. sherpa-onnx) do not require an API key.
|
|
630
|
+
let apiKey;
|
|
631
|
+
if (!provider.isLocal) {
|
|
632
|
+
const auth = await resolveApiKeyForProvider({
|
|
633
|
+
provider: providerId,
|
|
634
|
+
cfg,
|
|
635
|
+
profileId: entry.profile,
|
|
636
|
+
preferredProfile: entry.preferredProfile,
|
|
637
|
+
agentDir: params.agentDir,
|
|
638
|
+
});
|
|
639
|
+
apiKey = requireApiKey(auth, providerId);
|
|
640
|
+
}
|
|
646
641
|
const providerConfig = cfg.models?.providers?.[providerId];
|
|
647
642
|
const baseUrl = entry.baseUrl ?? params.config?.baseUrl ?? providerConfig?.baseUrl;
|
|
648
643
|
const mergedHeaders = {
|
|
@@ -751,9 +746,7 @@ async function runCliEntry(params) {
|
|
|
751
746
|
};
|
|
752
747
|
const argv = [command, ...args].map((part, index) => index === 0 ? part : applyTemplate(part, templCtx));
|
|
753
748
|
try {
|
|
754
|
-
|
|
755
|
-
logVerbose(`Media understanding via CLI: ${argv.join(" ")}`);
|
|
756
|
-
}
|
|
749
|
+
log.debug(`CLI: ${argv.join(" ")}`);
|
|
757
750
|
const { stdout } = await runExec(argv[0], argv.slice(1), {
|
|
758
751
|
timeoutMs,
|
|
759
752
|
maxBuffer: CLI_OUTPUT_MAX_BUFFER,
|
|
@@ -825,9 +818,7 @@ async function runAttachmentEntries(params) {
|
|
|
825
818
|
outcome: "skipped",
|
|
826
819
|
reason: `${err.reason}: ${err.message}`,
|
|
827
820
|
}));
|
|
828
|
-
|
|
829
|
-
logVerbose(`Skipping ${capability} model due to ${err.reason}: ${err.message}`);
|
|
830
|
-
}
|
|
821
|
+
log.debug(`Skipping ${capability} model: ${err.reason}: ${err.message}`);
|
|
831
822
|
continue;
|
|
832
823
|
}
|
|
833
824
|
attempts.push(buildModelDecision({
|
|
@@ -836,9 +827,7 @@ async function runAttachmentEntries(params) {
|
|
|
836
827
|
outcome: "failed",
|
|
837
828
|
reason: String(err),
|
|
838
829
|
}));
|
|
839
|
-
|
|
840
|
-
logVerbose(`${capability} understanding failed: ${String(err)}`);
|
|
841
|
-
}
|
|
830
|
+
log.error(`${capability} failed: ${String(err)}`);
|
|
842
831
|
}
|
|
843
832
|
}
|
|
844
833
|
return { output: null, attempts };
|
|
@@ -866,9 +855,7 @@ export async function runCapability(params) {
|
|
|
866
855
|
}
|
|
867
856
|
const scopeDecision = resolveScopeDecision({ scope: config?.scope, ctx });
|
|
868
857
|
if (scopeDecision === "deny") {
|
|
869
|
-
|
|
870
|
-
logVerbose(`${capability} understanding disabled by scope policy.`);
|
|
871
|
-
}
|
|
858
|
+
log.debug(`${capability} disabled by scope policy`);
|
|
872
859
|
return {
|
|
873
860
|
outputs: [],
|
|
874
861
|
decision: {
|
|
@@ -885,9 +872,7 @@ export async function runCapability(params) {
|
|
|
885
872
|
const catalog = await loadModelCatalog({ config: cfg });
|
|
886
873
|
const entry = findModelInCatalog(catalog, activeProvider, params.activeModel?.model ?? "");
|
|
887
874
|
if (modelSupportsVision(entry)) {
|
|
888
|
-
|
|
889
|
-
logVerbose("Skipping image understanding: primary model supports vision natively");
|
|
890
|
-
}
|
|
875
|
+
log.debug("Skipping image understanding: primary model supports vision natively");
|
|
891
876
|
const model = params.activeModel?.model?.trim();
|
|
892
877
|
const reason = "primary model supports vision natively";
|
|
893
878
|
return {
|
|
@@ -966,8 +951,12 @@ export async function runCapability(params) {
|
|
|
966
951
|
outcome: outputs.length > 0 ? "success" : "skipped",
|
|
967
952
|
attachments: attachmentDecisions,
|
|
968
953
|
};
|
|
969
|
-
|
|
970
|
-
|
|
954
|
+
const summary = formatDecisionSummary(decision);
|
|
955
|
+
if (decision.outcome === "success") {
|
|
956
|
+
log.info(summary);
|
|
957
|
+
}
|
|
958
|
+
else {
|
|
959
|
+
log.debug(summary);
|
|
971
960
|
}
|
|
972
961
|
return {
|
|
973
962
|
outputs,
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local speech-to-text using sherpa-onnx-node (optional dependency).
|
|
3
|
+
*
|
|
4
|
+
* Model: NeMo CTC English Conformer Small (int8)
|
|
5
|
+
* Storage: ~/.taskmaster/lib/sherpa-onnx/nemo-ctc-en-conformer-small/
|
|
6
|
+
*
|
|
7
|
+
* Requires: sherpa-onnx-node (optional dep), ffmpeg (for OGG→WAV conversion)
|
|
8
|
+
*/
|
|
9
|
+
import { execFile as execFileCb, spawnSync } from "node:child_process";
|
|
10
|
+
import fs from "node:fs/promises";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { promisify } from "node:util";
|
|
14
|
+
const execFile = promisify(execFileCb);
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Constants
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
const LIB_DIR = path.join(os.homedir(), ".taskmaster", "lib", "sherpa-onnx");
|
|
19
|
+
const MODEL_DIR_NAME = "nemo-ctc-en-conformer-small";
|
|
20
|
+
const MODEL_DIR = path.join(LIB_DIR, MODEL_DIR_NAME);
|
|
21
|
+
const MODEL_FILE = "model.int8.onnx";
|
|
22
|
+
const TOKENS_FILE = "tokens.txt";
|
|
23
|
+
const MODEL_ARCHIVE_URL = "https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-nemo-ctc-en-conformer-small.tar.bz2";
|
|
24
|
+
const ARCHIVE_INNER_DIR = "sherpa-onnx-nemo-ctc-en-conformer-small";
|
|
25
|
+
export const MODEL_LABEL = "sherpa-onnx/nemo-ctc-en-conformer-small-int8";
|
|
26
|
+
let sherpaModule = null;
|
|
27
|
+
let sherpaModuleError = null;
|
|
28
|
+
async function importSherpaOnnx() {
|
|
29
|
+
if (sherpaModule)
|
|
30
|
+
return sherpaModule;
|
|
31
|
+
if (sherpaModuleError)
|
|
32
|
+
throw sherpaModuleError;
|
|
33
|
+
try {
|
|
34
|
+
const mod = await import("sherpa-onnx-node");
|
|
35
|
+
// ESM dynamic import puts named exports under .default
|
|
36
|
+
sherpaModule = (mod.default ?? mod);
|
|
37
|
+
return sherpaModule;
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
sherpaModuleError = err instanceof Error ? err : new Error(String(err));
|
|
41
|
+
throw sherpaModuleError;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// Availability checks
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
let ffmpegAvailable = null;
|
|
48
|
+
function checkFfmpeg() {
|
|
49
|
+
if (ffmpegAvailable !== null)
|
|
50
|
+
return ffmpegAvailable;
|
|
51
|
+
const result = spawnSync("ffmpeg", ["-version"], { stdio: "ignore", timeout: 5_000 });
|
|
52
|
+
ffmpegAvailable = result.status === 0;
|
|
53
|
+
return ffmpegAvailable;
|
|
54
|
+
}
|
|
55
|
+
async function fileExists(filePath) {
|
|
56
|
+
try {
|
|
57
|
+
await fs.stat(filePath);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export async function isModelDownloaded() {
|
|
65
|
+
return ((await fileExists(path.join(MODEL_DIR, MODEL_FILE))) &&
|
|
66
|
+
(await fileExists(path.join(MODEL_DIR, TOKENS_FILE))));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Returns true when both the npm package and ffmpeg are available.
|
|
70
|
+
* Does NOT check whether the model is downloaded (use isReady for that).
|
|
71
|
+
*/
|
|
72
|
+
export async function isAvailable() {
|
|
73
|
+
try {
|
|
74
|
+
await importSherpaOnnx();
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return checkFfmpeg();
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Returns true when everything is ready for immediate transcription:
|
|
83
|
+
* npm package + ffmpeg + model files on disk.
|
|
84
|
+
*/
|
|
85
|
+
export async function isReady() {
|
|
86
|
+
return (await isAvailable()) && (await isModelDownloaded());
|
|
87
|
+
}
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Model download
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
let downloadInFlight = null;
|
|
92
|
+
/**
|
|
93
|
+
* Download and extract the model if not already present.
|
|
94
|
+
* Deduplicates concurrent calls.
|
|
95
|
+
*/
|
|
96
|
+
export async function ensureModel() {
|
|
97
|
+
if (await isModelDownloaded())
|
|
98
|
+
return MODEL_DIR;
|
|
99
|
+
if (!downloadInFlight) {
|
|
100
|
+
downloadInFlight = downloadModelOnce().finally(() => {
|
|
101
|
+
downloadInFlight = null;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
await downloadInFlight;
|
|
105
|
+
return MODEL_DIR;
|
|
106
|
+
}
|
|
107
|
+
async function downloadModelOnce() {
|
|
108
|
+
console.log("[sherpa-onnx] Downloading speech recognition model…");
|
|
109
|
+
await fs.mkdir(LIB_DIR, { recursive: true });
|
|
110
|
+
const archivePath = path.join(LIB_DIR, `${MODEL_DIR_NAME}.tar.bz2`);
|
|
111
|
+
try {
|
|
112
|
+
// Download archive using curl (available on macOS and Linux)
|
|
113
|
+
await execFile("curl", ["-fsSL", "-o", archivePath, MODEL_ARCHIVE_URL], {
|
|
114
|
+
timeout: 300_000,
|
|
115
|
+
});
|
|
116
|
+
// Extract — archive contains sherpa-onnx-nemo-ctc-en-conformer-small/{files}
|
|
117
|
+
await execFile("tar", ["-xjf", archivePath, "-C", LIB_DIR], {
|
|
118
|
+
timeout: 120_000,
|
|
119
|
+
});
|
|
120
|
+
// Rename extracted directory to our standard name (remove sherpa-onnx- prefix)
|
|
121
|
+
const extractedDir = path.join(LIB_DIR, ARCHIVE_INNER_DIR);
|
|
122
|
+
if ((await fileExists(extractedDir)) && extractedDir !== MODEL_DIR) {
|
|
123
|
+
// If target already exists (partial state), remove it first
|
|
124
|
+
if (await fileExists(MODEL_DIR)) {
|
|
125
|
+
await fs.rm(MODEL_DIR, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
await fs.rename(extractedDir, MODEL_DIR);
|
|
128
|
+
}
|
|
129
|
+
// Clean up: remove archive and unnecessary files to save disk space
|
|
130
|
+
await fs.unlink(archivePath).catch(() => { });
|
|
131
|
+
// Remove full-precision model (keep only int8 — saves ~37MB)
|
|
132
|
+
await fs.unlink(path.join(MODEL_DIR, "model.onnx")).catch(() => { });
|
|
133
|
+
// Remove test wav files
|
|
134
|
+
await fs
|
|
135
|
+
.rm(path.join(MODEL_DIR, "test_wavs"), { recursive: true, force: true })
|
|
136
|
+
.catch(() => { });
|
|
137
|
+
console.log("[sherpa-onnx] Model installed");
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
// Clean up partial download
|
|
141
|
+
await fs.unlink(archivePath).catch(() => { });
|
|
142
|
+
await fs.rm(MODEL_DIR, { recursive: true, force: true }).catch(() => { });
|
|
143
|
+
throw new Error(`sherpa-onnx model download failed: ${String(err)}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// Audio conversion (OGG/Opus → WAV 16kHz mono)
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
async function convertToWav(inputPath, outputPath) {
|
|
150
|
+
await execFile("ffmpeg", [
|
|
151
|
+
"-i",
|
|
152
|
+
inputPath,
|
|
153
|
+
"-ar",
|
|
154
|
+
"16000",
|
|
155
|
+
"-ac",
|
|
156
|
+
"1",
|
|
157
|
+
"-sample_fmt",
|
|
158
|
+
"s16",
|
|
159
|
+
"-f",
|
|
160
|
+
"wav",
|
|
161
|
+
"-y",
|
|
162
|
+
outputPath,
|
|
163
|
+
], { timeout: 30_000 });
|
|
164
|
+
}
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// Recognizer (cached singleton)
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
let recognizer = null;
|
|
169
|
+
async function getRecognizer() {
|
|
170
|
+
if (recognizer)
|
|
171
|
+
return recognizer;
|
|
172
|
+
const sherpa = await importSherpaOnnx();
|
|
173
|
+
const modelDir = await ensureModel();
|
|
174
|
+
const config = {
|
|
175
|
+
featConfig: { sampleRate: 16000, featureDim: 80 },
|
|
176
|
+
modelConfig: {
|
|
177
|
+
nemoCtc: {
|
|
178
|
+
model: path.join(modelDir, MODEL_FILE),
|
|
179
|
+
},
|
|
180
|
+
tokens: path.join(modelDir, TOKENS_FILE),
|
|
181
|
+
numThreads: 2,
|
|
182
|
+
provider: "cpu",
|
|
183
|
+
debug: 0,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
recognizer = new sherpa.OfflineRecognizer(config);
|
|
187
|
+
return recognizer;
|
|
188
|
+
}
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Public transcription API
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
/**
|
|
193
|
+
* Transcribe an audio buffer (any format ffmpeg can read) to text.
|
|
194
|
+
* Returns the transcription text, or throws on failure.
|
|
195
|
+
*/
|
|
196
|
+
export async function transcribeLocal(buffer, fileName) {
|
|
197
|
+
const sherpa = await importSherpaOnnx();
|
|
198
|
+
const rec = await getRecognizer();
|
|
199
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "sherpa-stt-"));
|
|
200
|
+
try {
|
|
201
|
+
const inputPath = path.join(tmpDir, fileName || "audio.ogg");
|
|
202
|
+
await fs.writeFile(inputPath, buffer);
|
|
203
|
+
const wavPath = path.join(tmpDir, "audio.wav");
|
|
204
|
+
await convertToWav(inputPath, wavPath);
|
|
205
|
+
const wave = sherpa.readWave(wavPath);
|
|
206
|
+
const stream = rec.createStream();
|
|
207
|
+
stream.acceptWaveform({ sampleRate: wave.sampleRate, samples: wave.samples });
|
|
208
|
+
rec.decode(stream);
|
|
209
|
+
const result = rec.getResult(stream);
|
|
210
|
+
const text = result?.text?.trim() ?? "";
|
|
211
|
+
if (!text) {
|
|
212
|
+
throw new Error("sherpa-onnx returned empty transcription");
|
|
213
|
+
}
|
|
214
|
+
return { text, model: MODEL_LABEL };
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Exported paths (for postinstall and diagnostics)
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
export { LIB_DIR, MODEL_DIR, MODEL_DIR_NAME, MODEL_FILE, TOKENS_FILE, MODEL_ARCHIVE_URL, ARCHIVE_INNER_DIR, };
|
package/dist/memory/audit.js
CHANGED
|
@@ -55,11 +55,20 @@ function writeAuditFile(workspaceDir, data) {
|
|
|
55
55
|
// Audit is best-effort — don't fail writes over audit persistence
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
/** Deduplicate window: ignore a second write to the same path by the same agent within this period. */
|
|
59
|
+
const DEDUP_WINDOW_MS = 60_000;
|
|
58
60
|
/**
|
|
59
61
|
* Record an audit entry for a memory write.
|
|
62
|
+
* Skips the entry if an identical path+agent combination was recorded within the dedup window.
|
|
60
63
|
*/
|
|
61
64
|
export function recordAuditEntry(workspaceDir, entry) {
|
|
62
65
|
const audit = readAuditFile(workspaceDir);
|
|
66
|
+
// Deduplicate: skip if same path + agent recorded within the last DEDUP_WINDOW_MS
|
|
67
|
+
const dominated = audit.entries.some((e) => e.path === entry.path &&
|
|
68
|
+
e.agentId === entry.agentId &&
|
|
69
|
+
entry.timestamp - e.timestamp < DEDUP_WINDOW_MS);
|
|
70
|
+
if (dominated)
|
|
71
|
+
return;
|
|
63
72
|
audit.entries.push(entry);
|
|
64
73
|
// Cap at 500 entries to prevent unbounded growth.
|
|
65
74
|
if (audit.entries.length > 500) {
|
package/dist/memory/manager.js
CHANGED
|
@@ -1245,7 +1245,7 @@ export class MemoryIndexManager {
|
|
|
1245
1245
|
}
|
|
1246
1246
|
const action = record ? "updated" : "added";
|
|
1247
1247
|
log.info(`file ${action} (${this.agentId}): ${entry.path}`);
|
|
1248
|
-
if (isAuditablePath(entry.path)) {
|
|
1248
|
+
if (isAuditablePath(entry.path) && record?.hash !== entry.hash) {
|
|
1249
1249
|
recordAuditEntry(this.workspaceDir, {
|
|
1250
1250
|
path: entry.path,
|
|
1251
1251
|
timestamp: Date.now(),
|
|
@@ -90,3 +90,13 @@ export function deleteRecordField(id, key) {
|
|
|
90
90
|
writeFile(data);
|
|
91
91
|
return record;
|
|
92
92
|
}
|
|
93
|
+
export function setRecordName(id, name) {
|
|
94
|
+
const data = readFile();
|
|
95
|
+
const record = data.records[id];
|
|
96
|
+
if (!record)
|
|
97
|
+
return null;
|
|
98
|
+
record.name = name;
|
|
99
|
+
record.updatedAt = new Date().toISOString();
|
|
100
|
+
writeFile(data);
|
|
101
|
+
return record;
|
|
102
|
+
}
|