@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
|
@@ -1,26 +1,52 @@
|
|
|
1
1
|
import { CliError, isCliError } from "@wirecat/cli-base";
|
|
2
2
|
import { fromFile } from "../cli/settings.js";
|
|
3
|
+
import { embeddingKeys } from "../embeddings/keys.js";
|
|
4
|
+
import { readOnlyLockOf, readOnlyRefusal } from "../ports/app.js";
|
|
3
5
|
import { capability } from "../ports/messenger.js";
|
|
4
6
|
import { typedCommand } from "../runs/scope.js";
|
|
5
7
|
import { installedBytes, isInstalled, megabytes, modelPath, modelsDirectory, vadPath } from "./install.js";
|
|
6
|
-
import { orderedModels, speechModel } from "./models.js";
|
|
7
|
-
/**
|
|
8
|
-
|
|
8
|
+
import { OPENAI_TRANSCRIBE_MODEL, orderedModels, speechModel } from "./models.js";
|
|
9
|
+
/**
|
|
10
|
+
* `messenger`: its own recognition (Telegram Premium). `local`: a model on this machine. `auto`: the first, then the
|
|
11
|
+
* second. `openai`: OpenAI's API, only when asked by name — `auto` never sends audio off the machine.
|
|
12
|
+
*/
|
|
13
|
+
export const TRANSCRIBE_WITH = ["auto", "messenger", "local", "openai"];
|
|
9
14
|
/** A recording is read into memory whole; minutes of Opus are a few megabytes. */
|
|
10
15
|
const LARGEST_VOICE = 32 * 1024 * 1024;
|
|
11
16
|
/** A voice note, or an audio file someone sent — an iPhone recording reaches WhatsApp as `audio/mp4`. */
|
|
12
17
|
export const isAudioFile = ({ kind, mime }) => kind === "voice" || kind === "audio" || (mime?.startsWith("audio/") ?? false);
|
|
13
|
-
/** The flag, then the profile's `transcribeWith` and `
|
|
14
|
-
export const choose = (messenger, settings, flags, env = process.env) => {
|
|
18
|
+
/** The flag, then the profile's `transcribeWith`, `speechModel` and `transcribeModel`, then `auto` and the CLI's first model. */
|
|
19
|
+
export const choose = (messenger, settings, flags, env = process.env, keyring) => {
|
|
15
20
|
const model = flags.model ?? fromFile(settings, "speechModel", undefined).value;
|
|
21
|
+
const chosen = flags.local || flags.model !== undefined
|
|
22
|
+
? "local"
|
|
23
|
+
: (flags.with ?? fromFile(settings, "transcribeWith", "auto").value);
|
|
24
|
+
// Refused only where something is heard: `models list` reads the same profile for its model alone.
|
|
25
|
+
const known = TRANSCRIBE_WITH.includes(chosen);
|
|
16
26
|
return {
|
|
17
27
|
directory: modelsDirectory(env),
|
|
18
|
-
with:
|
|
19
|
-
|
|
20
|
-
|
|
28
|
+
with: known ? chosen : "auto",
|
|
29
|
+
...(known
|
|
30
|
+
? {}
|
|
31
|
+
: {
|
|
32
|
+
refusal: new CliError("validation_error", `the profile's transcribeWith is one of ${TRANSCRIBE_WITH.join(", ")}`),
|
|
33
|
+
}),
|
|
21
34
|
model: model === undefined ? orderedModels(messenger.speechModels)[0] : speechModel(model),
|
|
35
|
+
transcribeModel: fromFile(settings, "transcribeModel", OPENAI_TRANSCRIBE_MODEL).value,
|
|
36
|
+
...(known && chosen === "openai" ? openaiAccess(messenger, env, keyring) : {}),
|
|
22
37
|
};
|
|
23
38
|
};
|
|
39
|
+
const openaiAccess = (messenger, env, keyring) => {
|
|
40
|
+
const lock = readOnlyLockOf(messenger.app, env);
|
|
41
|
+
if (lock)
|
|
42
|
+
return { refusal: readOnlyRefusal(lock, "send audio to OpenAI") };
|
|
43
|
+
const apiKey = embeddingKeys(messenger.app, env, keyring).read("openai")?.key;
|
|
44
|
+
return apiKey
|
|
45
|
+
? { apiKey }
|
|
46
|
+
: {
|
|
47
|
+
refusal: new CliError("authentication_error", `no OpenAI key — \`${typedCommand(messenger.app.command, "models keys set openai")}\`, or OPENAI_API_KEY`, { reason: "transcription.no-key" }),
|
|
48
|
+
};
|
|
49
|
+
};
|
|
24
50
|
/** Never a download: a model is hundreds of MB, and whether to fetch one is the owner's call. */
|
|
25
51
|
export const notDownloaded = (messenger, model) => new CliError("not_found", `the speech model ${model.id} is not on this machine — the owner downloads it once with ` +
|
|
26
52
|
`\`${typedCommand(messenger.app.command, `models download ${model.id}`)}\` (${megabytes(installedBytes(model))})`, { reason: "model.not-downloaded", model: model.id });
|
|
@@ -30,7 +56,9 @@ export const notDownloaded = (messenger, model) => new CliError("not_found", `th
|
|
|
30
56
|
* `read` gets the recording once its bytes are all in, for a reference the messenger renewed while they were read.
|
|
31
57
|
*/
|
|
32
58
|
export const hearOnline = async (messenger, connection, chat, messageId, choice, read) => {
|
|
33
|
-
if (choice.
|
|
59
|
+
if (choice.refusal)
|
|
60
|
+
throw choice.refusal;
|
|
61
|
+
if (choice.with === "auto" || choice.with === "messenger") {
|
|
34
62
|
const canAsk = typeof connection.transcribe === "function";
|
|
35
63
|
if (choice.with === "messenger" || canAsk) {
|
|
36
64
|
try {
|
|
@@ -44,7 +72,7 @@ export const hearOnline = async (messenger, connection, chat, messageId, choice,
|
|
|
44
72
|
}
|
|
45
73
|
}
|
|
46
74
|
}
|
|
47
|
-
if (!isInstalled(choice.model, choice.directory))
|
|
75
|
+
if (choice.with !== "openai" && !isInstalled(choice.model, choice.directory))
|
|
48
76
|
throw notDownloaded(messenger, choice.model);
|
|
49
77
|
const { files } = await capability(connection, "download", "download attachments")(chat, messageId);
|
|
50
78
|
const voice = files.find(isAudioFile);
|
|
@@ -54,11 +82,34 @@ export const hearOnline = async (messenger, connection, chat, messageId, choice,
|
|
|
54
82
|
await read?.(voice);
|
|
55
83
|
return bytes;
|
|
56
84
|
};
|
|
57
|
-
/** The local model, on the bytes `hearOnline` fetched. */
|
|
58
|
-
export const hearLocally = async (bytes, messageId,
|
|
59
|
-
const {
|
|
85
|
+
/** The local model, or OpenAI when chosen, on the bytes `hearOnline` fetched. */
|
|
86
|
+
export const hearLocally = async (bytes, messageId, choice) => {
|
|
87
|
+
const { model, directory, open, ffmpeg } = choice;
|
|
60
88
|
const { decodeAudio } = await import("./decode.js");
|
|
61
89
|
const { samples, rate } = await decodeAudio(bytes, ffmpeg);
|
|
90
|
+
if (choice.with === "openai") {
|
|
91
|
+
if (choice.refusal)
|
|
92
|
+
throw choice.refusal;
|
|
93
|
+
const { transcribeOpenAI } = await import("./openai.js");
|
|
94
|
+
const transcribeModel = choice.transcribeModel ?? OPENAI_TRANSCRIBE_MODEL;
|
|
95
|
+
const pieces = await transcribeOpenAI({ samples, rate }, {
|
|
96
|
+
apiKey: choice.apiKey ?? "",
|
|
97
|
+
model: transcribeModel,
|
|
98
|
+
vadModel: vadPath(directory),
|
|
99
|
+
// A voice message wants its text, not its turns: one request, and no voice detector needed.
|
|
100
|
+
bundle: true,
|
|
101
|
+
...(choice.fetch ? { fetch: choice.fetch } : {}),
|
|
102
|
+
...(choice.signal ? { signal: choice.signal } : {}),
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
messageId,
|
|
106
|
+
text: pieces.map(({ text }) => text).join(" "),
|
|
107
|
+
pending: false,
|
|
108
|
+
via: "openai",
|
|
109
|
+
model: transcribeModel,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const { openRecognizer, toModelRate } = await import("./recognize.js");
|
|
62
113
|
const recognizer = (open ?? openRecognizer)(model, modelPath(directory, model), vadPath(directory));
|
|
63
114
|
try {
|
|
64
115
|
return {
|
|
@@ -73,6 +124,14 @@ export const hearLocally = async (bytes, messageId, { model, directory, open, ff
|
|
|
73
124
|
recognizer.free();
|
|
74
125
|
}
|
|
75
126
|
};
|
|
127
|
+
/** Where a kept transcript came from, as `Kept` stores it: the messenger's provider, a local model, or `openai:<model>`. */
|
|
128
|
+
export const keptSource = (heard) => heard.via === "openai" ? `openai:${heard.model ?? OPENAI_TRANSCRIBE_MODEL}` : (heard.model ?? heard.via);
|
|
129
|
+
/** The reverse of `keptSource`. */
|
|
130
|
+
export const heardVia = (source, provider) => source === provider
|
|
131
|
+
? { via: source }
|
|
132
|
+
: source.startsWith("openai:")
|
|
133
|
+
? { via: "openai", model: source.slice("openai:".length) }
|
|
134
|
+
: { via: "local", model: source };
|
|
76
135
|
const readAll = async (file) => {
|
|
77
136
|
const chunks = [];
|
|
78
137
|
let total = 0;
|
package/dist/speech/wav.d.ts
CHANGED
|
@@ -25,3 +25,5 @@ export declare const openWavAudio: (plan: WavAudioPlan, input: WavAudioOptions)
|
|
|
25
25
|
windows(): AsyncIterable<WavPcmWindow>;
|
|
26
26
|
close(): Promise<void>;
|
|
27
27
|
}>;
|
|
28
|
+
/** PCM16 mono WAV of samples in [-1, 1], for a recognizer that takes a file. */
|
|
29
|
+
export declare const encodeWav: (samples: Float32Array, sampleRate?: number) => Uint8Array;
|
package/dist/speech/wav.js
CHANGED
|
@@ -196,3 +196,23 @@ export const openWavAudio = async (plan, input) => {
|
|
|
196
196
|
throw error;
|
|
197
197
|
}
|
|
198
198
|
};
|
|
199
|
+
/** PCM16 mono WAV of samples in [-1, 1], for a recognizer that takes a file. */
|
|
200
|
+
export const encodeWav = (samples, sampleRate = 16000) => {
|
|
201
|
+
const bytes = Buffer.alloc(44 + samples.length * 2);
|
|
202
|
+
bytes.write("RIFF", 0, "ascii");
|
|
203
|
+
bytes.writeUInt32LE(bytes.length - 8, 4);
|
|
204
|
+
bytes.write("WAVEfmt ", 8, "ascii");
|
|
205
|
+
bytes.writeUInt32LE(16, 16);
|
|
206
|
+
bytes.writeUInt16LE(1, 20);
|
|
207
|
+
bytes.writeUInt16LE(1, 22);
|
|
208
|
+
bytes.writeUInt32LE(sampleRate, 24);
|
|
209
|
+
bytes.writeUInt32LE(sampleRate * 2, 28);
|
|
210
|
+
bytes.writeUInt16LE(2, 32);
|
|
211
|
+
bytes.writeUInt16LE(16, 34);
|
|
212
|
+
bytes.write("data", 36, "ascii");
|
|
213
|
+
bytes.writeUInt32LE(samples.length * 2, 40);
|
|
214
|
+
samples.forEach((sample, index) => {
|
|
215
|
+
bytes.writeInt16LE(Math.round(Math.max(-1, Math.min(1, sample || 0)) * 32767), 44 + index * 2);
|
|
216
|
+
});
|
|
217
|
+
return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.length);
|
|
218
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.499.0",
|
|
4
4
|
"description": "The messenger-neutral half of a messaging CLI: domain model, message locators, rendering, name resolution, the SQLite seam, the command skeleton and shared read commands, the send guard, run records and the message store",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|