@ouro.bot/cli 0.1.0-alpha.26 → 0.1.0-alpha.28
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/changelog.json +15 -0
- package/dist/heart/daemon/agent-discovery.js +81 -0
- package/dist/heart/daemon/daemon-cli.js +6 -30
- package/dist/heart/daemon/daemon-entry.js +2 -1
- package/dist/heart/daemon/wrapper-publish-guard.js +41 -3
- package/dist/heart/identity.js +8 -0
- package/dist/mind/first-impressions.js +2 -1
- package/dist/mind/phrases.js +1 -0
- package/dist/senses/bluebubbles-client.js +2 -1
- package/dist/senses/bluebubbles-media.js +117 -23
- package/dist/senses/bluebubbles.js +1 -1
- package/dist/senses/debug-activity.js +14 -13
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.28",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Bare npx ouro.bot now stays aligned with the current alpha CLI track because the published ouro.bot wrapper is version-locked and republished alongside the CLI instead of lagging behind it.",
|
|
8
|
+
"Slugger no longer re-opens active iMessage task threads with generic greetings like 'hiya' when work is already in motion; fresh idle conversations can still start warmly.",
|
|
9
|
+
"BlueBubbles voice notes now use a harness-managed whisper.cpp transcription path for the current OpenAI, Anthropic, and MiniMax runtime contracts, including automatic local provisioning and truthful error notices when transcription cannot complete."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"version": "0.1.0-alpha.27",
|
|
14
|
+
"changes": [
|
|
15
|
+
"The daemon now discovers all enabled agents in ~/AgentBundles, so ouro status and managed workers reflect every real agent instead of only slugger and ouroboros.",
|
|
16
|
+
"BlueBubbles typing now wraps the visible working phase correctly, and phrase updates from agent.json take effect on the next turn without requiring a restart."
|
|
17
|
+
]
|
|
18
|
+
},
|
|
4
19
|
{
|
|
5
20
|
"version": "0.1.0-alpha.26",
|
|
6
21
|
"changes": [
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.listEnabledBundleAgents = listEnabledBundleAgents;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const identity_1 = require("../identity");
|
|
40
|
+
const runtime_1 = require("../../nerves/runtime");
|
|
41
|
+
function listEnabledBundleAgents(options = {}) {
|
|
42
|
+
const bundlesRoot = options.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
|
|
43
|
+
const readdirSync = options.readdirSync ?? fs.readdirSync;
|
|
44
|
+
const readFileSync = options.readFileSync ?? fs.readFileSync;
|
|
45
|
+
let entries;
|
|
46
|
+
try {
|
|
47
|
+
entries = readdirSync(bundlesRoot, { withFileTypes: true });
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
(0, runtime_1.emitNervesEvent)({
|
|
51
|
+
level: "warn",
|
|
52
|
+
component: "daemon",
|
|
53
|
+
event: "daemon.agent_discovery_failed",
|
|
54
|
+
message: "failed to read bundle root for daemon agent discovery",
|
|
55
|
+
meta: { bundlesRoot },
|
|
56
|
+
});
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
const discovered = [];
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
if (!entry.isDirectory() || !entry.name.endsWith(".ouro"))
|
|
62
|
+
continue;
|
|
63
|
+
const agentName = entry.name.slice(0, -5);
|
|
64
|
+
const configPath = path.join(bundlesRoot, entry.name, "agent.json");
|
|
65
|
+
let enabled = true;
|
|
66
|
+
try {
|
|
67
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
68
|
+
const parsed = JSON.parse(raw);
|
|
69
|
+
if (typeof parsed.enabled === "boolean") {
|
|
70
|
+
enabled = parsed.enabled;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (enabled) {
|
|
77
|
+
discovered.push(agentName);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return discovered.sort((left, right) => left.localeCompare(right));
|
|
81
|
+
}
|
|
@@ -56,6 +56,7 @@ const specialist_prompt_1 = require("./specialist-prompt");
|
|
|
56
56
|
const specialist_tools_1 = require("./specialist-tools");
|
|
57
57
|
const runtime_metadata_1 = require("./runtime-metadata");
|
|
58
58
|
const daemon_runtime_sync_1 = require("./daemon-runtime-sync");
|
|
59
|
+
const agent_discovery_1 = require("./agent-discovery");
|
|
59
60
|
const update_hooks_1 = require("./update-hooks");
|
|
60
61
|
const bundle_meta_1 = require("./hooks/bundle-meta");
|
|
61
62
|
const bundle_manifest_1 = require("../../mind/bundle-manifest");
|
|
@@ -596,36 +597,11 @@ async function defaultPromptInput(question) {
|
|
|
596
597
|
}
|
|
597
598
|
}
|
|
598
599
|
function defaultListDiscoveredAgents() {
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
}
|
|
604
|
-
catch {
|
|
605
|
-
return [];
|
|
606
|
-
}
|
|
607
|
-
const discovered = [];
|
|
608
|
-
for (const entry of entries) {
|
|
609
|
-
if (!entry.isDirectory() || !entry.name.endsWith(".ouro"))
|
|
610
|
-
continue;
|
|
611
|
-
const agentName = entry.name.slice(0, -5);
|
|
612
|
-
const configPath = path.join(bundlesRoot, entry.name, "agent.json");
|
|
613
|
-
let enabled = true;
|
|
614
|
-
try {
|
|
615
|
-
const raw = fs.readFileSync(configPath, "utf-8");
|
|
616
|
-
const parsed = JSON.parse(raw);
|
|
617
|
-
if (typeof parsed.enabled === "boolean") {
|
|
618
|
-
enabled = parsed.enabled;
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
catch {
|
|
622
|
-
continue;
|
|
623
|
-
}
|
|
624
|
-
if (enabled) {
|
|
625
|
-
discovered.push(agentName);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
return discovered.sort((left, right) => left.localeCompare(right));
|
|
600
|
+
return (0, agent_discovery_1.listEnabledBundleAgents)({
|
|
601
|
+
bundlesRoot: (0, identity_1.getAgentBundlesRoot)(),
|
|
602
|
+
readdirSync: fs.readdirSync,
|
|
603
|
+
readFileSync: fs.readFileSync,
|
|
604
|
+
});
|
|
629
605
|
}
|
|
630
606
|
async function defaultLinkFriendIdentity(command) {
|
|
631
607
|
const fp = path.join((0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`, "friends");
|
|
@@ -9,6 +9,7 @@ const health_monitor_1 = require("./health-monitor");
|
|
|
9
9
|
const task_scheduler_1 = require("./task-scheduler");
|
|
10
10
|
const runtime_logging_1 = require("./runtime-logging");
|
|
11
11
|
const sense_manager_1 = require("./sense-manager");
|
|
12
|
+
const agent_discovery_1 = require("./agent-discovery");
|
|
12
13
|
function parseSocketPath(argv) {
|
|
13
14
|
const socketIndex = argv.indexOf("--socket");
|
|
14
15
|
if (socketIndex >= 0) {
|
|
@@ -26,7 +27,7 @@ const socketPath = parseSocketPath(process.argv);
|
|
|
26
27
|
message: "starting daemon entrypoint",
|
|
27
28
|
meta: { socketPath },
|
|
28
29
|
});
|
|
29
|
-
const managedAgents =
|
|
30
|
+
const managedAgents = (0, agent_discovery_1.listEnabledBundleAgents)();
|
|
30
31
|
const processManager = new process_manager_1.DaemonProcessManager({
|
|
31
32
|
agents: managedAgents.map((agent) => ({
|
|
32
33
|
name: agent,
|
|
@@ -7,6 +7,26 @@ function wrapperPackageChanged(changedFiles) {
|
|
|
7
7
|
}
|
|
8
8
|
function assessWrapperPublishSync(input) {
|
|
9
9
|
let result;
|
|
10
|
+
if (input.localVersion !== input.cliVersion) {
|
|
11
|
+
result = {
|
|
12
|
+
ok: false,
|
|
13
|
+
message: `ouro.bot wrapper version ${input.localVersion} must match @ouro.bot/cli version ${input.cliVersion}`,
|
|
14
|
+
};
|
|
15
|
+
(0, runtime_1.emitNervesEvent)({
|
|
16
|
+
level: "warn",
|
|
17
|
+
component: "daemon",
|
|
18
|
+
event: "daemon.wrapper_publish_guard_checked",
|
|
19
|
+
message: "evaluated wrapper publish sync",
|
|
20
|
+
meta: {
|
|
21
|
+
changed: wrapperPackageChanged(input.changedFiles),
|
|
22
|
+
localVersion: input.localVersion,
|
|
23
|
+
cliVersion: input.cliVersion,
|
|
24
|
+
publishedVersion: input.publishedVersion,
|
|
25
|
+
ok: result.ok,
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
10
30
|
if (!wrapperPackageChanged(input.changedFiles)) {
|
|
11
31
|
result = {
|
|
12
32
|
ok: true,
|
|
@@ -16,7 +36,13 @@ function assessWrapperPublishSync(input) {
|
|
|
16
36
|
component: "daemon",
|
|
17
37
|
event: "daemon.wrapper_publish_guard_checked",
|
|
18
38
|
message: "evaluated wrapper publish sync",
|
|
19
|
-
meta: {
|
|
39
|
+
meta: {
|
|
40
|
+
changed: false,
|
|
41
|
+
localVersion: input.localVersion,
|
|
42
|
+
cliVersion: input.cliVersion,
|
|
43
|
+
publishedVersion: input.publishedVersion,
|
|
44
|
+
ok: result.ok,
|
|
45
|
+
},
|
|
20
46
|
});
|
|
21
47
|
return result;
|
|
22
48
|
}
|
|
@@ -30,7 +56,13 @@ function assessWrapperPublishSync(input) {
|
|
|
30
56
|
component: "daemon",
|
|
31
57
|
event: "daemon.wrapper_publish_guard_checked",
|
|
32
58
|
message: "evaluated wrapper publish sync",
|
|
33
|
-
meta: {
|
|
59
|
+
meta: {
|
|
60
|
+
changed: true,
|
|
61
|
+
localVersion: input.localVersion,
|
|
62
|
+
cliVersion: input.cliVersion,
|
|
63
|
+
publishedVersion: input.publishedVersion,
|
|
64
|
+
ok: result.ok,
|
|
65
|
+
},
|
|
34
66
|
});
|
|
35
67
|
return result;
|
|
36
68
|
}
|
|
@@ -42,7 +74,13 @@ function assessWrapperPublishSync(input) {
|
|
|
42
74
|
component: "daemon",
|
|
43
75
|
event: "daemon.wrapper_publish_guard_checked",
|
|
44
76
|
message: "evaluated wrapper publish sync",
|
|
45
|
-
meta: {
|
|
77
|
+
meta: {
|
|
78
|
+
changed: true,
|
|
79
|
+
localVersion: input.localVersion,
|
|
80
|
+
cliVersion: input.cliVersion,
|
|
81
|
+
publishedVersion: input.publishedVersion,
|
|
82
|
+
ok: result.ok,
|
|
83
|
+
},
|
|
46
84
|
});
|
|
47
85
|
return result;
|
|
48
86
|
}
|
package/dist/heart/identity.js
CHANGED
|
@@ -43,6 +43,7 @@ exports.getAgentSecretsPath = getAgentSecretsPath;
|
|
|
43
43
|
exports.loadAgentConfig = loadAgentConfig;
|
|
44
44
|
exports.setAgentName = setAgentName;
|
|
45
45
|
exports.setAgentConfigOverride = setAgentConfigOverride;
|
|
46
|
+
exports.resetAgentConfigCache = resetAgentConfigCache;
|
|
46
47
|
exports.resetIdentity = resetIdentity;
|
|
47
48
|
const fs = __importStar(require("fs"));
|
|
48
49
|
const os = __importStar(require("os"));
|
|
@@ -346,6 +347,13 @@ function setAgentName(name) {
|
|
|
346
347
|
function setAgentConfigOverride(config) {
|
|
347
348
|
_agentConfigOverride = config;
|
|
348
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* Clear only the cached agent config while preserving the resolved agent identity.
|
|
352
|
+
* Used when a running agent should pick up updated disk-backed config on the next turn.
|
|
353
|
+
*/
|
|
354
|
+
function resetAgentConfigCache() {
|
|
355
|
+
_cachedAgentConfig = null;
|
|
356
|
+
}
|
|
349
357
|
/**
|
|
350
358
|
* Clear all cached identity state.
|
|
351
359
|
* Used in tests and when switching agent context.
|
|
@@ -37,7 +37,8 @@ function getFirstImpressions(friend) {
|
|
|
37
37
|
lines.push("- what do they do outside of work that they care about?");
|
|
38
38
|
lines.push("i don't ask all of these at once -- i weave them into conversation naturally, one or two at a time, and i genuinely follow up on what they share.");
|
|
39
39
|
lines.push("i introduce what i can do -- i have tools, integrations, and skills that can help them. i mention these naturally as they become relevant.");
|
|
40
|
-
lines.push("if
|
|
40
|
+
lines.push("if we're already in motion on a task, thread, or follow-up, i do not reset with a generic opener like 'hiya' or 'what do ya need help with?'. i continue directly or ask the specific next question.");
|
|
41
|
+
lines.push("only when the conversation is genuinely fresh and idle, with no active ask or thread in flight, a light opener is okay.");
|
|
41
42
|
lines.push("i save everything i learn immediately with save_friend_note -- names, roles, preferences, projects, anything. the bar is low: if i learned it, i save it.");
|
|
42
43
|
return lines.join("\n");
|
|
43
44
|
}
|
package/dist/mind/phrases.js
CHANGED
|
@@ -196,7 +196,8 @@ function extractRepairData(payload) {
|
|
|
196
196
|
return asRecord(record?.data) ?? record;
|
|
197
197
|
}
|
|
198
198
|
function providerSupportsAudioInput(provider) {
|
|
199
|
-
|
|
199
|
+
void provider;
|
|
200
|
+
return false;
|
|
200
201
|
}
|
|
201
202
|
async function resolveChatGuid(chat, config, channelConfig) {
|
|
202
203
|
return chat.chatGuid
|
|
@@ -62,6 +62,12 @@ const AUDIO_INPUT_FORMAT_BY_EXTENSION = {
|
|
|
62
62
|
".wav": "wav",
|
|
63
63
|
".mp3": "mp3",
|
|
64
64
|
};
|
|
65
|
+
const WHISPER_CPP_FORMULA = "whisper-cpp";
|
|
66
|
+
const WHISPER_CPP_MODEL_NAME = "ggml-base.en.bin";
|
|
67
|
+
const WHISPER_CPP_MODEL_URL = `https://huggingface.co/ggerganov/whisper.cpp/resolve/main/${WHISPER_CPP_MODEL_NAME}`;
|
|
68
|
+
const WHISPER_CPP_TOOLS_DIR = path.join(os.homedir(), ".agentstate", "tools", "whisper-cpp");
|
|
69
|
+
const WHISPER_CPP_MODELS_DIR = path.join(WHISPER_CPP_TOOLS_DIR, "models");
|
|
70
|
+
const WHISPER_CPP_MODEL_PATH = path.join(WHISPER_CPP_MODELS_DIR, WHISPER_CPP_MODEL_NAME);
|
|
65
71
|
function buildBlueBubblesApiUrl(baseUrl, endpoint, password) {
|
|
66
72
|
const root = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
67
73
|
const url = new URL(endpoint.replace(/^\//, ""), root);
|
|
@@ -107,36 +113,123 @@ function audioFormatForInput(contentType, attachment) {
|
|
|
107
113
|
const extension = path.extname(attachment?.transferName ?? "").toLowerCase();
|
|
108
114
|
return AUDIO_INPUT_FORMAT_BY_CONTENT_TYPE[contentType ?? ""] ?? AUDIO_INPUT_FORMAT_BY_EXTENSION[extension];
|
|
109
115
|
}
|
|
110
|
-
async function
|
|
116
|
+
async function execFileText(file, args, timeout) {
|
|
117
|
+
return await new Promise((resolve, reject) => {
|
|
118
|
+
(0, node_child_process_1.execFile)(file, args, { timeout }, (error, stdout = "", stderr = "") => {
|
|
119
|
+
if (error) {
|
|
120
|
+
const detail = stderr.trim() || stdout.trim() || error.message;
|
|
121
|
+
reject(new Error(detail));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
resolve(stdout);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async function pathExists(targetPath) {
|
|
129
|
+
try {
|
|
130
|
+
await fs.access(targetPath);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function resolveWhisperCppBinary(timeoutMs) {
|
|
138
|
+
try {
|
|
139
|
+
const existing = (await execFileText("which", ["whisper-cli"], timeoutMs)).trim();
|
|
140
|
+
if (existing) {
|
|
141
|
+
return existing;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// fall through to managed install
|
|
146
|
+
}
|
|
147
|
+
let prefix = "";
|
|
148
|
+
try {
|
|
149
|
+
prefix = (await execFileText("brew", ["--prefix", WHISPER_CPP_FORMULA], timeoutMs)).trim();
|
|
150
|
+
if (prefix) {
|
|
151
|
+
const candidate = path.join(prefix, "bin", "whisper-cli");
|
|
152
|
+
if (await pathExists(candidate)) {
|
|
153
|
+
return candidate;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// fall through to managed install
|
|
159
|
+
}
|
|
160
|
+
await execFileText("brew", ["install", WHISPER_CPP_FORMULA], Math.max(timeoutMs, 300_000));
|
|
161
|
+
prefix = (await execFileText("brew", ["--prefix", WHISPER_CPP_FORMULA], timeoutMs)).trim();
|
|
162
|
+
if (!prefix) {
|
|
163
|
+
throw new Error("whisper.cpp installed but brew did not return a usable prefix");
|
|
164
|
+
}
|
|
165
|
+
const candidate = path.join(prefix, "bin", "whisper-cli");
|
|
166
|
+
if (!await pathExists(candidate)) {
|
|
167
|
+
throw new Error("whisper.cpp installed but whisper-cli binary is missing");
|
|
168
|
+
}
|
|
169
|
+
return candidate;
|
|
170
|
+
}
|
|
171
|
+
async function ensureWhisperCppModel(timeoutMs, fetchImpl) {
|
|
172
|
+
try {
|
|
173
|
+
await fs.access(WHISPER_CPP_MODEL_PATH);
|
|
174
|
+
return WHISPER_CPP_MODEL_PATH;
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
await fs.mkdir(WHISPER_CPP_MODELS_DIR, { recursive: true });
|
|
178
|
+
const response = await fetchImpl(WHISPER_CPP_MODEL_URL, {
|
|
179
|
+
method: "GET",
|
|
180
|
+
signal: AbortSignal.timeout(Math.max(timeoutMs, 300_000)),
|
|
181
|
+
});
|
|
182
|
+
if (!response.ok) {
|
|
183
|
+
throw new Error(`failed to download whisper.cpp model: HTTP ${response.status}`);
|
|
184
|
+
}
|
|
185
|
+
await fs.writeFile(WHISPER_CPP_MODEL_PATH, Buffer.from(await response.arrayBuffer()));
|
|
186
|
+
return WHISPER_CPP_MODEL_PATH;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function convertAudioForWhisperCpp(sourcePath, outputPath, timeoutMs) {
|
|
190
|
+
try {
|
|
191
|
+
await execFileText("ffmpeg", ["-y", "-i", sourcePath, "-ar", "16000", "-ac", "1", "-c:a", "pcm_s16le", outputPath], Math.max(timeoutMs, 120_000));
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
catch (ffmpegError) {
|
|
195
|
+
try {
|
|
196
|
+
await execFileText("afconvert", ["-f", "WAVE", "-d", "LEI16@16000", "-c", "1", sourcePath, outputPath], Math.max(timeoutMs, 120_000));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
catch (afconvertError) {
|
|
200
|
+
const ffmpegReason = ffmpegError.message;
|
|
201
|
+
const afconvertReason = afconvertError.message;
|
|
202
|
+
throw new Error(`failed to prepare audio for whisper.cpp (ffmpeg: ${ffmpegReason}; afconvert: ${afconvertReason})`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function transcribeAudioWithWhisperCpp(params, modelFetchImpl = fetch) {
|
|
111
207
|
const workDir = await fs.mkdtemp(path.join(os.tmpdir(), "ouro-bb-audio-"));
|
|
112
208
|
const filename = sanitizeFilename(describeAttachment(params.attachment));
|
|
113
209
|
const extension = fileExtensionForAudio(params.attachment, params.contentType);
|
|
114
210
|
const audioPath = path.join(workDir, `${path.parse(filename).name}${extension}`);
|
|
211
|
+
const wavPath = path.join(workDir, `${path.parse(audioPath).name}.wav`);
|
|
212
|
+
const outputBase = path.join(workDir, path.parse(audioPath).name);
|
|
115
213
|
try {
|
|
116
214
|
await fs.writeFile(audioPath, params.buffer);
|
|
117
|
-
await
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
"--output_dir",
|
|
123
|
-
workDir,
|
|
124
|
-
"--output_format",
|
|
125
|
-
"json",
|
|
126
|
-
"--verbose",
|
|
127
|
-
"False",
|
|
128
|
-
], { timeout: Math.max(params.timeoutMs, 120000) }, (error) => {
|
|
129
|
-
if (error) {
|
|
130
|
-
reject(error);
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
resolve();
|
|
134
|
-
});
|
|
135
|
-
});
|
|
136
|
-
const transcriptPath = path.join(workDir, `${path.parse(audioPath).name}.json`);
|
|
215
|
+
const whisperCliPath = await resolveWhisperCppBinary(params.timeoutMs);
|
|
216
|
+
const modelPath = await ensureWhisperCppModel(params.timeoutMs, modelFetchImpl);
|
|
217
|
+
await convertAudioForWhisperCpp(audioPath, wavPath, params.timeoutMs);
|
|
218
|
+
await execFileText(whisperCliPath, ["-m", modelPath, "-f", wavPath, "-oj", "-of", outputBase], Math.max(params.timeoutMs, 120_000));
|
|
219
|
+
const transcriptPath = `${outputBase}.json`;
|
|
137
220
|
const raw = await fs.readFile(transcriptPath, "utf8");
|
|
138
221
|
const parsed = JSON.parse(raw);
|
|
139
|
-
|
|
222
|
+
if (typeof parsed.text === "string") {
|
|
223
|
+
return parsed.text.trim();
|
|
224
|
+
}
|
|
225
|
+
if (Array.isArray(parsed.transcription)) {
|
|
226
|
+
return parsed.transcription
|
|
227
|
+
.map((entry) => (typeof entry?.text === "string" ? entry.text.trim() : ""))
|
|
228
|
+
.filter(Boolean)
|
|
229
|
+
.join(" ")
|
|
230
|
+
.trim();
|
|
231
|
+
}
|
|
232
|
+
return "";
|
|
140
233
|
}
|
|
141
234
|
finally {
|
|
142
235
|
await fs.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
@@ -178,7 +271,8 @@ async function hydrateBlueBubblesAttachments(attachments, config, channelConfig,
|
|
|
178
271
|
},
|
|
179
272
|
});
|
|
180
273
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
181
|
-
const
|
|
274
|
+
const modelFetchImpl = deps.modelFetchImpl ?? fetch;
|
|
275
|
+
const transcribeAudio = deps.transcribeAudio ?? ((params) => transcribeAudioWithWhisperCpp(params, modelFetchImpl));
|
|
182
276
|
const preferAudioInput = deps.preferAudioInput ?? false;
|
|
183
277
|
const inputParts = [];
|
|
184
278
|
const transcriptAdditions = [];
|
|
@@ -205,12 +205,12 @@ function createBlueBubblesCallbacks(client, chat, replyToMessageGuid) {
|
|
|
205
205
|
return;
|
|
206
206
|
}
|
|
207
207
|
textBuffer = "";
|
|
208
|
+
await activity.finish();
|
|
208
209
|
await client.sendText({
|
|
209
210
|
chat,
|
|
210
211
|
text: trimmed,
|
|
211
212
|
replyToMessageGuid,
|
|
212
213
|
});
|
|
213
|
-
await activity.finish();
|
|
214
214
|
},
|
|
215
215
|
async finish() {
|
|
216
216
|
await activity.finish();
|
|
@@ -36,15 +36,6 @@ function createDebugActivityController(options) {
|
|
|
36
36
|
lastPhrase = phrase;
|
|
37
37
|
return phrase;
|
|
38
38
|
}
|
|
39
|
-
function ensureTyping(active) {
|
|
40
|
-
if (typingActive === active) {
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
typingActive = active;
|
|
44
|
-
enqueue(active ? "typing_start" : "typing_stop", async () => {
|
|
45
|
-
await options.transport.setTyping(active);
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
39
|
function setStatus(text) {
|
|
49
40
|
(0, runtime_1.emitNervesEvent)({
|
|
50
41
|
component: "senses",
|
|
@@ -55,13 +46,20 @@ function createDebugActivityController(options) {
|
|
|
55
46
|
textLength: text.length,
|
|
56
47
|
},
|
|
57
48
|
});
|
|
58
|
-
|
|
49
|
+
const shouldStartTyping = !typingActive;
|
|
50
|
+
if (shouldStartTyping) {
|
|
51
|
+
typingActive = true;
|
|
52
|
+
}
|
|
59
53
|
enqueue("status_update", async () => {
|
|
60
54
|
if (statusMessageGuid) {
|
|
61
55
|
await options.transport.editStatus(statusMessageGuid, text);
|
|
62
|
-
return;
|
|
63
56
|
}
|
|
64
|
-
|
|
57
|
+
else {
|
|
58
|
+
statusMessageGuid = await options.transport.sendStatus(text);
|
|
59
|
+
}
|
|
60
|
+
if (shouldStartTyping) {
|
|
61
|
+
await options.transport.setTyping(true);
|
|
62
|
+
}
|
|
65
63
|
});
|
|
66
64
|
}
|
|
67
65
|
return {
|
|
@@ -100,7 +98,10 @@ function createDebugActivityController(options) {
|
|
|
100
98
|
await queue;
|
|
101
99
|
return;
|
|
102
100
|
}
|
|
103
|
-
|
|
101
|
+
typingActive = false;
|
|
102
|
+
enqueue("typing_stop", async () => {
|
|
103
|
+
await options.transport.setTyping(false);
|
|
104
|
+
});
|
|
104
105
|
await queue;
|
|
105
106
|
},
|
|
106
107
|
};
|