@wirecat/cli-core 0.497.0 → 0.499.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/CHANGELOG.md +53 -0
- package/dist/calendar/mcp/index.d.ts +12 -12
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/messenger/hearing-command.js +1 -1
- package/dist/cli/messenger/models-command.js +2 -2
- package/dist/cli/messenger/transcribe-command.js +14 -10
- package/dist/cli/runs/command.js +2 -1
- package/dist/errors/reasons.d.ts +1 -1
- package/dist/errors/reasons.js +8 -0
- package/dist/meetings/transcription.d.ts +5 -0
- package/dist/meetings/transcription.js +27 -6
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/speech/decode.d.ts +11 -1
- package/dist/speech/decode.js +20 -15
- package/dist/speech/hearing.d.ts +3 -1
- package/dist/speech/hearing.js +4 -4
- package/dist/speech/index.d.ts +5 -2
- package/dist/speech/index.js +4 -2
- package/dist/speech/local.d.ts +8 -0
- package/dist/speech/local.js +22 -1
- package/dist/speech/models.d.ts +2 -0
- package/dist/speech/models.js +2 -0
- package/dist/speech/openai.d.ts +54 -0
- package/dist/speech/openai.js +199 -0
- package/dist/speech/recognize.d.ts +25 -1
- package/dist/speech/recognize.js +52 -21
- package/dist/speech/transcribe.d.ts +25 -9
- package/dist/speech/transcribe.js +72 -13
- package/dist/speech/wav.d.ts +2 -0
- package/dist/speech/wav.js +20 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,53 @@ Notable changes to `@wirecat/cli-core` (`@wirecat/cli-messaging` up to 0.261.0,
|
|
|
5
5
|
minor version may break callers, and says how under "Changed — may break callers". `pnpm docs:check` checks the
|
|
6
6
|
shape of this file.
|
|
7
7
|
|
|
8
|
+
## Unreleased
|
|
9
|
+
|
|
10
|
+
## 0.499.0 — 31.10.2026
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`decodeChannels(bytes)` in `@wirecat/cli-core/speech` returns every channel of a recording:**
|
|
15
|
+
`{ channels: Pcm[], mixed }`. A phone call recorded in stereo keeps one speaker per channel, and `decodeAudio`
|
|
16
|
+
keeps only the first, so the other speaker was lost. `decodeAudio` is unchanged and now exported too. Opus,
|
|
17
|
+
Vorbis, FLAC and MP3 keep their channels apart; a format only ffmpeg reads arrives as one channel with
|
|
18
|
+
`mixed: true`, so a caller can tell a mixed-down call from a mono one.
|
|
19
|
+
- **OpenAI transcription: `transcribeOpenAI(pcm, { apiKey, model, languages, prompt, keywords })`.** It sends one
|
|
20
|
+
channel to OpenAI's `/audio/transcriptions` (`gpt-transcribe` by default, `languages[]` and `keywords[]` as
|
|
21
|
+
repeated fields, no SDK) and answers `[{ start, end, text }]` in seconds: one piece per stretch of speech the
|
|
22
|
+
voice detector found, each sent alone, so two channels merge into a dialogue by `start`. Two stretches the
|
|
23
|
+
detector split only because speech ran past 20 seconds stay one piece. `gpt-transcribe` gives no timestamps, so
|
|
24
|
+
the times are the detector's: about 0.1–0.3 s late at the start and 0.5–0.8 s late at the end, and very quiet
|
|
25
|
+
speech is missed, as it is locally. `bundle: true` trades the turns for fewer requests: up to ten minutes
|
|
26
|
+
(`LONGEST_REQUEST_SECONDS`) in one. It needs the voice detector file that comes with any speech model
|
|
27
|
+
(`models download`), except bundled audio of up to ten minutes. A 429 or a server error is tried again, waiting
|
|
28
|
+
as long as `Retry-After` asks; errors carry `auth.failed` (401), `access.denied` (403, which OpenAI also answers
|
|
29
|
+
for an unsupported country), `transcription.quota-exhausted` (credit, spend or usage limit; never retried),
|
|
30
|
+
`transcription.no-key`, `transcription.too-large`, `provider.rate-limited` or `provider.unavailable`, and never
|
|
31
|
+
the key. A keyword with `<`, `>` or a line break is refused before sending.
|
|
32
|
+
- **`recognizeLocalPieces(model, pcm)` gives the same timed pieces from a local model**, one per stretch of speech
|
|
33
|
+
the voice detector cut.
|
|
34
|
+
- **`transcribeWith: "openai"` sends voice messages to OpenAI**, with the key `models keys set openai` or
|
|
35
|
+
`OPENAI_API_KEY` holds and the model in the profile's `transcribeModel` (`gpt-transcribe` when unset). Only when
|
|
36
|
+
named: `auto` never sends audio off the machine, and a read-only process refuses. A transcript heard this way is
|
|
37
|
+
kept as `openai:<model>`.
|
|
38
|
+
- **Meeting transcription keeps who spoke.** `MeetingRecognitionSegment.speaker` (trimmed; `null` is none) is stored
|
|
39
|
+
as the row's speaker, and segments of different speakers may overlap — two people on a call talk over each
|
|
40
|
+
other. One speaker's own segments still may not, and segments without a speaker are checked as before. A
|
|
41
|
+
transcript whose `recognition.engine` is `openai` is stored as `openai/<model>`, format `openai-transcription`;
|
|
42
|
+
local transcripts keep `local/<engine>/<model>` exactly.
|
|
43
|
+
|
|
44
|
+
### Changed — may break callers
|
|
45
|
+
|
|
46
|
+
- **An unknown `transcribeWith` in a profile now refuses to transcribe** with `validation_error`, naming the four
|
|
47
|
+
values. Before, any value other than `local` silently meant asking the messenger first. `models list` and
|
|
48
|
+
`models download` still read such a profile. Set it to `auto`, `messenger`, `local` or `openai`.
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- **`runs list` with nothing recorded no longer says a run is kept only with `--record`.** Every command that does
|
|
53
|
+
its work is kept by default; `record: false` in the config keeps failures only, and `--no-record` keeps nothing.
|
|
54
|
+
|
|
8
55
|
## 0.497.0 — 31.10.2026
|
|
9
56
|
|
|
10
57
|
### Added
|
|
@@ -95,6 +142,12 @@ shape of this file.
|
|
|
95
142
|
|
|
96
143
|
- **`NoteQuery.refs` checks a list of notes against a query, and `NoteHit.at` is the time `date:` read.**
|
|
97
144
|
|
|
145
|
+
### Added
|
|
146
|
+
|
|
147
|
+
- **`chatCoverage` and `ChatCoverage` are exported from `@wirecat/cli-core/services`, and `coverageGaps` from
|
|
148
|
+
`@wirecat/cli-core/cli`.** A CLI can now pass `ports.coverage` to `gatherProjectEvidence` and build `missing` and
|
|
149
|
+
`next` for chats held in part, as cli-core's own agenda and search do.
|
|
150
|
+
|
|
98
151
|
## 0.495.0 — 31.10.2026
|
|
99
152
|
|
|
100
153
|
### Changed — may break callers
|
|
@@ -10,13 +10,13 @@ export declare const calendarTools: ({
|
|
|
10
10
|
inputSchema: {
|
|
11
11
|
type: string;
|
|
12
12
|
properties: {
|
|
13
|
+
limit?: undefined;
|
|
13
14
|
account_id: {
|
|
14
15
|
type: string;
|
|
15
16
|
minimum: number;
|
|
16
17
|
};
|
|
17
18
|
words?: undefined;
|
|
18
19
|
timezone?: undefined;
|
|
19
|
-
limit?: undefined;
|
|
20
20
|
event?: undefined;
|
|
21
21
|
} | {
|
|
22
22
|
account_id: {
|
|
@@ -38,9 +38,9 @@ export declare const calendarTools: ({
|
|
|
38
38
|
};
|
|
39
39
|
event?: undefined;
|
|
40
40
|
} | {
|
|
41
|
+
limit?: undefined;
|
|
41
42
|
words?: undefined;
|
|
42
43
|
timezone?: undefined;
|
|
43
|
-
limit?: undefined;
|
|
44
44
|
event: {
|
|
45
45
|
type: string;
|
|
46
46
|
minLength: number;
|
|
@@ -50,9 +50,9 @@ export declare const calendarTools: ({
|
|
|
50
50
|
minimum: number;
|
|
51
51
|
};
|
|
52
52
|
} | {
|
|
53
|
+
limit?: undefined;
|
|
53
54
|
words?: undefined;
|
|
54
55
|
timezone?: undefined;
|
|
55
|
-
limit?: undefined;
|
|
56
56
|
event?: undefined;
|
|
57
57
|
account_id?: undefined;
|
|
58
58
|
};
|
|
@@ -71,13 +71,13 @@ export declare const calendarTools: ({
|
|
|
71
71
|
inputSchema: {
|
|
72
72
|
type: string;
|
|
73
73
|
properties: {
|
|
74
|
+
limit?: undefined;
|
|
74
75
|
account_id: {
|
|
75
76
|
type: string;
|
|
76
77
|
minimum: number;
|
|
77
78
|
};
|
|
78
79
|
words?: undefined;
|
|
79
80
|
timezone?: undefined;
|
|
80
|
-
limit?: undefined;
|
|
81
81
|
event?: undefined;
|
|
82
82
|
} | {
|
|
83
83
|
account_id: {
|
|
@@ -99,9 +99,9 @@ export declare const calendarTools: ({
|
|
|
99
99
|
};
|
|
100
100
|
event?: undefined;
|
|
101
101
|
} | {
|
|
102
|
+
limit?: undefined;
|
|
102
103
|
words?: undefined;
|
|
103
104
|
timezone?: undefined;
|
|
104
|
-
limit?: undefined;
|
|
105
105
|
event: {
|
|
106
106
|
type: string;
|
|
107
107
|
minLength: number;
|
|
@@ -111,9 +111,9 @@ export declare const calendarTools: ({
|
|
|
111
111
|
minimum: number;
|
|
112
112
|
};
|
|
113
113
|
} | {
|
|
114
|
+
limit?: undefined;
|
|
114
115
|
words?: undefined;
|
|
115
116
|
timezone?: undefined;
|
|
116
|
-
limit?: undefined;
|
|
117
117
|
event?: undefined;
|
|
118
118
|
account_id?: undefined;
|
|
119
119
|
};
|
|
@@ -132,13 +132,13 @@ export declare const calendarTools: ({
|
|
|
132
132
|
inputSchema: {
|
|
133
133
|
type: string;
|
|
134
134
|
properties: {
|
|
135
|
+
limit?: undefined;
|
|
135
136
|
account_id: {
|
|
136
137
|
type: string;
|
|
137
138
|
minimum: number;
|
|
138
139
|
};
|
|
139
140
|
words?: undefined;
|
|
140
141
|
timezone?: undefined;
|
|
141
|
-
limit?: undefined;
|
|
142
142
|
event?: undefined;
|
|
143
143
|
} | {
|
|
144
144
|
account_id: {
|
|
@@ -160,9 +160,9 @@ export declare const calendarTools: ({
|
|
|
160
160
|
};
|
|
161
161
|
event?: undefined;
|
|
162
162
|
} | {
|
|
163
|
+
limit?: undefined;
|
|
163
164
|
words?: undefined;
|
|
164
165
|
timezone?: undefined;
|
|
165
|
-
limit?: undefined;
|
|
166
166
|
event: {
|
|
167
167
|
type: string;
|
|
168
168
|
minLength: number;
|
|
@@ -172,9 +172,9 @@ export declare const calendarTools: ({
|
|
|
172
172
|
minimum: number;
|
|
173
173
|
};
|
|
174
174
|
} | {
|
|
175
|
+
limit?: undefined;
|
|
175
176
|
words?: undefined;
|
|
176
177
|
timezone?: undefined;
|
|
177
|
-
limit?: undefined;
|
|
178
178
|
event?: undefined;
|
|
179
179
|
account_id?: undefined;
|
|
180
180
|
};
|
|
@@ -193,13 +193,13 @@ export declare const calendarTools: ({
|
|
|
193
193
|
inputSchema: {
|
|
194
194
|
type: string;
|
|
195
195
|
properties: {
|
|
196
|
+
limit?: undefined;
|
|
196
197
|
account_id: {
|
|
197
198
|
type: string;
|
|
198
199
|
minimum: number;
|
|
199
200
|
};
|
|
200
201
|
words?: undefined;
|
|
201
202
|
timezone?: undefined;
|
|
202
|
-
limit?: undefined;
|
|
203
203
|
event?: undefined;
|
|
204
204
|
} | {
|
|
205
205
|
account_id: {
|
|
@@ -221,9 +221,9 @@ export declare const calendarTools: ({
|
|
|
221
221
|
};
|
|
222
222
|
event?: undefined;
|
|
223
223
|
} | {
|
|
224
|
+
limit?: undefined;
|
|
224
225
|
words?: undefined;
|
|
225
226
|
timezone?: undefined;
|
|
226
|
-
limit?: undefined;
|
|
227
227
|
event: {
|
|
228
228
|
type: string;
|
|
229
229
|
minLength: number;
|
|
@@ -233,9 +233,9 @@ export declare const calendarTools: ({
|
|
|
233
233
|
minimum: number;
|
|
234
234
|
};
|
|
235
235
|
} | {
|
|
236
|
+
limit?: undefined;
|
|
236
237
|
words?: undefined;
|
|
237
238
|
timezone?: undefined;
|
|
238
|
-
limit?: undefined;
|
|
239
239
|
event?: undefined;
|
|
240
240
|
account_id?: undefined;
|
|
241
241
|
};
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -96,7 +96,7 @@ export { httpTokenFile, type McpEnvironment, type McpHost, serverEntry } from ".
|
|
|
96
96
|
export { type MessageItem, messageItem, type ReadMessage } from "./messenger/message-items.js";
|
|
97
97
|
export { type ModeProblem, type PrivateFiles, privateFiles, withSqliteSidecars } from "./messenger/private-files.js";
|
|
98
98
|
export { buildReport as buildDoctorReport, type Report as DoctorReport, type ReportContext as DoctorReportContext, reportFileName as doctorReportFileName, } from "./messenger/report.js";
|
|
99
|
-
export { allAnswer, allHitItem, type HitItem, type MessageHitItem, messageHitItem, type NoteHitItem, noteHitItem, } from "./messenger/search-answers.js";
|
|
99
|
+
export { allAnswer, allHitItem, coverageGaps, type HitItem, type MessageHitItem, messageHitItem, type NoteHitItem, noteHitItem, } from "./messenger/search-answers.js";
|
|
100
100
|
export { SEARCHES_QUERY } from "./messenger/searches-command.js";
|
|
101
101
|
export { servingProfiles } from "./messenger/serve-command.js";
|
|
102
102
|
export { DAY_MS, FIRST_FETCH_MS, type MemberFetches, memberFetches } from "./messenger/serve-members.js";
|
package/dist/cli/index.js
CHANGED
|
@@ -87,7 +87,7 @@ export { httpTokenFile, serverEntry } from "./messenger/mcp-command.js";
|
|
|
87
87
|
export { messageItem } from "./messenger/message-items.js";
|
|
88
88
|
export { privateFiles, withSqliteSidecars } from "./messenger/private-files.js";
|
|
89
89
|
export { buildReport as buildDoctorReport, reportFileName as doctorReportFileName, } from "./messenger/report.js";
|
|
90
|
-
export { allAnswer, allHitItem, messageHitItem, noteHitItem, } from "./messenger/search-answers.js";
|
|
90
|
+
export { allAnswer, allHitItem, coverageGaps, messageHitItem, noteHitItem, } from "./messenger/search-answers.js";
|
|
91
91
|
export { SEARCHES_QUERY } from "./messenger/searches-command.js";
|
|
92
92
|
export { servingProfiles } from "./messenger/serve-command.js";
|
|
93
93
|
export { DAY_MS, FIRST_FETCH_MS, memberFetches } from "./messenger/serve-members.js";
|
|
@@ -23,7 +23,7 @@ export const hearForCommand = async (context, messenger, messages, transcribe, m
|
|
|
23
23
|
const hearing = await hearVoices(messenger, messages, kept, transcribe && !offline
|
|
24
24
|
? {
|
|
25
25
|
choice: {
|
|
26
|
-
...choose(messenger, context.settings, model === undefined ? {} : { model }, context.env),
|
|
26
|
+
...choose(messenger, context.settings, model === undefined ? {} : { model }, context.env, context.keyring),
|
|
27
27
|
...(context.recognizer ? { open: context.recognizer } : {}),
|
|
28
28
|
},
|
|
29
29
|
connect: connect ?? ((work) => context.withMessenger(work)),
|
|
@@ -89,7 +89,7 @@ const downloadAudioModel = async (command, messenger, id) => {
|
|
|
89
89
|
finally {
|
|
90
90
|
recognizer.free();
|
|
91
91
|
}
|
|
92
|
-
const chosen = choose(messenger, context.settings, {}, context.env).model.id;
|
|
92
|
+
const chosen = choose(messenger, context.settings, {}, context.env, context.keyring).model.id;
|
|
93
93
|
renderAnswer(command, context, itemOf(audioItem(model, directory, chosen)), downloaded);
|
|
94
94
|
}, { unbounded: true });
|
|
95
95
|
};
|
|
@@ -174,7 +174,7 @@ export const modelsCommand = (messenger) => {
|
|
|
174
174
|
const rerankers = kinds === undefined || kinds === "reranker"
|
|
175
175
|
? RERANK_MODELS.filter((model) => id === undefined || model.id === id)
|
|
176
176
|
: [];
|
|
177
|
-
const chosen = audioModels.length > 0 ? choose(messenger, context.settings, {}, context.env).model.id : "";
|
|
177
|
+
const chosen = audioModels.length > 0 ? choose(messenger, context.settings, {}, context.env, context.keyring).model.id : "";
|
|
178
178
|
const catalogue = [
|
|
179
179
|
...audioModels.map((model) => audioItem(model, modelsDirectory(context.env), chosen)),
|
|
180
180
|
...textModels.map((model) => textItem(model, textModelsDirectory(context.env))),
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { annotate } from "@wirecat/cli-base/commands";
|
|
2
2
|
import { openKept } from "../../speech/hearing.js";
|
|
3
3
|
import { isInstalled } from "../../speech/install.js";
|
|
4
|
-
import { choose, hearLocally, hearOnline, notDownloaded } from "../../speech/transcribe.js";
|
|
4
|
+
import { choose, heardVia, hearLocally, hearOnline, keptSource, notDownloaded, } from "../../speech/transcribe.js";
|
|
5
|
+
import { environmentOf } from "../context.js";
|
|
5
6
|
import { REMOTE_READ } from "../effects.js";
|
|
6
7
|
import { renderAnswer } from "../envelope.js";
|
|
7
8
|
import { messengerContext } from "./context.js";
|
|
@@ -22,10 +23,13 @@ export const transcribeSubcommand = (messages, messenger) => annotate(messages.c
|
|
|
22
23
|
.action(async function (chat, messageId) {
|
|
23
24
|
const context = messengerContext(this, messenger);
|
|
24
25
|
const choice = {
|
|
25
|
-
...choose(messenger, context.settings, this.opts(), context.env),
|
|
26
|
+
...choose(messenger, context.settings, this.opts(), context.env, context.keyring),
|
|
26
27
|
...(context.recognizer ? { open: context.recognizer } : {}),
|
|
28
|
+
...signalOf(this),
|
|
27
29
|
};
|
|
28
|
-
// Before connecting: a missing model should not cost a login.
|
|
30
|
+
// Before connecting: a missing model or OpenAI key should not cost a login.
|
|
31
|
+
if (choice.refusal)
|
|
32
|
+
throw choice.refusal;
|
|
29
33
|
if (choice.with === "local" && !isInstalled(choice.model, choice.directory)) {
|
|
30
34
|
throw notDownloaded(messenger, choice.model);
|
|
31
35
|
}
|
|
@@ -51,17 +55,17 @@ export const transcribeSubcommand = (messages, messenger) => annotate(messages.c
|
|
|
51
55
|
return [resolved, undefined, await hearOnline(messenger, connection, resolved, id, choice, read)];
|
|
52
56
|
});
|
|
53
57
|
const heard = before
|
|
54
|
-
? {
|
|
55
|
-
messageId: id,
|
|
56
|
-
text: before.text,
|
|
57
|
-
pending: false,
|
|
58
|
-
...(before.source === messenger.provider ? { via: before.source } : { via: "local", model: before.source }),
|
|
59
|
-
}
|
|
58
|
+
? { messageId: id, text: before.text, pending: false, ...heardVia(before.source, messenger.provider) }
|
|
60
59
|
: online instanceof Uint8Array
|
|
61
60
|
? await hearLocally(online, id, choice)
|
|
62
61
|
: online;
|
|
63
62
|
if (!heard.pending && !before) {
|
|
64
|
-
await withKept((kept) => kept.keep({ chatId, messageId: id }, heard.text, heard
|
|
63
|
+
await withKept((kept) => kept.keep({ chatId, messageId: id }, heard.text, keptSource(heard)));
|
|
65
64
|
}
|
|
66
65
|
renderAnswer(this, context, heardAnswer(heard, chatId), () => `${heard.text}\n`);
|
|
67
66
|
});
|
|
67
|
+
const signalOf = (command) => {
|
|
68
|
+
const { signal, commandSignal } = environmentOf(command);
|
|
69
|
+
const signals = [signal, commandSignal].filter((one) => one !== undefined);
|
|
70
|
+
return signals.length ? { signal: AbortSignal.any(signals) } : {};
|
|
71
|
+
};
|
package/dist/cli/runs/command.js
CHANGED
|
@@ -41,7 +41,8 @@ export const runsCommand = (app) => {
|
|
|
41
41
|
args: words.length ? { query: typedQuery(words) } : {},
|
|
42
42
|
}));
|
|
43
43
|
if (runs.length === 0)
|
|
44
|
-
renderer.note("nothing recorded — a
|
|
44
|
+
renderer.note("nothing recorded — a command that does its work is kept unless the config says `record: false` (then only " +
|
|
45
|
+
"failures) or it ran with `--no-record`");
|
|
45
46
|
});
|
|
46
47
|
annotate(command.command("show"), {
|
|
47
48
|
examples: [`${app.command} runs show run-101`],
|
package/dist/errors/reasons.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export declare const REASONS: ErrorReason[];
|
|
|
7
7
|
* Detail keys an error from this package may carry, until each is registered on its own reason. Not `cause`:
|
|
8
8
|
* that can hold a provider's or SQLite's raw text.
|
|
9
9
|
*/
|
|
10
|
-
export declare const DETAILS: readonly ["contracts", "calendars", "allowed", "alternative", "arguments", "at", "available", "batch", "budget", "built", "candidates", "chat", "chat_id", "cli", "complete", "emitted_bytes", "exact", "forum", "guide", "hint", "id", "index", "kind", "limit", "limit_source", "max_bytes", "model", "name", "operation_id", "operation_ids", "text_limit", "store_refused_in", "partial_output", "path", "permission", "person_ids", "previous_chat_id", "profiles", "provider_error", "provider", "recovery", "reference", "remembered", "retry_at", "runtime", "saved", "scheduled_for", "script", "sent", "sent_count", "settlement_failed", "span", "sqlite", "stage", "standing", "state", "stderr", "term", "total", "upgraded", "wanted"];
|
|
10
|
+
export declare const DETAILS: readonly ["contracts", "calendars", "allowed", "alternative", "arguments", "at", "available", "batch", "budget", "built", "candidates", "chat", "chat_id", "cli", "complete", "emitted_bytes", "exact", "forum", "guide", "hint", "id", "index", "kind", "limit", "limit_source", "max_bytes", "model", "name", "operation_id", "operation_ids", "text_limit", "store_refused_in", "partial_output", "path", "permission", "person_ids", "previous_chat_id", "profiles", "provider_error", "provider", "recovery", "reference", "remembered", "retry_at", "retry_after_ms", "runtime", "saved", "scheduled_for", "script", "sent", "sent_count", "settlement_failed", "span", "sqlite", "stage", "standing", "state", "stderr", "term", "total", "upgraded", "wanted"];
|
|
11
11
|
/** What a CLI adds to the shared registry: its own reasons, and the detail keys its adapters raise. */
|
|
12
12
|
export interface AppErrors {
|
|
13
13
|
reasons?: readonly ErrorReason[];
|
package/dist/errors/reasons.js
CHANGED
|
@@ -88,6 +88,13 @@ const OTHERS = [
|
|
|
88
88
|
validation("memory.evidence-required", "a memory needs its evidence"),
|
|
89
89
|
validation("memory.scope-required", "a memory needs its scope"),
|
|
90
90
|
["model.not-downloaded", "not_found", "the model is not downloaded on this machine"],
|
|
91
|
+
["transcription.no-key", "authentication_error", "no OpenAI key is set for transcription"],
|
|
92
|
+
validation("transcription.too-large", "the audio is over the transcription provider's size limit"),
|
|
93
|
+
[
|
|
94
|
+
"transcription.quota-exhausted",
|
|
95
|
+
"permission_error",
|
|
96
|
+
"the transcription provider account has no credit or limit left",
|
|
97
|
+
],
|
|
91
98
|
validation("name.invalid", "the name is not valid"),
|
|
92
99
|
validation("output.choose-fields", "at a terminal, --json names the fields it prints"),
|
|
93
100
|
validation("output.one-object", "a report is one JSON object; --jsonl streams lists only"),
|
|
@@ -201,6 +208,7 @@ export const DETAILS = [
|
|
|
201
208
|
"reference",
|
|
202
209
|
"remembered",
|
|
203
210
|
"retry_at",
|
|
211
|
+
"retry_after_ms",
|
|
204
212
|
"runtime",
|
|
205
213
|
"saved",
|
|
206
214
|
"scheduled_for",
|
|
@@ -19,6 +19,11 @@ export interface MeetingRecognitionSegment {
|
|
|
19
19
|
startMs: number;
|
|
20
20
|
endMs: number;
|
|
21
21
|
text: string;
|
|
22
|
+
/**
|
|
23
|
+
* Who spoke, when the source knows — a channel of a two-channel call. Speakers may overlap; one speaker may not.
|
|
24
|
+
* Trimmed; `null` is no speaker.
|
|
25
|
+
*/
|
|
26
|
+
speaker?: string | null;
|
|
22
27
|
}
|
|
23
28
|
export interface MeetingTranscriptionContext {
|
|
24
29
|
maxBytes: number;
|
|
@@ -11,6 +11,8 @@ const identifier = (value) => {
|
|
|
11
11
|
throw new CliError("validation_error", "transcription identifiers must be bounded nonempty strings");
|
|
12
12
|
return value;
|
|
13
13
|
};
|
|
14
|
+
/** Recognized off this machine: the stored source names the provider rather than `local/`. */
|
|
15
|
+
const REMOTE_ENGINES = new Set(["openai"]);
|
|
14
16
|
const cancelled = (signal) => {
|
|
15
17
|
if (signal?.aborted) {
|
|
16
18
|
if (signal.reason instanceof CliError)
|
|
@@ -64,21 +66,33 @@ export const transcribeMeeting = async (source, store, target, options = {}) =>
|
|
|
64
66
|
const outcome = await dispatchMeetingMutation(mutation, async () => {
|
|
65
67
|
const rows = [];
|
|
66
68
|
let textBytes = 0;
|
|
67
|
-
let
|
|
69
|
+
let start = 0;
|
|
70
|
+
const ends = new Map();
|
|
68
71
|
let provided = 0;
|
|
69
72
|
for await (const segment of source.segments(media, context)) {
|
|
70
73
|
cancelled(context.signal);
|
|
71
74
|
provided++;
|
|
75
|
+
const given = segment.speaker ?? undefined;
|
|
76
|
+
if (given !== undefined &&
|
|
77
|
+
(typeof given !== "string" || !given.trim() || given.length > 200 || /[\0\r\n]/.test(given)))
|
|
78
|
+
throw new CliError("invalid_response", "invalid recognition segment speaker");
|
|
79
|
+
const speaker = given?.trim();
|
|
80
|
+
// Without speakers, nothing may overlap; with them, only one speaker's own segments may not.
|
|
81
|
+
const overlapping = speaker === undefined
|
|
82
|
+
? [...ends.values()].reduce((latest, end) => Math.max(latest, end), 0)
|
|
83
|
+
: Math.max(ends.get(undefined) ?? 0, ends.get(speaker) ?? 0);
|
|
72
84
|
if (provided > maxSegments ||
|
|
73
85
|
!Number.isSafeInteger(segment.startMs) ||
|
|
74
86
|
!Number.isSafeInteger(segment.endMs) ||
|
|
75
|
-
segment.startMs <
|
|
87
|
+
segment.startMs < start ||
|
|
88
|
+
segment.startMs < overlapping ||
|
|
76
89
|
segment.endMs <= segment.startMs ||
|
|
77
90
|
segment.endMs > media.durationMs ||
|
|
78
91
|
typeof segment.text !== "string" ||
|
|
79
92
|
segment.text.includes("\0"))
|
|
80
93
|
throw new CliError("invalid_response", "invalid or excessive recognition segments");
|
|
81
|
-
|
|
94
|
+
start = segment.startMs;
|
|
95
|
+
ends.set(speaker, Math.max(ends.get(speaker) ?? 0, segment.endMs));
|
|
82
96
|
const text = segment.text.trim();
|
|
83
97
|
if (text.length > maxTextBytes - textBytes)
|
|
84
98
|
throw new CliError("validation_error", "transcript exceeds its byte limit");
|
|
@@ -90,7 +104,7 @@ export const transcribeMeeting = async (source, store, target, options = {}) =>
|
|
|
90
104
|
position: rows.length,
|
|
91
105
|
startMs: segment.startMs,
|
|
92
106
|
endMs: segment.endMs,
|
|
93
|
-
speakerName: null,
|
|
107
|
+
speakerName: speaker ?? null,
|
|
94
108
|
text,
|
|
95
109
|
normalizedText: null,
|
|
96
110
|
metadata: null,
|
|
@@ -114,8 +128,15 @@ export const transcribeMeeting = async (source, store, target, options = {}) =>
|
|
|
114
128
|
now,
|
|
115
129
|
transcripts: [
|
|
116
130
|
{
|
|
117
|
-
|
|
118
|
-
|
|
131
|
+
...(REMOTE_ENGINES.has(media.recognition.engine)
|
|
132
|
+
? {
|
|
133
|
+
source: `${encodeURIComponent(media.recognition.engine)}/${encodeURIComponent(media.recognition.model)}`,
|
|
134
|
+
format: `${media.recognition.engine}-transcription`,
|
|
135
|
+
}
|
|
136
|
+
: {
|
|
137
|
+
source: `local/${encodeURIComponent(media.recognition.engine)}/${encodeURIComponent(media.recognition.model)}`,
|
|
138
|
+
format: "local-transcription",
|
|
139
|
+
}),
|
|
119
140
|
language: null,
|
|
120
141
|
contentHash: hash,
|
|
121
142
|
externalCreatedAt: null,
|
package/dist/services/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type BotCheckService } from "../botcheck/service.js";
|
|
2
|
+
export { type ChatCoverage, chatCoverage } from "./chat-coverage.js";
|
|
2
3
|
export type { ContextEvidenceItem, ContextEvidencePage, ContextEvidenceReader, ContextEvidenceRequest, ContextEvidenceSender, ContextIndex, ContextReadLimits, ContextReadUsage, ContextRecordReader, ContextSource, ScopedContextEvidenceReader, } from "./context-evidence.js";
|
|
3
4
|
export { contextEvidenceFingerprint, createContextEvidenceReader } from "./context-evidence.js";
|
|
4
5
|
export { excerpt } from "./excerpt.js";
|
package/dist/services/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { botCheckService } from "../botcheck/service.js";
|
|
2
|
+
export { chatCoverage } from "./chat-coverage.js";
|
|
2
3
|
export { contextEvidenceFingerprint, createContextEvidenceReader } from "./context-evidence.js";
|
|
3
4
|
export { excerpt } from "./excerpt.js";
|
|
4
5
|
export { archiveMedia, checkMediaArchive } from "./media-archive.js";
|
package/dist/speech/decode.d.ts
CHANGED
|
@@ -13,9 +13,19 @@ export declare const formatOf: (bytes: Uint8Array) => Format;
|
|
|
13
13
|
* the GPL. From a file rather than a pipe: an MP4 whose index sits at the end cannot be read from a pipe.
|
|
14
14
|
*/
|
|
15
15
|
export declare const runFfmpeg: Ffmpeg;
|
|
16
|
+
export interface DecodedChannels {
|
|
17
|
+
channels: Pcm[];
|
|
18
|
+
/** True when ffmpeg read the file: whatever channels it had arrive mixed into one, so one speaker per channel is lost. */
|
|
19
|
+
mixed: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Every channel the file has, one `Pcm` each: a phone call recorded in stereo keeps one speaker per channel.
|
|
23
|
+
* Only the bundled decoders keep channels apart; what ffmpeg reads arrives mixed down to one, and says so.
|
|
24
|
+
*/
|
|
25
|
+
export declare const decodeChannels: (bytes: Uint8Array, ffmpeg?: Ffmpeg) => Promise<DecodedChannels>;
|
|
16
26
|
/**
|
|
17
27
|
* Opus, Vorbis, FLAC and MP3 by the bundled decoders; anything else, or what one of them could not read, by ffmpeg
|
|
18
|
-
* when the machine has it. The bundled decoder's error wins: it names the file's own problem.
|
|
28
|
+
* when the machine has it. The bundled decoder's error wins: it names the file's own problem. The first channel only.
|
|
19
29
|
*/
|
|
20
30
|
export declare const decodeAudio: (bytes: Uint8Array, ffmpeg?: Ffmpeg) => Promise<Pcm>;
|
|
21
31
|
export {};
|
package/dist/speech/decode.js
CHANGED
|
@@ -33,11 +33,10 @@ export const formatOf = (bytes) => {
|
|
|
33
33
|
return "mp3";
|
|
34
34
|
return "other";
|
|
35
35
|
};
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
if (!samples || decoded.samplesDecoded === 0)
|
|
36
|
+
const everyChannel = (decoded) => {
|
|
37
|
+
if (!decoded.channelData[0] || decoded.samplesDecoded === 0)
|
|
39
38
|
throw new CliError("validation_error", "no audio in that file");
|
|
40
|
-
return { samples, rate: decoded.sampleRate };
|
|
39
|
+
return decoded.channelData.map((samples) => ({ samples, rate: decoded.sampleRate }));
|
|
41
40
|
};
|
|
42
41
|
/**
|
|
43
42
|
* Found from cli-core's own place and imported by URL, so a CLI's bundler leaves the decoder whole: bundled, it looks
|
|
@@ -49,7 +48,7 @@ const decodeVorbis = async (bytes) => {
|
|
|
49
48
|
const decoder = new OggVorbisDecoder();
|
|
50
49
|
await decoder.ready;
|
|
51
50
|
try {
|
|
52
|
-
return
|
|
51
|
+
return everyChannel(await decoder.decodeFile(bytes));
|
|
53
52
|
}
|
|
54
53
|
finally {
|
|
55
54
|
decoder.free();
|
|
@@ -60,7 +59,7 @@ const decodeFlac = async (bytes) => {
|
|
|
60
59
|
const decoder = new FLACDecoder();
|
|
61
60
|
await decoder.ready;
|
|
62
61
|
try {
|
|
63
|
-
return
|
|
62
|
+
return everyChannel(await decoder.decodeFile(bytes));
|
|
64
63
|
}
|
|
65
64
|
finally {
|
|
66
65
|
decoder.free();
|
|
@@ -71,7 +70,7 @@ const decodeMp3 = async (bytes) => {
|
|
|
71
70
|
const decoder = new MPEGDecoder();
|
|
72
71
|
await decoder.ready;
|
|
73
72
|
try {
|
|
74
|
-
return
|
|
73
|
+
return everyChannel(decoder.decode(bytes));
|
|
75
74
|
}
|
|
76
75
|
finally {
|
|
77
76
|
decoder.free();
|
|
@@ -147,18 +146,24 @@ const BUNDLED = {
|
|
|
147
146
|
flac: decodeFlac,
|
|
148
147
|
mp3: decodeMp3,
|
|
149
148
|
};
|
|
149
|
+
const tooLong = (pcm) => pcm.samples.length / pcm.rate > LONGEST_AUDIO_SECONDS;
|
|
150
150
|
/**
|
|
151
|
-
*
|
|
152
|
-
*
|
|
151
|
+
* Every channel the file has, one `Pcm` each: a phone call recorded in stereo keeps one speaker per channel.
|
|
152
|
+
* Only the bundled decoders keep channels apart; what ffmpeg reads arrives mixed down to one, and says so.
|
|
153
153
|
*/
|
|
154
|
-
export const
|
|
154
|
+
export const decodeChannels = async (bytes, ffmpeg = runFfmpeg) => {
|
|
155
155
|
const format = formatOf(bytes);
|
|
156
|
-
const
|
|
157
|
-
? await ffmpeg(bytes)
|
|
158
|
-
: await BUNDLED[format](bytes).
|
|
156
|
+
const decoded = format === "other"
|
|
157
|
+
? { channels: [await ffmpeg(bytes)], mixed: true }
|
|
158
|
+
: await BUNDLED[format](bytes).then((channels) => ({ channels, mixed: false }), (error) => ffmpeg(bytes).then((pcm) => ({ channels: [pcm], mixed: true }), () => {
|
|
159
159
|
throw error;
|
|
160
160
|
}));
|
|
161
|
-
if (
|
|
161
|
+
if (decoded.channels.some(tooLong))
|
|
162
162
|
throw new CliError("validation_error", `the audio is longer than ${LONGEST_AUDIO_SECONDS / 60} minutes`);
|
|
163
|
-
return
|
|
163
|
+
return decoded;
|
|
164
164
|
};
|
|
165
|
+
/**
|
|
166
|
+
* Opus, Vorbis, FLAC and MP3 by the bundled decoders; anything else, or what one of them could not read, by ffmpeg
|
|
167
|
+
* when the machine has it. The bundled decoder's error wins: it names the file's own problem. The first channel only.
|
|
168
|
+
*/
|
|
169
|
+
export const decodeAudio = async (bytes, ffmpeg = runFfmpeg) => (await decodeChannels(bytes, ffmpeg)).channels[0];
|
package/dist/speech/hearing.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type KeyringStore } from "@wirecat/cli-base";
|
|
1
2
|
import type { Missing } from "@wirecat/cli-base/standards";
|
|
2
3
|
import type { Messenger } from "../cli/messenger/context.js";
|
|
3
4
|
import type { Settings } from "../cli/settings.js";
|
|
@@ -60,9 +61,10 @@ export declare const spoken: <T extends Message & {
|
|
|
60
61
|
transcript?: string;
|
|
61
62
|
}>({ transcript, ...message }: T) => Message;
|
|
62
63
|
/** For an MCP tool: the same hearing, over the session's own connection. */
|
|
63
|
-
export declare const hearForTool: (messenger: Messenger, connect: <T>(work: (adapter: MessengerAdapter) => Promise<T>) => Promise<T>, messages: readonly Message[], transcribe: boolean, { settings, env }: {
|
|
64
|
+
export declare const hearForTool: (messenger: Messenger, connect: <T>(work: (adapter: MessengerAdapter) => Promise<T>) => Promise<T>, messages: readonly Message[], transcribe: boolean, { settings, env, keyring, }: {
|
|
64
65
|
settings: Pick<Settings, "configured" | "shared" | "profile">;
|
|
65
66
|
env: NodeJS.ProcessEnv;
|
|
67
|
+
keyring?: KeyringStore;
|
|
66
68
|
}, model?: string) => Promise<Hearing | undefined>;
|
|
67
69
|
/** Messages with their transcripts; `unheard` as well when `--transcribe` was asked for. */
|
|
68
70
|
export declare const heard: <T extends Message>(messages: readonly T[], hearing: Hearing | undefined) => T[];
|
package/dist/speech/hearing.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CliError } from "@wirecat/cli-base";
|
|
2
2
|
import { recalledAccount } from "../cli/messenger/accounts.js";
|
|
3
3
|
import { openStore } from "../store/store.js";
|
|
4
|
-
import { choose, hearLocally, hearOnline, isAudioFile } from "./transcribe.js";
|
|
4
|
+
import { choose, hearLocally, hearOnline, isAudioFile, keptSource } from "./transcribe.js";
|
|
5
5
|
/** `--model` chooses who hears what `--transcribe` asks for; alone it would quietly do nothing. */
|
|
6
6
|
export const modelWith = (transcribe, model) => {
|
|
7
7
|
if (model !== undefined && transcribe !== true) {
|
|
@@ -73,7 +73,7 @@ export const hearVoices = async (messenger, messages, kept, transcribe) => {
|
|
|
73
73
|
if (heard.pending || heard.text.trim() === "")
|
|
74
74
|
continue;
|
|
75
75
|
transcripts.set(keyOf(voice), heard.text);
|
|
76
|
-
await kept.keep(voice, heard.text, heard
|
|
76
|
+
await kept.keep(voice, heard.text, keptSource(heard));
|
|
77
77
|
}
|
|
78
78
|
catch (error) {
|
|
79
79
|
problem ??= error instanceof Error ? error.message : String(error);
|
|
@@ -92,14 +92,14 @@ export const spoken = ({ transcript, ...message }) => transcript === undefined
|
|
|
92
92
|
? message
|
|
93
93
|
: { ...message, text: [message.text, `🎤 ${transcript}`].filter(Boolean).join("\n") };
|
|
94
94
|
/** For an MCP tool: the same hearing, over the session's own connection. */
|
|
95
|
-
export const hearForTool = async (messenger, connect, messages, transcribe, { settings, env }, model) => {
|
|
95
|
+
export const hearForTool = async (messenger, connect, messages, transcribe, { settings, env, keyring, }, model) => {
|
|
96
96
|
if (!messages.some(hasAudio))
|
|
97
97
|
return undefined;
|
|
98
98
|
const kept = await openKept(messenger, settings.profile, env);
|
|
99
99
|
try {
|
|
100
100
|
return await hearVoices(messenger, messages, kept, transcribe
|
|
101
101
|
? {
|
|
102
|
-
choice: choose(messenger, settings, model === undefined ? {} : { model }, env),
|
|
102
|
+
choice: choose(messenger, settings, model === undefined ? {} : { model }, env, keyring),
|
|
103
103
|
connect,
|
|
104
104
|
}
|
|
105
105
|
: undefined);
|
package/dist/speech/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export { decodeAudio, decodeChannels, type Ffmpeg, type Pcm, runFfmpeg, } from "./decode.js";
|
|
1
2
|
export { type Fetch, install, installedBytes, isInstalled, megabytes, modelPath, modelsDirectory, sharedModelsDirectory, vadPath, } from "./install.js";
|
|
2
|
-
export { type LocalSpeechBackend, type LocalSpeechRecognitionModel, type LocalSpeechRecognizer, type LocalSpeechRecognizerOptions, localSpeechRecognitionModel, openLocalSpeechRecognizer, } from "./local.js";
|
|
3
|
-
export { DEFAULT_ORDER, findModel, MODELS, type ModelFile, orderedModels, type SpeechModel, speechModel, VAD, } from "./models.js";
|
|
3
|
+
export { type LocalSpeechBackend, type LocalSpeechRecognitionModel, type LocalSpeechRecognizer, type LocalSpeechRecognizerOptions, localSpeechRecognitionModel, openLocalSpeechRecognizer, recognizeLocalPieces, } from "./local.js";
|
|
4
|
+
export { DEFAULT_ORDER, findModel, MODELS, type ModelFile, OPENAI_TRANSCRIBE_MODEL, orderedModels, type SpeechModel, speechModel, VAD, } from "./models.js";
|
|
5
|
+
export { LONGEST_REQUEST_SECONDS, type OpenAITranscription, transcribeOpenAI } from "./openai.js";
|
|
6
|
+
export type { TimedText } from "./recognize.js";
|
package/dist/speech/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { decodeAudio, decodeChannels, runFfmpeg, } from "./decode.js";
|
|
1
2
|
export { install, installedBytes, isInstalled, megabytes, modelPath, modelsDirectory, sharedModelsDirectory, vadPath, } from "./install.js";
|
|
2
|
-
export { localSpeechRecognitionModel, openLocalSpeechRecognizer, } from "./local.js";
|
|
3
|
-
export { DEFAULT_ORDER, findModel, MODELS, orderedModels, speechModel, VAD, } from "./models.js";
|
|
3
|
+
export { localSpeechRecognitionModel, openLocalSpeechRecognizer, recognizeLocalPieces, } from "./local.js";
|
|
4
|
+
export { DEFAULT_ORDER, findModel, MODELS, OPENAI_TRANSCRIBE_MODEL, orderedModels, speechModel, VAD, } from "./models.js";
|
|
5
|
+
export { LONGEST_REQUEST_SECONDS, transcribeOpenAI } from "./openai.js";
|