@rivus/gateway 0.18.1 → 0.19.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/README.md +4 -0
- package/assets/audio-transcriber/THIRD_PARTY_NOTICES.md +86 -0
- package/assets/audio-transcriber/byte-tokens.mjs +33 -0
- package/assets/audio-transcriber/fetch-model.mjs +225 -0
- package/assets/audio-transcriber/manifest.json +64 -0
- package/assets/audio-transcriber/npm-shrinkwrap.json +345 -0
- package/assets/audio-transcriber/package.json +15 -0
- package/assets/audio-transcriber/transcribe.mjs +411 -0
- package/dist/bootstrap/pi-feishu.js +81 -22
- package/dist/chunks/herdr-feishu-runtime.js +1 -1
- package/dist/chunks/{runtime-cache.js → rivus-home-deployment-env.js} +16 -3
- package/dist/chunks/rivus-node-entrypoint.js +609 -8
- package/package.json +14 -10
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
import { S as deepFreeze, _ as
|
|
1
|
+
import { C as validateRivusDeploymentManifest, S as InvalidRivusProjectSpace, T as deepFreeze, _ as gatewayTemplateDirectory, a as invokeRuntimeControl, b as loadMergedLocalEnvFile, d as renderRivusModelCliArgumentError, f as renderRivusModelCliHelp, h as gatewayPackageManifestPath, i as disposeRuntimeCacheEntries, l as createRivusModelManagementWireRequest, n as withoutRivusHomeDeployment, o as RIVUS_RELEASE_DESCRIPTOR, p as gatewayAudioTranscriberAssetDirectory, r as createRuntimeCache, t as RIVUS_HOME_DEPLOYMENT_ENV, u as parseRivusModelCliArguments, v as loadRivusDeploymentManifest, w as validateRivusProjectSpaceDeployment, x as resolveFeishuEndpointCredentials, y as loadRivusDaemonConfig } from "./rivus-home-deployment-env.js";
|
|
2
2
|
import { n as narrowBackgroundSessionDefinition, t as createBackgroundSessionToolContracts } from "./background-session-authority.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { createSha256Digest, createStableId } from "@rivus/platform/identity";
|
|
5
5
|
import { RIVUS_MEMORY_TOOL_PLUGIN_ID, RIVUS_RUNTIME_TOOL_IDS, createRivusMemoryToolContract, isRivusRuntimeToolId } from "@rivus/runtime";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
|
-
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
|
+
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
|
-
import { Cause, Deferred, Effect, Exit, Result, Semaphore } from "effect";
|
|
10
|
-
import { lstat, mkdir, open, readFile, readdir, realpath, rmdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
11
|
-
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { Cause, Deferred, Effect, Exit, Option, Result, Schema, Semaphore } from "effect";
|
|
10
|
+
import { cp, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
|
|
11
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
12
12
|
import { assertPathWithin, isPathWithin } from "@rivus/platform/filesystem";
|
|
13
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
14
|
+
import { readPersistenceFile, writeAtomicTextFile } from "@rivus/platform/persistence";
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
13
16
|
import { createConnection } from "node:net";
|
|
14
17
|
import { constants as constants$1 } from "node:fs";
|
|
15
18
|
import { unwrapExit } from "@rivus/platform/effect";
|
|
@@ -952,6 +955,561 @@ function createNodeRivusHome(options) {
|
|
|
952
955
|
setup: initializeRivusHome
|
|
953
956
|
};
|
|
954
957
|
}
|
|
958
|
+
const RIVUS_AUDIO_CLI_USAGE = `Usage:
|
|
959
|
+
rivus audio install [--force] [--json]
|
|
960
|
+
rivus audio status
|
|
961
|
+
rivus audio transcribe <path>
|
|
962
|
+
|
|
963
|
+
Commands:
|
|
964
|
+
install Install or upgrade the managed audio transcriber in the current Home's state directory
|
|
965
|
+
status Print the installed and shipped transcriber releases as JSON
|
|
966
|
+
transcribe Print the transcript of one audio file; exits 2 when no speech is detected
|
|
967
|
+
and ${String(3)} when the transcriber is not installed
|
|
968
|
+
`;
|
|
969
|
+
function parseRivusAudioCliArguments(argv) {
|
|
970
|
+
if (argv.includes("--help") || argv.includes("-h")) return { help: true };
|
|
971
|
+
const [operation, ...rest] = argv;
|
|
972
|
+
if (operation === "install") {
|
|
973
|
+
const flags = new Set(rest);
|
|
974
|
+
const unknown = rest.find((argument) => argument !== "--force" && argument !== "--json");
|
|
975
|
+
if (unknown !== void 0) return { error: `Unknown audio install option: ${unknown}` };
|
|
976
|
+
if (flags.size !== rest.length) return { error: "audio install options may only be provided once" };
|
|
977
|
+
return {
|
|
978
|
+
force: flags.has("--force"),
|
|
979
|
+
json: flags.has("--json"),
|
|
980
|
+
operation
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
if (operation === "status") return rest.length === 0 ? { operation } : { error: "audio status accepts no arguments" };
|
|
984
|
+
if (operation === "transcribe") {
|
|
985
|
+
const path = rest[0];
|
|
986
|
+
if (rest.length !== 1 || !path || path.startsWith("-")) return { error: "audio transcribe requires exactly one audio path" };
|
|
987
|
+
return {
|
|
988
|
+
operation,
|
|
989
|
+
path
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
return { error: "audio requires one of install, status, transcribe" };
|
|
993
|
+
}
|
|
994
|
+
function renderRivusAudioCliArgumentError(error) {
|
|
995
|
+
return `${error}\n\n${RIVUS_AUDIO_CLI_USAGE}`;
|
|
996
|
+
}
|
|
997
|
+
function renderRivusAudioCliStatus(status) {
|
|
998
|
+
return `${JSON.stringify(status)}\n`;
|
|
999
|
+
}
|
|
1000
|
+
function renderRivusAudioCliInstallResult(result, json) {
|
|
1001
|
+
if (json) return `${JSON.stringify(result)}\n`;
|
|
1002
|
+
return result.status === "installed" ? `Installed audio transcriber ${result.id} in ${result.release}\n` : `Audio transcriber ${result.id} is already installed in ${result.release}\n`;
|
|
1003
|
+
}
|
|
1004
|
+
function renderRivusAudioCliNotInstalled() {
|
|
1005
|
+
return "audio transcriber is not installed; run: rivus audio install\n";
|
|
1006
|
+
}
|
|
1007
|
+
//#endregion
|
|
1008
|
+
//#region src/adapters/cli/audio/rivus-audio-install-lock.ts
|
|
1009
|
+
var RivusAudioInstallInProgressError = class extends Error {
|
|
1010
|
+
constructor() {
|
|
1011
|
+
super("another audio transcriber installation is in progress");
|
|
1012
|
+
this.name = "RivusAudioInstallInProgressError";
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
const lockOwnerSchema = Schema.fromJsonString(Schema.Struct({
|
|
1016
|
+
pid: Schema.Int,
|
|
1017
|
+
startedAt: Schema.Number,
|
|
1018
|
+
token: Schema.NonEmptyString
|
|
1019
|
+
}));
|
|
1020
|
+
/**
|
|
1021
|
+
* Takes the installation lock at `path`: a directory holding one owner record named by the owner's token. The record
|
|
1022
|
+
* is written into a private draft directory that is then renamed onto `path`, which succeeds only while `path` is
|
|
1023
|
+
* absent or empty, so a reader never observes a partially written lock. A lock whose pid is dead, whose record is
|
|
1024
|
+
* unreadable, or which is older than `maxAgeMs` is reclaimed by removing that record by name and then `path` only
|
|
1025
|
+
* while it is empty, so a reclaim based on an outdated reading never removes another installer's live lock.
|
|
1026
|
+
*/
|
|
1027
|
+
async function acquireRivusAudioInstallLock(path, options) {
|
|
1028
|
+
const now = options.now ?? Date.now;
|
|
1029
|
+
const isProcessAlive = options.isProcessAlive ?? isLiveProcess;
|
|
1030
|
+
const owner = {
|
|
1031
|
+
pid: process.pid,
|
|
1032
|
+
startedAt: now(),
|
|
1033
|
+
token: randomUUID()
|
|
1034
|
+
};
|
|
1035
|
+
const draft = `${path}.${owner.token}.draft`;
|
|
1036
|
+
await mkdir(draft, { mode: 448 });
|
|
1037
|
+
try {
|
|
1038
|
+
await writeFile(join(draft, owner.token), `${JSON.stringify(owner)}\n`, {
|
|
1039
|
+
flag: "wx",
|
|
1040
|
+
mode: 384
|
|
1041
|
+
});
|
|
1042
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1043
|
+
if (await tryPublish(draft, path)) return { release: () => removeRecords(path, [owner.token]) };
|
|
1044
|
+
const records = await readLockRecords(path);
|
|
1045
|
+
if (records.some(({ owner: held }) => held !== void 0 && held.pid !== process.pid && isProcessAlive(held.pid) && now() - held.startedAt < options.maxAgeMs)) throw new RivusAudioInstallInProgressError();
|
|
1046
|
+
await removeRecords(path, records.map(({ name }) => name));
|
|
1047
|
+
}
|
|
1048
|
+
} finally {
|
|
1049
|
+
await rm(draft, {
|
|
1050
|
+
force: true,
|
|
1051
|
+
recursive: true
|
|
1052
|
+
}).catch(() => void 0);
|
|
1053
|
+
}
|
|
1054
|
+
throw new RivusAudioInstallInProgressError();
|
|
1055
|
+
}
|
|
1056
|
+
async function tryPublish(draft, path) {
|
|
1057
|
+
try {
|
|
1058
|
+
await rename(draft, path);
|
|
1059
|
+
return true;
|
|
1060
|
+
} catch (error) {
|
|
1061
|
+
if (hasErrorCode(error, "EEXIST") || hasErrorCode(error, "ENOTEMPTY")) return false;
|
|
1062
|
+
throw error;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
async function readLockRecords(path) {
|
|
1066
|
+
const names = await readdir(path).catch((error) => {
|
|
1067
|
+
if (hasErrorCode(error, "ENOENT")) return [];
|
|
1068
|
+
throw error;
|
|
1069
|
+
});
|
|
1070
|
+
return Promise.all(names.map(async (name) => {
|
|
1071
|
+
const text = await readPersistenceFile(join(path, name));
|
|
1072
|
+
return {
|
|
1073
|
+
name,
|
|
1074
|
+
owner: text === void 0 ? void 0 : Option.getOrUndefined(Schema.decodeUnknownOption(lockOwnerSchema)(text))
|
|
1075
|
+
};
|
|
1076
|
+
}));
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Removes the named owner records and then the lock directory only if nothing else is in it, so a record that another
|
|
1080
|
+
* installer published meanwhile keeps the lock held.
|
|
1081
|
+
*/
|
|
1082
|
+
async function removeRecords(path, names) {
|
|
1083
|
+
await Promise.all(names.map((name) => unlink(join(path, name)).catch(ignoreErrorCodes("ENOENT"))));
|
|
1084
|
+
await rmdir(path).catch(ignoreErrorCodes("ENOENT", "ENOTEMPTY", "EEXIST"));
|
|
1085
|
+
}
|
|
1086
|
+
function ignoreErrorCodes(...codes) {
|
|
1087
|
+
return (error) => {
|
|
1088
|
+
if (!codes.some((code) => hasErrorCode(error, code))) throw error;
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
function isLiveProcess(pid) {
|
|
1092
|
+
try {
|
|
1093
|
+
process.kill(pid, 0);
|
|
1094
|
+
return true;
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
return hasErrorCode(error, "EPERM");
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
function hasErrorCode(error, code) {
|
|
1100
|
+
return error?.code === code;
|
|
1101
|
+
}
|
|
1102
|
+
//#endregion
|
|
1103
|
+
//#region src/adapters/cli/audio/rivus-audio-release.ts
|
|
1104
|
+
const RIVUS_AUDIO_MANIFEST_FILE = "manifest.json";
|
|
1105
|
+
const RIVUS_AUDIO_RELEASES_DIRECTORY = "releases";
|
|
1106
|
+
const manifestSchema = Schema.fromJsonString(Schema.Struct({
|
|
1107
|
+
id: Schema.NonEmptyString,
|
|
1108
|
+
smoke: Schema.Struct({
|
|
1109
|
+
expect: Schema.NonEmptyString,
|
|
1110
|
+
input: Schema.NonEmptyString
|
|
1111
|
+
})
|
|
1112
|
+
}));
|
|
1113
|
+
const currentSchema = Schema.fromJsonString(Schema.Struct({
|
|
1114
|
+
digest: Schema.optional(Schema.NonEmptyString),
|
|
1115
|
+
id: Schema.NonEmptyString,
|
|
1116
|
+
release: Schema.NonEmptyString
|
|
1117
|
+
}));
|
|
1118
|
+
function rivusAudioCurrentPath(installRoot) {
|
|
1119
|
+
return join(installRoot, "current.json");
|
|
1120
|
+
}
|
|
1121
|
+
async function readRivusAudioReleaseManifest(directory) {
|
|
1122
|
+
const path = join(directory, RIVUS_AUDIO_MANIFEST_FILE);
|
|
1123
|
+
const manifest = Option.getOrUndefined(Schema.decodeUnknownOption(manifestSchema)(await readFile(path, "utf8")));
|
|
1124
|
+
if (manifest === void 0 || !isStrictlyWithin(directory, resolve(directory, manifest.smoke.input))) throw new Error(`audio transcriber manifest is invalid: ${path}`);
|
|
1125
|
+
return manifest;
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Whether an entry of the asset directory, at any depth, belongs in a release. Dependencies come from the shrinkwrap
|
|
1129
|
+
* and colocated tests stay out of the published package, so a release never contains either and a workspace checkout
|
|
1130
|
+
* produces the same release and digest as the published package.
|
|
1131
|
+
*/
|
|
1132
|
+
function isRivusAudioReleaseAsset(name) {
|
|
1133
|
+
return name !== "node_modules" && !name.endsWith(".test.ts");
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Digests the asset files that an installation copies, so an asset change reinstalls the transcriber even when the
|
|
1137
|
+
* manifest id stays the same.
|
|
1138
|
+
*/
|
|
1139
|
+
async function readRivusAudioAssetDigest(directory) {
|
|
1140
|
+
const hash = createHash("sha256");
|
|
1141
|
+
for (const file of (await listAssetFiles(directory, "")).sort()) {
|
|
1142
|
+
const content = await readFile(join(directory, file));
|
|
1143
|
+
hash.update(`${file}\0${String(content.length)}\0`).update(content);
|
|
1144
|
+
}
|
|
1145
|
+
return hash.digest("hex");
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Reads the release that current.json selects. A missing or malformed record, a release outside `releases/`, or a
|
|
1149
|
+
* missing release directory all mean "not installed", which a reinstall repairs.
|
|
1150
|
+
*/
|
|
1151
|
+
async function readRivusAudioInstalledRelease(installRoot) {
|
|
1152
|
+
const text = await readPersistenceFile(rivusAudioCurrentPath(installRoot));
|
|
1153
|
+
const current = text === void 0 ? void 0 : Option.getOrUndefined(Schema.decodeUnknownOption(currentSchema)(text));
|
|
1154
|
+
if (current === void 0) return;
|
|
1155
|
+
const directory = resolve(installRoot, current.release);
|
|
1156
|
+
if (!isStrictlyWithin(join(installRoot, "releases"), directory)) return;
|
|
1157
|
+
return await stat(directory).then((info) => info.isDirectory(), () => false) ? {
|
|
1158
|
+
...current.digest === void 0 ? {} : { digest: current.digest },
|
|
1159
|
+
directory,
|
|
1160
|
+
id: current.id,
|
|
1161
|
+
release: current.release
|
|
1162
|
+
} : void 0;
|
|
1163
|
+
}
|
|
1164
|
+
async function readRivusAudioStatus(installRoot, assetDirectory) {
|
|
1165
|
+
const expected = await readRivusAudioReleaseManifest(assetDirectory);
|
|
1166
|
+
const expectedDigest = await readRivusAudioAssetDigest(assetDirectory);
|
|
1167
|
+
const installed = await readRivusAudioInstalledRelease(installRoot);
|
|
1168
|
+
return installed === void 0 ? {
|
|
1169
|
+
installed: false,
|
|
1170
|
+
expectedId: expected.id,
|
|
1171
|
+
expectedDigest,
|
|
1172
|
+
upToDate: false
|
|
1173
|
+
} : {
|
|
1174
|
+
installed: true,
|
|
1175
|
+
id: installed.id,
|
|
1176
|
+
...installed.digest === void 0 ? {} : { digest: installed.digest },
|
|
1177
|
+
release: installed.directory,
|
|
1178
|
+
expectedId: expected.id,
|
|
1179
|
+
expectedDigest,
|
|
1180
|
+
upToDate: installed.id === expected.id && installed.digest === expectedDigest
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
/** Lists the release asset files by relative path with "/" separators. */
|
|
1184
|
+
async function listAssetFiles(root, prefix) {
|
|
1185
|
+
const entries = await readdir(join(root, prefix), { withFileTypes: true });
|
|
1186
|
+
return (await Promise.all(entries.filter((entry) => isRivusAudioReleaseAsset(entry.name)).map(async (entry) => {
|
|
1187
|
+
const path = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
|
|
1188
|
+
return entry.isDirectory() ? listAssetFiles(root, path) : [path];
|
|
1189
|
+
}))).flat();
|
|
1190
|
+
}
|
|
1191
|
+
function isStrictlyWithin(root, candidate) {
|
|
1192
|
+
return candidate !== resolve(root) && isPathWithin(root, candidate);
|
|
1193
|
+
}
|
|
1194
|
+
//#endregion
|
|
1195
|
+
//#region src/adapters/cli/audio/rivus-audio-installer.ts
|
|
1196
|
+
const INSTALL_DEADLINE_MS = 15e5;
|
|
1197
|
+
const NPM_TIMEOUT_MS = 3e5;
|
|
1198
|
+
const FETCH_TIMEOUT_MS = 12e5;
|
|
1199
|
+
const SMOKE_TIMEOUT_MS = 3e5;
|
|
1200
|
+
const STALE_LOCK_MS = 2 * INSTALL_DEADLINE_MS;
|
|
1201
|
+
const LOCK_POLL_MS = 2e3;
|
|
1202
|
+
const STAGING_PREFIX = "staging-";
|
|
1203
|
+
var RivusAudioInstallStepError = class extends Error {
|
|
1204
|
+
step;
|
|
1205
|
+
constructor(step, reason, output = "") {
|
|
1206
|
+
const detail = output.trim().slice(-4e3);
|
|
1207
|
+
super(`audio transcriber installation failed at ${step}: ${reason}${detail ? `\n${detail}` : ""}`);
|
|
1208
|
+
this.name = "RivusAudioInstallStepError";
|
|
1209
|
+
this.step = step;
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
/**
|
|
1213
|
+
* Builds a release in a staging directory beside `releases/`, smoke-tests it, then publishes it by renaming the
|
|
1214
|
+
* directory and atomically replacing current.json. A failure before current.json changes leaves the previous release
|
|
1215
|
+
* selected and removes the staging directory. While another installer holds the lock, this one waits within its
|
|
1216
|
+
* deadline and then rechecks current.json, so it does not rebuild a release that installer just published.
|
|
1217
|
+
*/
|
|
1218
|
+
async function installRivusAudioTranscriber(options) {
|
|
1219
|
+
const now = options.now ?? Date.now;
|
|
1220
|
+
const deadlineMs = options.deadlineMs ?? INSTALL_DEADLINE_MS;
|
|
1221
|
+
const deadline = {
|
|
1222
|
+
at: now() + deadlineMs,
|
|
1223
|
+
ms: deadlineMs,
|
|
1224
|
+
now
|
|
1225
|
+
};
|
|
1226
|
+
const root = resolve(options.installRoot);
|
|
1227
|
+
await mkdir(root, { recursive: true });
|
|
1228
|
+
const lock = await acquireLockWhenFree(root, options, deadline);
|
|
1229
|
+
try {
|
|
1230
|
+
await removeInterruptedStaging(root);
|
|
1231
|
+
const manifest = await readRivusAudioReleaseManifest(options.assetDirectory);
|
|
1232
|
+
const digest = await readRivusAudioAssetDigest(options.assetDirectory);
|
|
1233
|
+
const previous = await readRivusAudioInstalledRelease(root);
|
|
1234
|
+
if (!options.force && previous?.id === manifest.id && previous.digest === digest) return {
|
|
1235
|
+
status: "already-installed",
|
|
1236
|
+
id: previous.id,
|
|
1237
|
+
release: previous.directory
|
|
1238
|
+
};
|
|
1239
|
+
const release = await buildRelease(root, manifest, digest, options, deadline);
|
|
1240
|
+
await pruneReleases(root, [release, previous?.directory]);
|
|
1241
|
+
return {
|
|
1242
|
+
status: "installed",
|
|
1243
|
+
id: manifest.id,
|
|
1244
|
+
release
|
|
1245
|
+
};
|
|
1246
|
+
} finally {
|
|
1247
|
+
await lock.release().catch(() => void 0);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
async function acquireLockWhenFree(root, options, deadline) {
|
|
1251
|
+
const sleep = options.sleep ?? ((ms) => sleepUnlessAborted(ms, options.signal));
|
|
1252
|
+
for (;;) {
|
|
1253
|
+
try {
|
|
1254
|
+
return await acquireRivusAudioInstallLock(join(root, "install.lock"), {
|
|
1255
|
+
maxAgeMs: STALE_LOCK_MS,
|
|
1256
|
+
...options.isProcessAlive ? { isProcessAlive: options.isProcessAlive } : {}
|
|
1257
|
+
});
|
|
1258
|
+
} catch (error) {
|
|
1259
|
+
if (!(error instanceof RivusAudioInstallInProgressError)) throw error;
|
|
1260
|
+
}
|
|
1261
|
+
const remainingMs = deadline.at - deadline.now();
|
|
1262
|
+
if (remainingMs <= 0) throw new Error("another audio transcriber installation did not finish in time");
|
|
1263
|
+
await sleep(Math.min(LOCK_POLL_MS, remainingMs));
|
|
1264
|
+
if (options.signal?.aborted) throw new Error("audio transcriber installation was interrupted while waiting for another installation");
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
function sleepUnlessAborted(ms, signal) {
|
|
1268
|
+
return setTimeout$1(ms, void 0, signal ? { signal } : {}).catch(() => void 0);
|
|
1269
|
+
}
|
|
1270
|
+
async function buildRelease(root, manifest, digest, options, deadline) {
|
|
1271
|
+
const staging = join(root, `${STAGING_PREFIX}${randomUUID()}`);
|
|
1272
|
+
const release = join(root, RIVUS_AUDIO_RELEASES_DIRECTORY, `${manifest.id.replaceAll(/[^\w.-]/g, "_")}-${digest.slice(0, 12)}-${randomUUID().slice(0, 8)}`);
|
|
1273
|
+
const env = withNodeOnPath(options.env, options.nodeExecutable);
|
|
1274
|
+
let renamed = false;
|
|
1275
|
+
try {
|
|
1276
|
+
await cp(options.assetDirectory, staging, {
|
|
1277
|
+
filter: (source) => isRivusAudioReleaseAsset(basename(source)),
|
|
1278
|
+
recursive: true
|
|
1279
|
+
}).catch((error) => {
|
|
1280
|
+
throw new RivusAudioInstallStepError("copy assets", errorMessage(error));
|
|
1281
|
+
});
|
|
1282
|
+
await runStep(options, deadline, "npm ci", {
|
|
1283
|
+
args: [
|
|
1284
|
+
"ci",
|
|
1285
|
+
"--ignore-scripts",
|
|
1286
|
+
"--omit=dev",
|
|
1287
|
+
"--no-audit",
|
|
1288
|
+
"--no-fund"
|
|
1289
|
+
],
|
|
1290
|
+
command: "npm",
|
|
1291
|
+
cwd: staging,
|
|
1292
|
+
env,
|
|
1293
|
+
timeoutMs: NPM_TIMEOUT_MS
|
|
1294
|
+
});
|
|
1295
|
+
await runStep(options, deadline, "model download", {
|
|
1296
|
+
args: [
|
|
1297
|
+
"fetch-model.mjs",
|
|
1298
|
+
RIVUS_AUDIO_MANIFEST_FILE,
|
|
1299
|
+
"model"
|
|
1300
|
+
],
|
|
1301
|
+
command: options.nodeExecutable,
|
|
1302
|
+
cwd: staging,
|
|
1303
|
+
env,
|
|
1304
|
+
timeoutMs: FETCH_TIMEOUT_MS
|
|
1305
|
+
});
|
|
1306
|
+
const { RIVUS_AUDIO_LANGUAGE: _language, ...smokeEnv } = env;
|
|
1307
|
+
const smoke = await runStep(options, deadline, "smoke test", {
|
|
1308
|
+
args: ["transcribe.mjs", resolve(staging, manifest.smoke.input)],
|
|
1309
|
+
command: options.nodeExecutable,
|
|
1310
|
+
cwd: staging,
|
|
1311
|
+
env: smokeEnv,
|
|
1312
|
+
timeoutMs: SMOKE_TIMEOUT_MS
|
|
1313
|
+
});
|
|
1314
|
+
if (smoke.stderrTail !== "") throw new RivusAudioInstallStepError("smoke test", "the transcriber wrote to stderr", smoke.stderrTail);
|
|
1315
|
+
if (!smoke.stdout.includes(manifest.smoke.expect)) throw new RivusAudioInstallStepError("smoke test", `the transcript does not contain ${JSON.stringify(manifest.smoke.expect)}`, smoke.stdout);
|
|
1316
|
+
await mkdir(dirname(release), { recursive: true });
|
|
1317
|
+
await rename(staging, release);
|
|
1318
|
+
renamed = true;
|
|
1319
|
+
await writeAtomicTextFile(rivusAudioCurrentPath(root), `${JSON.stringify({
|
|
1320
|
+
id: manifest.id,
|
|
1321
|
+
digest,
|
|
1322
|
+
release: relative(root, release)
|
|
1323
|
+
})}\n`, { durable: true });
|
|
1324
|
+
return release;
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
if ((renamed ? await readRivusAudioInstalledRelease(root).then((installed) => installed?.directory, () => void 0) : void 0) !== release) await rm(renamed ? release : staging, {
|
|
1327
|
+
force: true,
|
|
1328
|
+
recursive: true
|
|
1329
|
+
}).catch(() => void 0);
|
|
1330
|
+
throw error instanceof RivusAudioInstallStepError ? error : new RivusAudioInstallStepError("publish", errorMessage(error));
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
async function runStep(options, deadline, step, request) {
|
|
1334
|
+
if (options.signal?.aborted) throw new RivusAudioInstallStepError(step, "interrupted");
|
|
1335
|
+
const deadlineReason = `the installation did not finish within ${seconds(deadline.ms)} s`;
|
|
1336
|
+
const remainingMs = deadline.at - deadline.now();
|
|
1337
|
+
if (remainingMs <= 0) throw new RivusAudioInstallStepError(step, deadlineReason);
|
|
1338
|
+
const timeoutMs = Math.min(request.timeoutMs, remainingMs);
|
|
1339
|
+
const result = await options.runProcess({
|
|
1340
|
+
...request,
|
|
1341
|
+
timeoutMs,
|
|
1342
|
+
...options.signal ? { signal: options.signal } : {}
|
|
1343
|
+
}).catch((error) => {
|
|
1344
|
+
throw new RivusAudioInstallStepError(step, `could not start ${request.command}: ${errorMessage(error)}`);
|
|
1345
|
+
});
|
|
1346
|
+
if (result.outcome === "timed-out") throw new RivusAudioInstallStepError(step, timeoutMs < request.timeoutMs ? deadlineReason : `timed out after ${seconds(request.timeoutMs)} s`, result.stderrTail);
|
|
1347
|
+
if (result.outcome === "interrupted") throw new RivusAudioInstallStepError(step, "interrupted", result.stderrTail);
|
|
1348
|
+
if (result.exitCode !== 0) throw new RivusAudioInstallStepError(step, `exited with code ${String(result.exitCode)}`, result.stderrTail);
|
|
1349
|
+
return result;
|
|
1350
|
+
}
|
|
1351
|
+
async function removeInterruptedStaging(root) {
|
|
1352
|
+
const entries = await readdir(root);
|
|
1353
|
+
await Promise.all(entries.filter((name) => name.startsWith(STAGING_PREFIX)).map((name) => join(root, name)).map((path) => rm(path, {
|
|
1354
|
+
force: true,
|
|
1355
|
+
maxRetries: 5,
|
|
1356
|
+
recursive: true
|
|
1357
|
+
}).catch((error) => {
|
|
1358
|
+
throw new RivusAudioInstallStepError("staging cleanup", `could not remove the staging directory of an interrupted installation, ${path}: ${errorMessage(error)}`);
|
|
1359
|
+
})));
|
|
1360
|
+
}
|
|
1361
|
+
/** Keeps the new and the previous release, so a transcription that started before the switch can still finish. */
|
|
1362
|
+
async function pruneReleases(root, keep) {
|
|
1363
|
+
const directory = join(root, RIVUS_AUDIO_RELEASES_DIRECTORY);
|
|
1364
|
+
const kept = new Set(keep.filter((path) => path !== void 0));
|
|
1365
|
+
const entries = await readdir(directory).catch(() => []);
|
|
1366
|
+
await Promise.all(entries.map((name) => join(directory, name)).filter((path) => !kept.has(path)).map((path) => rm(path, {
|
|
1367
|
+
force: true,
|
|
1368
|
+
recursive: true
|
|
1369
|
+
}).catch(() => void 0)));
|
|
1370
|
+
}
|
|
1371
|
+
/** npm may be missing from a service PATH, but it is installed beside the running Node executable. */
|
|
1372
|
+
function withNodeOnPath(env, nodeExecutable) {
|
|
1373
|
+
const nodeDirectory = dirname(nodeExecutable);
|
|
1374
|
+
return {
|
|
1375
|
+
...env,
|
|
1376
|
+
PATH: env.PATH ? `${nodeDirectory}${delimiter}${env.PATH}` : nodeDirectory
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
function seconds(ms) {
|
|
1380
|
+
return String(ms / 1e3);
|
|
1381
|
+
}
|
|
1382
|
+
function errorMessage(error) {
|
|
1383
|
+
return error instanceof Error ? error.message : String(error);
|
|
1384
|
+
}
|
|
1385
|
+
//#endregion
|
|
1386
|
+
//#region src/adapters/cli/audio/rivus-audio-process.ts
|
|
1387
|
+
const OUTPUT_LIMIT = 65536;
|
|
1388
|
+
const KILL_GRACE_MS = 2e3;
|
|
1389
|
+
/**
|
|
1390
|
+
* Runs one installation step. This CLI's own timeout or interruption signals only the step process, so a step must not
|
|
1391
|
+
* start processes of its own: npm ci runs with --ignore-scripts and the asset scripts are plain Node.
|
|
1392
|
+
*/
|
|
1393
|
+
function runRivusAudioProcess(request) {
|
|
1394
|
+
return new Promise((resolve, reject) => {
|
|
1395
|
+
let stdout = "";
|
|
1396
|
+
let stderrTail = "";
|
|
1397
|
+
const child = spawnInCallerGroup(request, (text) => {
|
|
1398
|
+
stdout = stdout.length >= OUTPUT_LIMIT ? stdout : (stdout + text).slice(0, OUTPUT_LIMIT);
|
|
1399
|
+
}, (text) => {
|
|
1400
|
+
stderrTail = (stderrTail + text).slice(-65536);
|
|
1401
|
+
});
|
|
1402
|
+
let outcome = "exited";
|
|
1403
|
+
let exitCode = null;
|
|
1404
|
+
let exited = false;
|
|
1405
|
+
let escalation;
|
|
1406
|
+
const stop = (reason) => {
|
|
1407
|
+
if (exited || outcome !== "exited") return;
|
|
1408
|
+
outcome = reason;
|
|
1409
|
+
child.kill("SIGTERM");
|
|
1410
|
+
escalation = setTimeout(() => child.kill("SIGKILL"), KILL_GRACE_MS);
|
|
1411
|
+
};
|
|
1412
|
+
const onAbort = () => stop("interrupted");
|
|
1413
|
+
const timeout = setTimeout(() => stop("timed-out"), request.timeoutMs);
|
|
1414
|
+
const finish = () => {
|
|
1415
|
+
clearTimeout(timeout);
|
|
1416
|
+
clearTimeout(escalation);
|
|
1417
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
1418
|
+
};
|
|
1419
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1420
|
+
if (request.signal?.aborted) onAbort();
|
|
1421
|
+
child.once("exit", (code) => {
|
|
1422
|
+
exited = true;
|
|
1423
|
+
exitCode = code;
|
|
1424
|
+
});
|
|
1425
|
+
child.once("error", (error) => {
|
|
1426
|
+
finish();
|
|
1427
|
+
reject(error);
|
|
1428
|
+
});
|
|
1429
|
+
child.once("close", () => {
|
|
1430
|
+
finish();
|
|
1431
|
+
resolve({
|
|
1432
|
+
exitCode,
|
|
1433
|
+
outcome,
|
|
1434
|
+
stderrTail,
|
|
1435
|
+
stdout
|
|
1436
|
+
});
|
|
1437
|
+
});
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
/** Streams the transcriber's stdout and stderr to the caller and resolves with its exit code. */
|
|
1441
|
+
function runRivusAudioPassthroughProcess(request) {
|
|
1442
|
+
return new Promise((resolve, reject) => {
|
|
1443
|
+
const child = spawnInCallerGroup(request, (text) => request.stdout.write(text), (text) => request.stderr.write(text));
|
|
1444
|
+
const onAbort = () => {
|
|
1445
|
+
child.kill("SIGTERM");
|
|
1446
|
+
};
|
|
1447
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1448
|
+
child.once("error", (error) => {
|
|
1449
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
1450
|
+
reject(error);
|
|
1451
|
+
});
|
|
1452
|
+
child.once("close", (code) => {
|
|
1453
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
1454
|
+
resolve(code);
|
|
1455
|
+
});
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Starts a child in the caller's process group, because Pi stops a timed-out or aborted bash command by killing that
|
|
1460
|
+
* group and the child must stop with it.
|
|
1461
|
+
*/
|
|
1462
|
+
function spawnInCallerGroup(request, onStdout, onStderr) {
|
|
1463
|
+
const child = spawn(request.command, [...request.args], {
|
|
1464
|
+
cwd: request.cwd,
|
|
1465
|
+
env: { ...request.env },
|
|
1466
|
+
stdio: [
|
|
1467
|
+
"ignore",
|
|
1468
|
+
"pipe",
|
|
1469
|
+
"pipe"
|
|
1470
|
+
]
|
|
1471
|
+
});
|
|
1472
|
+
child.stdout.setEncoding("utf8").on("data", onStdout);
|
|
1473
|
+
child.stderr.setEncoding("utf8").on("data", onStderr);
|
|
1474
|
+
return child;
|
|
1475
|
+
}
|
|
1476
|
+
//#endregion
|
|
1477
|
+
//#region src/adapters/cli/audio/rivus-audio-cli.ts
|
|
1478
|
+
/** Runs one parsed `rivus audio` command and returns its exit code; installation failures are thrown. */
|
|
1479
|
+
async function runRivusAudioCliCommand(command, context) {
|
|
1480
|
+
if (command.operation === "install") {
|
|
1481
|
+
const result = await installRivusAudioTranscriber({
|
|
1482
|
+
assetDirectory: context.assetDirectory,
|
|
1483
|
+
env: context.env,
|
|
1484
|
+
force: command.force,
|
|
1485
|
+
installRoot: context.installRoot,
|
|
1486
|
+
nodeExecutable: context.nodeExecutable,
|
|
1487
|
+
runProcess: context.runProcess,
|
|
1488
|
+
...context.signal ? { signal: context.signal } : {}
|
|
1489
|
+
});
|
|
1490
|
+
context.stdout.write(renderRivusAudioCliInstallResult(result, command.json));
|
|
1491
|
+
return 0;
|
|
1492
|
+
}
|
|
1493
|
+
if (command.operation === "status") {
|
|
1494
|
+
context.stdout.write(renderRivusAudioCliStatus(await readRivusAudioStatus(context.installRoot, context.assetDirectory)));
|
|
1495
|
+
return 0;
|
|
1496
|
+
}
|
|
1497
|
+
const release = await readRivusAudioInstalledRelease(context.installRoot);
|
|
1498
|
+
if (release === void 0) {
|
|
1499
|
+
context.stderr.write(renderRivusAudioCliNotInstalled());
|
|
1500
|
+
return 3;
|
|
1501
|
+
}
|
|
1502
|
+
const exitCode = await runRivusAudioPassthroughProcess({
|
|
1503
|
+
args: [join(release.directory, "transcribe.mjs"), resolve(context.cwd, command.path)],
|
|
1504
|
+
command: context.nodeExecutable,
|
|
1505
|
+
cwd: release.directory,
|
|
1506
|
+
env: context.env,
|
|
1507
|
+
...context.signal ? { signal: context.signal } : {},
|
|
1508
|
+
stderr: context.stderr,
|
|
1509
|
+
stdout: context.stdout
|
|
1510
|
+
});
|
|
1511
|
+
return exitCode === 0 || exitCode === 1 || exitCode === 2 ? exitCode : 1;
|
|
1512
|
+
}
|
|
955
1513
|
//#endregion
|
|
956
1514
|
//#region src/adapters/cli/command/rivus-cli-protocol.ts
|
|
957
1515
|
const RIVUS_CLI_USAGE = `Usage:
|
|
@@ -962,6 +1520,7 @@ const RIVUS_CLI_USAGE = `Usage:
|
|
|
962
1520
|
rivus init [directory]
|
|
963
1521
|
rivus doctor [directory] [--env-file <path>]
|
|
964
1522
|
rivus model <status|set|rollback> ...
|
|
1523
|
+
rivus audio <install|status|transcribe> ...
|
|
965
1524
|
rivus --bootstrap <module> [--manifest <rivus.config.json>] [options]
|
|
966
1525
|
|
|
967
1526
|
Commands:
|
|
@@ -973,6 +1532,7 @@ Commands:
|
|
|
973
1532
|
init Create a standalone local Rivus project without overwriting files
|
|
974
1533
|
doctor Check Rivus Home by default, or an explicit standalone project directory
|
|
975
1534
|
model Query or change the managed default model through the current Home
|
|
1535
|
+
audio Install, inspect, or run the managed audio transcriber of the current Home
|
|
976
1536
|
|
|
977
1537
|
Run rivus --help for the complete daemon option list.
|
|
978
1538
|
`;
|
|
@@ -1945,6 +2505,7 @@ Project commands:
|
|
|
1945
2505
|
rivus init [directory]
|
|
1946
2506
|
rivus doctor [directory] [--env-file <path>]
|
|
1947
2507
|
rivus model <status|set|rollback> ...
|
|
2508
|
+
rivus audio <install|status|transcribe> ...
|
|
1948
2509
|
|
|
1949
2510
|
Options:
|
|
1950
2511
|
--bootstrap <module> Module exporting createRivusDaemonProcess(context)
|
|
@@ -3948,7 +4509,10 @@ function runRivusDaemonCli(options) {
|
|
|
3948
4509
|
return 0;
|
|
3949
4510
|
}
|
|
3950
4511
|
const env = yield* Effect.tryPromise({
|
|
3951
|
-
try: () =>
|
|
4512
|
+
try: async () => {
|
|
4513
|
+
const loaded = await loadCliEnv(parsed.envFilePath, options.env);
|
|
4514
|
+
return options.env["RIVUS_HOME_DEPLOYMENT_DIR"] === void 0 ? withoutRivusHomeDeployment(loaded) : loaded;
|
|
4515
|
+
},
|
|
3952
4516
|
catch: (error) => error
|
|
3953
4517
|
});
|
|
3954
4518
|
const bootstrapSpecifier = parsed.bootstrap ?? env.RIVUS_BOOTSTRAP_MODULE?.trim();
|
|
@@ -4159,11 +4723,15 @@ function runRivusCli(options) {
|
|
|
4159
4723
|
if (command === "init") return runInitCommand(options);
|
|
4160
4724
|
if (command === "doctor") return runDoctorCommand(options);
|
|
4161
4725
|
if (command === "model") return runModelCommand(options);
|
|
4726
|
+
if (command === "audio") return runAudioCommand(options);
|
|
4162
4727
|
if (command && !command.startsWith("-")) return Effect.sync(() => {
|
|
4163
4728
|
options.stderr.write(renderRivusCliUnknownCommand(command));
|
|
4164
4729
|
return 1;
|
|
4165
4730
|
});
|
|
4166
|
-
return runRivusDaemonCli(
|
|
4731
|
+
return runRivusDaemonCli({
|
|
4732
|
+
...options,
|
|
4733
|
+
env: withoutRivusHomeDeployment(options.env)
|
|
4734
|
+
});
|
|
4167
4735
|
}
|
|
4168
4736
|
function runModelCommand(options) {
|
|
4169
4737
|
const parsed = parseRivusModelCliArguments(options.argv.slice(1));
|
|
@@ -4223,6 +4791,38 @@ function modelTransportFailure(error) {
|
|
|
4223
4791
|
status: "failed"
|
|
4224
4792
|
};
|
|
4225
4793
|
}
|
|
4794
|
+
function runAudioCommand(options) {
|
|
4795
|
+
const parsed = parseRivusAudioCliArguments(options.argv.slice(1));
|
|
4796
|
+
if ("help" in parsed) return Effect.sync(() => {
|
|
4797
|
+
options.stdout.write(RIVUS_AUDIO_CLI_USAGE);
|
|
4798
|
+
return 0;
|
|
4799
|
+
});
|
|
4800
|
+
if ("error" in parsed) return Effect.sync(() => {
|
|
4801
|
+
options.stderr.write(renderRivusAudioCliArgumentError(parsed.error));
|
|
4802
|
+
return 1;
|
|
4803
|
+
});
|
|
4804
|
+
return resolveHomeDirectoryEffect(options).pipe(Effect.flatMap((directory) => options.homeApi.load(directory)), Effect.flatMap((home) => Effect.tryPromise({
|
|
4805
|
+
try: () => runRivusAudioCliCommand(parsed, {
|
|
4806
|
+
assetDirectory: gatewayAudioTranscriberAssetDirectory,
|
|
4807
|
+
cwd: options.cwd,
|
|
4808
|
+
env: options.env,
|
|
4809
|
+
installRoot: join(home.stateDirectory, "audio-transcriber"),
|
|
4810
|
+
nodeExecutable: options.nodeExecutable,
|
|
4811
|
+
runProcess: runRivusAudioProcess,
|
|
4812
|
+
signal: abortOnInterruption(options.signalSource),
|
|
4813
|
+
stderr: options.stderr,
|
|
4814
|
+
stdout: options.stdout
|
|
4815
|
+
}),
|
|
4816
|
+
catch: (error) => error
|
|
4817
|
+
})), Effect.catch((error) => writeError(options, error)));
|
|
4818
|
+
}
|
|
4819
|
+
/** Lets an interrupted install stop its child processes and remove its staging directory before the CLI exits. */
|
|
4820
|
+
function abortOnInterruption(signalSource) {
|
|
4821
|
+
const controller = new AbortController();
|
|
4822
|
+
signalSource.on("SIGINT", () => controller.abort());
|
|
4823
|
+
signalSource.on("SIGTERM", () => controller.abort());
|
|
4824
|
+
return controller.signal;
|
|
4825
|
+
}
|
|
4226
4826
|
function runSetupCommand(options) {
|
|
4227
4827
|
return withOptionalDirectoryArgument(options, "setup", (argument) => resolveHomeDirectoryEffect(options, argument).pipe(Effect.flatMap((directory) => options.homeApi.setup(directory).pipe(Effect.tap(() => Effect.sync(() => options.stdout.write(renderRivusSetupSuccess(directory)))), Effect.as(0))), Effect.catch((error) => writeError(options, error))));
|
|
4228
4828
|
}
|
|
@@ -4245,7 +4845,8 @@ function runHomeDaemonCommand(options, command, daemonArgs) {
|
|
|
4245
4845
|
env: {
|
|
4246
4846
|
...options.env,
|
|
4247
4847
|
RIVUS_DEPLOYMENT_STATE_DIR: home.stateDirectory,
|
|
4248
|
-
RIVUS_HOME: home.directory
|
|
4848
|
+
RIVUS_HOME: home.directory,
|
|
4849
|
+
[RIVUS_HOME_DEPLOYMENT_ENV]: home.directory
|
|
4249
4850
|
},
|
|
4250
4851
|
pluginPackageManifestPath: options.pluginPackageManifestPath ?? gatewayPackageManifestPath
|
|
4251
4852
|
})), Effect.catch((error) => writeError(options, error)));
|