@hasna/recordings 0.2.10 → 0.2.13
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 +350 -35
- package/bun.lock +3 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts +2 -0
- package/dist/__tests__/helpers/native-fs-guard.d.ts.map +1 -0
- package/dist/cli/index.js +1728 -320
- package/dist/cli/macos-permissions.d.ts +13 -0
- package/dist/cli/macos-permissions.d.ts.map +1 -0
- package/dist/cli/options.d.ts +12 -0
- package/dist/cli/options.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts.map +1 -1
- package/dist/db/recordings.d.ts +2 -1
- package/dist/db/recordings.d.ts.map +1 -1
- package/dist/db/remote-storage.d.ts +5 -1
- package/dist/db/remote-storage.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +628 -147
- package/dist/lib/bun-runtime.d.ts +11 -0
- package/dist/lib/bun-runtime.d.ts.map +1 -0
- package/dist/lib/config.d.ts +8 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/enhancer.d.ts +10 -2
- package/dist/lib/enhancer.d.ts.map +1 -1
- package/dist/lib/install-maintenance.d.ts +24 -0
- package/dist/lib/install-maintenance.d.ts.map +1 -0
- package/dist/lib/machine.d.ts +2 -0
- package/dist/lib/machine.d.ts.map +1 -0
- package/dist/lib/recorder.d.ts +2 -1
- package/dist/lib/recorder.d.ts.map +1 -1
- package/dist/lib/recording-create-identity.d.ts +13 -0
- package/dist/lib/recording-create-identity.d.ts.map +1 -0
- package/dist/lib/release-install-policy.d.ts +31 -0
- package/dist/lib/release-install-policy.d.ts.map +1 -0
- package/dist/lib/transcriber.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +977 -194
- package/dist/sdk/index.js +5 -1
- package/dist/sdk/v1.generated.d.ts +4 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud-config.d.ts +9 -0
- package/dist/server/cloud-config.d.ts.map +1 -0
- package/dist/server/cloud-readiness.d.ts +10 -0
- package/dist/server/cloud-readiness.d.ts.map +1 -0
- package/dist/server/cloud.d.ts +9 -18
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +1654 -304
- package/dist/server/migrate-command.d.ts +11 -0
- package/dist/server/migrate-command.d.ts.map +1 -0
- package/dist/server/openapi.d.ts +22 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/repo.d.ts +8 -1
- package/dist/server/repo.d.ts.map +1 -1
- package/dist/server/serve.d.ts +6 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage.js +515 -80
- package/dist/store.d.ts +3 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +7 -4
- package/packaging/macos/Empty.entitlements +5 -0
- package/packaging/macos/Library/LaunchDaemons/com.hasna.recordings.updater.plist +34 -0
- package/packaging/macos/Verifier.entitlements +10 -0
- package/packaging/macos/artifact-verifier.sb +33 -0
- package/packaging/macos/build_release_pkg.sh +872 -0
- package/packaging/macos/managed_bootstrap.sh +623 -0
- package/packaging/macos/pkgutil_fingerprint.awk +37 -0
- package/packaging/macos/release_lifecycle.ts +463 -0
- package/packaging/macos/scripts/postinstall +565 -0
- package/packaging/macos/scripts/preinstall +297 -0
- package/scripts/build_companion_cli.sh +337 -0
- package/scripts/build_native_fs_guard.sh +59 -0
- package/scripts/generate-sdk.ts +19 -1
- package/scripts/install_macos_app.sh +1922 -77
- package/scripts/macos_artifact.ts +5316 -0
- package/scripts/migrate.ts +38 -10
- package/scripts/native/prebuilds/darwin-universal/recordings_fs_guard.node +0 -0
- package/scripts/native/recordings_fs_guard.c +1167 -0
- package/scripts/native_fs_guard.ts +158 -0
- package/scripts/release-guard.ts +20 -1
- package/scripts/resolve_tailscale_cli.sh +389 -0
- package/scripts/smoke_macos_app.sh +573 -0
- package/src/native/Recordings/App/ContentView.swift +83 -0
- package/src/native/Recordings/App/MenuBarStatusView.swift +102 -0
- package/src/native/Recordings/App/RecordWorkspaceView.swift +468 -0
- package/src/native/Recordings/App/RecordingDetailView.swift +138 -0
- package/src/native/Recordings/App/RecordingsApp.swift +367 -44
- package/src/native/Recordings/App/RecordingsListView.swift +130 -0
- package/src/native/Recordings/App/RecordingsStore.swift +206 -0
- package/src/native/Recordings/App/RuntimeSmoke.swift +158 -0
- package/src/native/Recordings/App/SidebarView.swift +212 -0
- package/src/native/Recordings/App/Theme.swift +87 -0
- package/src/native/Recordings/Package.resolved +5 -5
- package/src/native/Recordings/Package.swift +62 -2
- package/src/native/Recordings/RecordingsLib/AccessibilityPromptGate.swift +104 -0
- package/src/native/Recordings/RecordingsLib/BrandAssets.swift +0 -32
- package/src/native/Recordings/RecordingsLib/ChromeSurface.swift +17 -0
- package/src/native/Recordings/RecordingsLib/Info.plist +7 -3
- package/src/native/Recordings/RecordingsLib/MenuBarPresentation.swift +37 -0
- package/src/native/Recordings/RecordingsLib/NativeAppDiagnostics.swift +32 -2
- package/src/native/Recordings/RecordingsLib/NativeMachineIdentity.swift +24 -0
- package/src/native/Recordings/RecordingsLib/NativePCMRecorder.swift +262 -23
- package/src/native/Recordings/RecordingsLib/OpenAIAPIKeyStore.swift +56 -0
- package/src/native/Recordings/RecordingsLib/PermissionRequestLaunchPlan.swift +56 -0
- package/src/native/Recordings/RecordingsLib/ProjectStore.swift +321 -25
- package/src/native/Recordings/RecordingsLib/RealtimeTranscriptionClient.swift +605 -90
- package/src/native/Recordings/RecordingsLib/Recording.swift +141 -0
- package/src/native/Recordings/RecordingsLib/RecordingEngine.swift +3704 -376
- package/src/native/Recordings/RecordingsLib/RecordingStartControlPresentation.swift +29 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.entitlements +10 -0
- package/src/native/Recordings/RecordingsLib/RecordingsCLI.swift +222 -0
- package/src/native/Recordings/RecordingsLib/SettingsView.swift +45 -11
- package/src/native/Recordings/RecordingsLib/SpeechIntent.swift +331 -0
- package/src/native/Recordings/RecordingsLib/SpeechIntentClassifier.swift +232 -0
- package/src/native/Recordings/RecordingsLib/VoiceShortcuts.swift +17 -7
- package/src/native/Recordings/RecordingsTests/CLIRunnerTests.swift +1343 -0
- package/src/native/Recordings/RecordingsTests/MenuBarPresentationTests.swift +98 -0
- package/src/native/Recordings/RecordingsTests/NativeAppDiagnosticsTests.swift +20 -0
- package/src/native/Recordings/RecordingsTests/NativePCMRecorderTests.swift +370 -1
- package/src/native/Recordings/RecordingsTests/PasteTargetTests.swift +987 -1
- package/src/native/Recordings/RecordingsTests/ProjectStoreTests.swift +385 -0
- package/src/native/Recordings/RecordingsTests/RealtimeTranscriptionTests.swift +835 -8
- package/src/native/Recordings/RecordingsTests/RecordingBridgeTests.swift +180 -0
- package/src/native/Recordings/RecordingsTests/RecordingEngineDeliveryTests.swift +760 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartControlPresentationTests.swift +42 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartGateTests.swift +264 -0
- package/src/native/Recordings/RecordingsTests/RecordingStartTimingTests.swift +129 -0
- package/src/native/Recordings/RecordingsTests/SpeechIntentTests.swift +600 -0
- package/src/native/Recordings/RecordingsTests/TranscriptResolutionTests.swift +62 -1
- package/src/native/Recordings/RecordingsTests/TranscriptionResultIdentityTests.swift +21 -0
- package/src/native/Recordings/Updater/BootstrapPreflight/BootstrapPreflightMain.swift +739 -0
- package/src/native/Recordings/Updater/Broker/ActivationRecoveryPolicy.swift +254 -0
- package/src/native/Recordings/Updater/Broker/ApplicationNamespace.swift +463 -0
- package/src/native/Recordings/Updater/Broker/ApplicationProcessQuiescence.swift +55 -0
- package/src/native/Recordings/Updater/Broker/ArtifactIngest.swift +341 -0
- package/src/native/Recordings/Updater/Broker/AtomicActivation.swift +477 -0
- package/src/native/Recordings/Updater/Broker/BrokerMain.swift +624 -0
- package/src/native/Recordings/Updater/Broker/CanonicalTreeCopy.swift +63 -0
- package/src/native/Recordings/Updater/Broker/CodeValidation.swift +561 -0
- package/src/native/Recordings/Updater/Broker/DarwinACLValidator.swift +66 -0
- package/src/native/Recordings/Updater/Broker/HostOSProductVersion.swift +50 -0
- package/src/native/Recordings/Updater/Broker/InstallJournal.swift +362 -0
- package/src/native/Recordings/Updater/Broker/InstallRecovery.swift +662 -0
- package/src/native/Recordings/Updater/Broker/MonotonicState.swift +456 -0
- package/src/native/Recordings/Updater/Broker/PeerIdentity.swift +274 -0
- package/src/native/Recordings/Updater/Broker/VerifierRunner.swift +81 -0
- package/src/native/Recordings/Updater/BrokerTests/ActivationRecoveryPolicyTests.swift +259 -0
- package/src/native/Recordings/Updater/BrokerTests/CanonicalReleaseOrderTests.swift +21 -0
- package/src/native/Recordings/Updater/Client/ClientMain.swift +119 -0
- package/src/native/Recordings/Updater/Protocol/BoundedProcess.swift +159 -0
- package/src/native/Recordings/Updater/Protocol/CandidateMetadataPolicy.swift +214 -0
- package/src/native/Recordings/Updater/Protocol/HostOSVersionPolicy.swift +55 -0
- package/src/native/Recordings/Updater/Protocol/MonotonicReleasePolicy.swift +152 -0
- package/src/native/Recordings/Updater/Protocol/ReleaseEnvelope.swift +229 -0
- package/src/native/Recordings/Updater/Protocol/UpdateProtocol.swift +119 -0
- package/src/native/Recordings/Updater/ProtocolTests/BoundedProcessRunnerTests.swift +60 -0
- package/src/native/Recordings/Updater/ProtocolTests/CandidateMetadataPolicyTests.swift +168 -0
- package/src/native/Recordings/Updater/ProtocolTests/HostOSVersionPolicyTests.swift +62 -0
- package/src/native/Recordings/Updater/ProtocolTests/MonotonicReleasePolicyTests.swift +172 -0
- package/src/native/Recordings/Updater/ProtocolTests/ReleaseEnvelopeValidationTests.swift +65 -0
- package/src/native/Recordings/Updater/Signer/SignerMain.swift +210 -0
- package/src/native/Recordings/Updater/VerifierLauncher/RecordingsVerifierLauncher.c +725 -0
- package/src/native/Recordings/Updater/VerifierLauncher/include/RecordingsVerifierLauncher.h +33 -0
- package/src/native/Recordings/build.sh +1990 -29
- package/src/native/Recordings/RecordingsLib/MenuBarPopover.swift +0 -380
package/dist/index.js
CHANGED
|
@@ -30,8 +30,12 @@ class RecordingsV1Client {
|
|
|
30
30
|
const url = new URL(this.baseUrl + path);
|
|
31
31
|
if (opts.query) {
|
|
32
32
|
for (const [key, value] of Object.entries(opts.query)) {
|
|
33
|
-
if (value
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
for (const item of value)
|
|
35
|
+
url.searchParams.append(key, String(item));
|
|
36
|
+
} else if (value !== undefined && value !== null) {
|
|
34
37
|
url.searchParams.set(key, String(value));
|
|
38
|
+
}
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
41
|
const headers = { Accept: "application/json", ...this.baseHeaders, ...opts.init?.headers };
|
|
@@ -194,23 +198,200 @@ class SqliteAdapter {
|
|
|
194
198
|
}
|
|
195
199
|
|
|
196
200
|
// src/db/database.ts
|
|
197
|
-
import { mkdirSync as
|
|
198
|
-
import { dirname as
|
|
201
|
+
import { mkdirSync as mkdirSync3 } from "fs";
|
|
202
|
+
import { dirname as dirname3 } from "path";
|
|
199
203
|
|
|
200
204
|
// src/lib/config.ts
|
|
201
|
-
import { copyFileSync, existsSync, readFileSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
202
|
-
import { dirname, join, resolve } from "path";
|
|
205
|
+
import { copyFileSync, existsSync, readFileSync, mkdirSync as mkdirSync2, readdirSync, realpathSync as realpathSync2, statSync } from "fs";
|
|
206
|
+
import { dirname as dirname2, join as join2, resolve as resolve2, sep as sep2 } from "path";
|
|
207
|
+
import { homedir as homedir2 } from "os";
|
|
208
|
+
|
|
209
|
+
// src/lib/install-maintenance.ts
|
|
210
|
+
import { randomUUID } from "crypto";
|
|
211
|
+
import {
|
|
212
|
+
chmodSync,
|
|
213
|
+
lstatSync,
|
|
214
|
+
mkdirSync,
|
|
215
|
+
realpathSync,
|
|
216
|
+
renameSync,
|
|
217
|
+
rmSync,
|
|
218
|
+
writeFileSync
|
|
219
|
+
} from "fs";
|
|
203
220
|
import { homedir } from "os";
|
|
221
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
222
|
+
import { spawnSync } from "child_process";
|
|
223
|
+
var INSTALL_MAINTENANCE_MARKER_NAME = ".recordings-install-maintenance";
|
|
224
|
+
var STORE_READER_LEASES_NAME = ".recordings-store-readers";
|
|
225
|
+
function stateParent(env = process.env) {
|
|
226
|
+
const home = env["HOME"] || env["USERPROFILE"] || homedir();
|
|
227
|
+
return join(home, ".hasna");
|
|
228
|
+
}
|
|
229
|
+
function installMaintenanceMarkerPath(env = process.env) {
|
|
230
|
+
return join(stateParent(env), INSTALL_MAINTENANCE_MARKER_NAME);
|
|
231
|
+
}
|
|
232
|
+
function storeReaderLeasesPath(env = process.env) {
|
|
233
|
+
return join(stateParent(env), STORE_READER_LEASES_NAME);
|
|
234
|
+
}
|
|
235
|
+
function pathExists(path) {
|
|
236
|
+
try {
|
|
237
|
+
lstatSync(path);
|
|
238
|
+
return true;
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (error.code === "ENOENT")
|
|
241
|
+
return false;
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function assertPrivateDirectory(path) {
|
|
246
|
+
const details = lstatSync(path);
|
|
247
|
+
if (!details.isDirectory() || details.isSymbolicLink()) {
|
|
248
|
+
throw new Error(`Local Recordings coordination path is not a secure directory: ${path}`);
|
|
249
|
+
}
|
|
250
|
+
if (typeof process.getuid === "function" && details.uid !== process.getuid()) {
|
|
251
|
+
throw new Error(`Local Recordings coordination path has an unexpected owner: ${path}`);
|
|
252
|
+
}
|
|
253
|
+
if ((details.mode & 18) !== 0) {
|
|
254
|
+
throw new Error(`Local Recordings coordination path is group/world writable: ${path}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function ensureReaderRoot(env) {
|
|
258
|
+
const parent = stateParent(env);
|
|
259
|
+
const readerRoot = storeReaderLeasesPath(env);
|
|
260
|
+
mkdirSync(parent, { recursive: true, mode: 448 });
|
|
261
|
+
assertPrivateDirectory(parent);
|
|
262
|
+
try {
|
|
263
|
+
mkdirSync(readerRoot, { mode: 448 });
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (error.code !== "EEXIST")
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
assertPrivateDirectory(readerRoot);
|
|
269
|
+
return readerRoot;
|
|
270
|
+
}
|
|
271
|
+
function processStartIdentity() {
|
|
272
|
+
const result = spawnSync("/bin/ps", ["-o", "lstart=", "-p", String(process.pid)], {
|
|
273
|
+
encoding: "utf8",
|
|
274
|
+
env: { ...process.env, LC_ALL: "C", LANG: "C", TZ: "UTC0" }
|
|
275
|
+
});
|
|
276
|
+
const identity = result.status === 0 ? result.stdout.trim().replace(/\s+/g, " ") : "";
|
|
277
|
+
if (!identity) {
|
|
278
|
+
throw new Error("Could not establish the local Store reader process identity");
|
|
279
|
+
}
|
|
280
|
+
return identity;
|
|
281
|
+
}
|
|
282
|
+
function maintenanceUnavailable() {
|
|
283
|
+
return new Error("Local Recordings storage is temporarily unavailable during app installation maintenance");
|
|
284
|
+
}
|
|
285
|
+
function assertMaintenanceAbsent(env) {
|
|
286
|
+
if (pathExists(installMaintenanceMarkerPath(env)))
|
|
287
|
+
throw maintenanceUnavailable();
|
|
288
|
+
}
|
|
289
|
+
function canonicalPotentialPath(path) {
|
|
290
|
+
const absolutePath = resolve(path);
|
|
291
|
+
let existingPath = absolutePath;
|
|
292
|
+
const missingEntries = [];
|
|
293
|
+
while (true) {
|
|
294
|
+
try {
|
|
295
|
+
return join(realpathSync(existingPath), ...missingEntries);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (error.code !== "ENOENT")
|
|
298
|
+
return absolutePath;
|
|
299
|
+
const parent = dirname(existingPath);
|
|
300
|
+
if (parent === existingPath)
|
|
301
|
+
return absolutePath;
|
|
302
|
+
missingEntries.unshift(basename(existingPath));
|
|
303
|
+
existingPath = parent;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function pathIsWithin(root, candidate) {
|
|
308
|
+
const relativePath = relative(root, candidate);
|
|
309
|
+
return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
|
|
310
|
+
}
|
|
311
|
+
function isGlobalRecordingsStatePath(path, env = process.env) {
|
|
312
|
+
const globalStateRoot = join(stateParent(env), "recordings");
|
|
313
|
+
return pathIsWithin(resolve(globalStateRoot), resolve(path)) || pathIsWithin(canonicalPotentialPath(globalStateRoot), canonicalPotentialPath(path));
|
|
314
|
+
}
|
|
315
|
+
function removeActiveLease(activeLease, releasedLease) {
|
|
316
|
+
try {
|
|
317
|
+
renameSync(activeLease, releasedLease);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
if (error.code !== "ENOENT")
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
rmSync(releasedLease, { recursive: true, force: true });
|
|
323
|
+
}
|
|
324
|
+
function acquireLocalStoreReaderLease(env = process.env) {
|
|
325
|
+
assertMaintenanceAbsent(env);
|
|
326
|
+
const readerRoot = ensureReaderRoot(env);
|
|
327
|
+
const nonce = randomUUID();
|
|
328
|
+
const pendingLease = join(readerRoot, `.pending-${process.pid}-${nonce}`);
|
|
329
|
+
const activeLease = join(readerRoot, `lease-${process.pid}-${nonce}`);
|
|
330
|
+
const releasedLease = join(readerRoot, `.released-${process.pid}-${nonce}`);
|
|
331
|
+
let active = false;
|
|
332
|
+
try {
|
|
333
|
+
mkdirSync(pendingLease, { mode: 448 });
|
|
334
|
+
const owner = join(pendingLease, "owner");
|
|
335
|
+
writeFileSync(owner, `${process.pid}
|
|
336
|
+
${processStartIdentity()}
|
|
337
|
+
`, {
|
|
338
|
+
encoding: "utf8",
|
|
339
|
+
flag: "wx",
|
|
340
|
+
mode: 384
|
|
341
|
+
});
|
|
342
|
+
chmodSync(owner, 384);
|
|
343
|
+
renameSync(pendingLease, activeLease);
|
|
344
|
+
active = true;
|
|
345
|
+
assertMaintenanceAbsent(env);
|
|
346
|
+
} catch (error) {
|
|
347
|
+
if (active)
|
|
348
|
+
removeActiveLease(activeLease, releasedLease);
|
|
349
|
+
else
|
|
350
|
+
rmSync(pendingLease, { recursive: true, force: true });
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
let released = false;
|
|
354
|
+
return () => {
|
|
355
|
+
if (released)
|
|
356
|
+
return;
|
|
357
|
+
removeActiveLease(activeLease, releasedLease);
|
|
358
|
+
released = true;
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
async function withLocalStoreReaderLease(operation, env = process.env) {
|
|
362
|
+
const release = acquireLocalStoreReaderLease(env);
|
|
363
|
+
try {
|
|
364
|
+
return await operation();
|
|
365
|
+
} finally {
|
|
366
|
+
release();
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/lib/config.ts
|
|
371
|
+
var POST_PROCESSING_MODES = new Set([
|
|
372
|
+
"off",
|
|
373
|
+
"auto",
|
|
374
|
+
"always"
|
|
375
|
+
]);
|
|
376
|
+
var DEFAULT_TRANSCRIPTION_MODEL = "gpt-4o-transcribe";
|
|
377
|
+
var DEFAULT_REALTIME_SESSION_MODEL = "gpt-realtime";
|
|
378
|
+
var DEFAULT_REALTIME_TRANSCRIPTION_MODEL = "gpt-realtime-whisper";
|
|
204
379
|
var DEFAULT_CONFIG = {
|
|
205
380
|
openai_api_key: "",
|
|
206
381
|
enhancement_api_key: "",
|
|
207
|
-
transcription_model:
|
|
382
|
+
transcription_model: DEFAULT_TRANSCRIPTION_MODEL,
|
|
383
|
+
realtime_session_model: DEFAULT_REALTIME_SESSION_MODEL,
|
|
384
|
+
realtime_transcription_model: DEFAULT_REALTIME_TRANSCRIPTION_MODEL,
|
|
208
385
|
enhancement_model: "gpt-4o",
|
|
386
|
+
transcriber_model: "gpt-4o",
|
|
209
387
|
language: "en",
|
|
210
388
|
audio_format: "wav",
|
|
211
389
|
sample_rate: 16000,
|
|
212
390
|
record_command: "sox",
|
|
213
391
|
hotkey: "space",
|
|
392
|
+
transcription_prompt: "",
|
|
393
|
+
transcriber_prompt: "",
|
|
394
|
+
post_processing_mode: "auto",
|
|
214
395
|
auto_enhance: true,
|
|
215
396
|
enhance_triggers: [
|
|
216
397
|
"say it better",
|
|
@@ -227,17 +408,23 @@ var DEFAULT_CONFIG = {
|
|
|
227
408
|
keyword_transforms: {},
|
|
228
409
|
db_path: "",
|
|
229
410
|
audio_dir: "",
|
|
230
|
-
max_recording_seconds: 1800
|
|
411
|
+
max_recording_seconds: 1800,
|
|
412
|
+
config_warnings: []
|
|
231
413
|
};
|
|
232
414
|
function loadConfig(configPath) {
|
|
233
415
|
const config = { ...DEFAULT_CONFIG };
|
|
234
|
-
|
|
416
|
+
config.config_warnings = [];
|
|
417
|
+
let explicitPostProcessingMode = false;
|
|
418
|
+
let explicitTranscriberModel = false;
|
|
419
|
+
const filePath = configPath || findConfigFile() || join2(getDataDir(), "config.json");
|
|
235
420
|
let fileProvidedOpenAIKey = false;
|
|
236
421
|
if (existsSync(filePath)) {
|
|
237
422
|
try {
|
|
238
423
|
const raw = readFileSync(filePath, "utf-8");
|
|
239
424
|
const fileConfig = JSON.parse(raw);
|
|
240
425
|
const expanded = expandEnvBackedConfig(fileConfig);
|
|
426
|
+
explicitPostProcessingMode = typeof fileConfig.post_processing_mode === "string";
|
|
427
|
+
explicitTranscriberModel = typeof fileConfig.transcriber_model === "string";
|
|
241
428
|
Object.assign(config, expanded);
|
|
242
429
|
fileProvidedOpenAIKey = typeof expanded.openai_api_key === "string" && expanded.openai_api_key.length > 0;
|
|
243
430
|
} catch {}
|
|
@@ -254,12 +441,35 @@ function loadConfig(configPath) {
|
|
|
254
441
|
if (process.env.RECORDINGS_MODEL) {
|
|
255
442
|
config.transcription_model = process.env.RECORDINGS_MODEL;
|
|
256
443
|
}
|
|
444
|
+
if (process.env.RECORDINGS_REALTIME_SESSION_MODEL) {
|
|
445
|
+
config.realtime_session_model = process.env.RECORDINGS_REALTIME_SESSION_MODEL;
|
|
446
|
+
}
|
|
447
|
+
if (process.env.RECORDINGS_REALTIME_TRANSCRIPTION_MODEL) {
|
|
448
|
+
config.realtime_transcription_model = process.env.RECORDINGS_REALTIME_TRANSCRIPTION_MODEL;
|
|
449
|
+
}
|
|
257
450
|
if (process.env.RECORDINGS_ENHANCEMENT_MODEL) {
|
|
258
451
|
config.enhancement_model = process.env.RECORDINGS_ENHANCEMENT_MODEL;
|
|
259
452
|
}
|
|
453
|
+
if (process.env.RECORDINGS_TRANSCRIBER_MODEL) {
|
|
454
|
+
config.transcriber_model = process.env.RECORDINGS_TRANSCRIBER_MODEL;
|
|
455
|
+
explicitTranscriberModel = true;
|
|
456
|
+
}
|
|
260
457
|
if (process.env.RECORDINGS_LANGUAGE) {
|
|
261
458
|
config.language = process.env.RECORDINGS_LANGUAGE;
|
|
262
459
|
}
|
|
460
|
+
if (process.env.RECORDINGS_TRANSCRIPTION_PROMPT) {
|
|
461
|
+
config.transcription_prompt = process.env.RECORDINGS_TRANSCRIPTION_PROMPT;
|
|
462
|
+
}
|
|
463
|
+
if (process.env.RECORDINGS_TRANSCRIBER_PROMPT) {
|
|
464
|
+
config.transcriber_prompt = process.env.RECORDINGS_TRANSCRIBER_PROMPT;
|
|
465
|
+
}
|
|
466
|
+
if (process.env.RECORDINGS_POST_PROCESSING_MODE) {
|
|
467
|
+
config.post_processing_mode = normalizePostProcessingMode(process.env.RECORDINGS_POST_PROCESSING_MODE, config.post_processing_mode ?? "auto");
|
|
468
|
+
explicitPostProcessingMode = true;
|
|
469
|
+
}
|
|
470
|
+
if (process.env.RECORDINGS_AUTO_ENHANCE) {
|
|
471
|
+
config.auto_enhance = parseBooleanEnv(process.env.RECORDINGS_AUTO_ENHANCE, config.auto_enhance);
|
|
472
|
+
}
|
|
263
473
|
if (process.env.HASNA_RECORDINGS_DB_PATH) {
|
|
264
474
|
config.db_path = process.env.HASNA_RECORDINGS_DB_PATH;
|
|
265
475
|
} else if (process.env.RECORDINGS_DB_PATH) {
|
|
@@ -277,14 +487,86 @@ function loadConfig(configPath) {
|
|
|
277
487
|
if (!config.enhancement_api_key) {
|
|
278
488
|
config.enhancement_api_key = config.openai_api_key || loadSecretKey("OPENAI_API_KEY");
|
|
279
489
|
}
|
|
490
|
+
if (!explicitTranscriberModel) {
|
|
491
|
+
config.transcriber_model = config.enhancement_model;
|
|
492
|
+
}
|
|
493
|
+
normalizeModelSlots(config);
|
|
494
|
+
normalizePostProcessingConfig(config, explicitPostProcessingMode);
|
|
280
495
|
if (!config.db_path) {
|
|
281
|
-
config.db_path =
|
|
496
|
+
config.db_path = join2(getDataDir(), "recordings.db");
|
|
282
497
|
}
|
|
283
498
|
if (!config.audio_dir) {
|
|
284
|
-
config.audio_dir =
|
|
499
|
+
config.audio_dir = join2(getDataDir(), "audio");
|
|
500
|
+
}
|
|
501
|
+
return config;
|
|
502
|
+
}
|
|
503
|
+
function normalizeModelSlots(config) {
|
|
504
|
+
const warnings = config.config_warnings ?? [];
|
|
505
|
+
config.config_warnings = warnings;
|
|
506
|
+
const boundedModel = config.transcription_model?.trim() || DEFAULT_TRANSCRIPTION_MODEL;
|
|
507
|
+
if (isRealtimeOnlyModel(boundedModel)) {
|
|
508
|
+
warnings.push(`Ignoring RECORDINGS_MODEL=${boundedModel}; bounded transcription uses ${DEFAULT_TRANSCRIPTION_MODEL}.`);
|
|
509
|
+
config.transcription_model = DEFAULT_TRANSCRIPTION_MODEL;
|
|
510
|
+
} else {
|
|
511
|
+
config.transcription_model = boundedModel;
|
|
512
|
+
}
|
|
513
|
+
const realtimeSessionModel = config.realtime_session_model?.trim() || DEFAULT_REALTIME_SESSION_MODEL;
|
|
514
|
+
if (isTranscriptionOnlyModel(realtimeSessionModel)) {
|
|
515
|
+
warnings.push(`Ignoring realtime session model ${realtimeSessionModel}; use ${DEFAULT_REALTIME_TRANSCRIPTION_MODEL} as realtime_transcription_model instead.`);
|
|
516
|
+
config.realtime_session_model = DEFAULT_REALTIME_SESSION_MODEL;
|
|
517
|
+
} else {
|
|
518
|
+
config.realtime_session_model = realtimeSessionModel;
|
|
519
|
+
}
|
|
520
|
+
const realtimeTranscriptionModel = config.realtime_transcription_model?.trim() || DEFAULT_REALTIME_TRANSCRIPTION_MODEL;
|
|
521
|
+
if (!isRealtimeTranscriptionModel(realtimeTranscriptionModel)) {
|
|
522
|
+
warnings.push(`Ignoring realtime transcription model ${realtimeTranscriptionModel}; realtime transcription uses ${DEFAULT_REALTIME_TRANSCRIPTION_MODEL}.`);
|
|
523
|
+
config.realtime_transcription_model = DEFAULT_REALTIME_TRANSCRIPTION_MODEL;
|
|
524
|
+
} else {
|
|
525
|
+
config.realtime_transcription_model = realtimeTranscriptionModel;
|
|
526
|
+
}
|
|
527
|
+
return config;
|
|
528
|
+
}
|
|
529
|
+
function isTranscriptionOnlyModel(model) {
|
|
530
|
+
const m = model.trim().toLowerCase();
|
|
531
|
+
return m === "whisper-1" || m === DEFAULT_REALTIME_TRANSCRIPTION_MODEL || m.includes("transcribe");
|
|
532
|
+
}
|
|
533
|
+
function isRealtimeOnlyModel(model) {
|
|
534
|
+
const m = model.trim().toLowerCase();
|
|
535
|
+
return m.startsWith("gpt-realtime");
|
|
536
|
+
}
|
|
537
|
+
function isRealtimeTranscriptionModel(model) {
|
|
538
|
+
const m = model.trim().toLowerCase();
|
|
539
|
+
return m === DEFAULT_REALTIME_TRANSCRIPTION_MODEL || m.startsWith("gpt-realtime") && m.includes("whisper");
|
|
540
|
+
}
|
|
541
|
+
function normalizePostProcessingMode(value, fallback = "auto") {
|
|
542
|
+
const mode = value?.trim().toLowerCase();
|
|
543
|
+
if (mode && POST_PROCESSING_MODES.has(mode)) {
|
|
544
|
+
return mode;
|
|
545
|
+
}
|
|
546
|
+
return fallback;
|
|
547
|
+
}
|
|
548
|
+
function normalizePostProcessingConfig(config, preferPostProcessingMode = true) {
|
|
549
|
+
if (preferPostProcessingMode) {
|
|
550
|
+
config.post_processing_mode = normalizePostProcessingMode(config.post_processing_mode, "auto");
|
|
551
|
+
config.auto_enhance = config.post_processing_mode !== "off";
|
|
552
|
+
return config;
|
|
553
|
+
}
|
|
554
|
+
if (config.auto_enhance === false) {
|
|
555
|
+
config.post_processing_mode = "off";
|
|
556
|
+
} else {
|
|
557
|
+
config.post_processing_mode = normalizePostProcessingMode(config.post_processing_mode, "auto");
|
|
285
558
|
}
|
|
559
|
+
config.auto_enhance = config.post_processing_mode !== "off";
|
|
286
560
|
return config;
|
|
287
561
|
}
|
|
562
|
+
function parseBooleanEnv(value, fallback) {
|
|
563
|
+
const normalized = value.trim().toLowerCase();
|
|
564
|
+
if (["1", "true", "yes", "on"].includes(normalized))
|
|
565
|
+
return true;
|
|
566
|
+
if (["0", "false", "no", "off"].includes(normalized))
|
|
567
|
+
return false;
|
|
568
|
+
return fallback;
|
|
569
|
+
}
|
|
288
570
|
function expandEnvBackedConfig(config) {
|
|
289
571
|
const expanded = { ...config };
|
|
290
572
|
for (const key of ["openai_api_key", "enhancement_api_key"]) {
|
|
@@ -303,36 +585,68 @@ function getDataDir() {
|
|
|
303
585
|
if (projectLocalDir)
|
|
304
586
|
return projectLocalDir;
|
|
305
587
|
const home = getHomeDir();
|
|
306
|
-
const newDir =
|
|
307
|
-
const oldDir =
|
|
588
|
+
const newDir = join2(home, ".hasna", "recordings");
|
|
589
|
+
const oldDir = join2(home, ".recordings");
|
|
308
590
|
if (existsSync(oldDir)) {
|
|
591
|
+
const releaseLease = acquireLocalStoreReaderLease();
|
|
309
592
|
try {
|
|
310
|
-
|
|
311
|
-
|
|
593
|
+
try {
|
|
594
|
+
if (existsSync(oldDir))
|
|
595
|
+
mergeDirectoryContents(oldDir, newDir);
|
|
596
|
+
} catch {}
|
|
597
|
+
} finally {
|
|
598
|
+
releaseLease();
|
|
599
|
+
}
|
|
312
600
|
}
|
|
313
601
|
return newDir;
|
|
314
602
|
}
|
|
315
603
|
function findProjectRecordingsPath(entry) {
|
|
316
|
-
const
|
|
317
|
-
|
|
604
|
+
const cwd = canonicalExistingPath(process.cwd());
|
|
605
|
+
const home = canonicalExistingPath(getHomeDir());
|
|
606
|
+
const repositoryRoot = findRepositoryRoot(cwd);
|
|
607
|
+
const cwdIsInsideHome = cwd === home || cwd.startsWith(`${home}${sep2}`);
|
|
608
|
+
const repositoryIsInsideHome = repositoryRoot !== null && repositoryRoot !== home && repositoryRoot.startsWith(`${home}${sep2}`);
|
|
609
|
+
const boundary = cwdIsInsideHome ? repositoryIsInsideHome ? repositoryRoot : home : repositoryRoot ?? cwd;
|
|
610
|
+
const excludeBoundary = boundary === home;
|
|
611
|
+
let dir = cwd;
|
|
318
612
|
while (true) {
|
|
319
|
-
if (dir
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
613
|
+
if (excludeBoundary && dir === boundary)
|
|
614
|
+
break;
|
|
615
|
+
const candidate = entry ? join2(dir, ".recordings", entry) : join2(dir, ".recordings");
|
|
616
|
+
if (existsSync(candidate))
|
|
617
|
+
return candidate;
|
|
618
|
+
if (dir === boundary)
|
|
619
|
+
break;
|
|
620
|
+
const parent = dirname2(dir);
|
|
325
621
|
if (parent === dir)
|
|
326
622
|
break;
|
|
327
623
|
dir = parent;
|
|
328
624
|
}
|
|
329
625
|
return null;
|
|
330
626
|
}
|
|
627
|
+
function findRepositoryRoot(start) {
|
|
628
|
+
let dir = start;
|
|
629
|
+
while (true) {
|
|
630
|
+
if (existsSync(join2(dir, ".git")))
|
|
631
|
+
return dir;
|
|
632
|
+
const parent = dirname2(dir);
|
|
633
|
+
if (parent === dir)
|
|
634
|
+
return null;
|
|
635
|
+
dir = parent;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function canonicalExistingPath(path) {
|
|
639
|
+
try {
|
|
640
|
+
return realpathSync2(path);
|
|
641
|
+
} catch {
|
|
642
|
+
return resolve2(path);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
331
645
|
function mergeDirectoryContents(sourceDir, targetDir) {
|
|
332
|
-
|
|
646
|
+
mkdirSync2(targetDir, { recursive: true });
|
|
333
647
|
for (const entry of readdirSync(sourceDir)) {
|
|
334
|
-
const sourcePath =
|
|
335
|
-
const targetPath =
|
|
648
|
+
const sourcePath = join2(sourceDir, entry);
|
|
649
|
+
const targetPath = join2(targetDir, entry);
|
|
336
650
|
const sourceStats = statSync(sourcePath);
|
|
337
651
|
if (sourceStats.isDirectory()) {
|
|
338
652
|
mergeDirectoryContents(sourcePath, targetPath);
|
|
@@ -342,7 +656,7 @@ function mergeDirectoryContents(sourceDir, targetDir) {
|
|
|
342
656
|
}
|
|
343
657
|
}
|
|
344
658
|
function loadSecretKey(keyName) {
|
|
345
|
-
const secretsPath =
|
|
659
|
+
const secretsPath = join2(getHomeDir(), ".secrets");
|
|
346
660
|
if (!existsSync(secretsPath))
|
|
347
661
|
return "";
|
|
348
662
|
for (const candidate of listSecretFiles(secretsPath)) {
|
|
@@ -369,7 +683,7 @@ function listSecretFiles(path) {
|
|
|
369
683
|
if (!stats.isDirectory())
|
|
370
684
|
return [];
|
|
371
685
|
return readdirSync(path).sort().flatMap((entry) => {
|
|
372
|
-
const child =
|
|
686
|
+
const child = join2(path, entry);
|
|
373
687
|
try {
|
|
374
688
|
const childStats = statSync(child);
|
|
375
689
|
if (childStats.isDirectory())
|
|
@@ -384,14 +698,19 @@ function listSecretFiles(path) {
|
|
|
384
698
|
}
|
|
385
699
|
}
|
|
386
700
|
function getHomeDir() {
|
|
387
|
-
return process.env["HOME"] || process.env["USERPROFILE"] ||
|
|
701
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir2();
|
|
388
702
|
}
|
|
389
703
|
function ensureDataDir(config) {
|
|
390
|
-
const { mkdirSync:
|
|
391
|
-
mkdirSync2(config.audio_dir, { recursive: true });
|
|
704
|
+
const { mkdirSync: mkdirSync3 } = __require("fs");
|
|
392
705
|
const dbDir = config.db_path.substring(0, config.db_path.lastIndexOf("/"));
|
|
393
|
-
|
|
394
|
-
|
|
706
|
+
const releaseLease = [config.audio_dir, dbDir].some((path) => path.length > 0 && isGlobalRecordingsStatePath(path)) ? acquireLocalStoreReaderLease() : () => {};
|
|
707
|
+
try {
|
|
708
|
+
mkdirSync3(config.audio_dir, { recursive: true });
|
|
709
|
+
if (dbDir)
|
|
710
|
+
mkdirSync3(dbDir, { recursive: true });
|
|
711
|
+
} finally {
|
|
712
|
+
releaseLease();
|
|
713
|
+
}
|
|
395
714
|
}
|
|
396
715
|
|
|
397
716
|
// src/db/database.ts
|
|
@@ -480,8 +799,8 @@ function getDatabase(dbPath) {
|
|
|
480
799
|
if (_db)
|
|
481
800
|
return _db;
|
|
482
801
|
const path = dbPath || loadConfig().db_path;
|
|
483
|
-
const dir =
|
|
484
|
-
|
|
802
|
+
const dir = dirname3(path);
|
|
803
|
+
mkdirSync3(dir, { recursive: true });
|
|
485
804
|
_adapter = new SqliteAdapter(path);
|
|
486
805
|
_db = _adapter.raw;
|
|
487
806
|
_db.run("PRAGMA busy_timeout = 5000");
|
|
@@ -537,6 +856,63 @@ function shortUuid() {
|
|
|
537
856
|
return crypto.randomUUID().slice(0, 8);
|
|
538
857
|
}
|
|
539
858
|
|
|
859
|
+
// src/db/errors.ts
|
|
860
|
+
class ProjectNotFoundError extends Error {
|
|
861
|
+
ref;
|
|
862
|
+
constructor(ref) {
|
|
863
|
+
super(`project not found: ${ref}`);
|
|
864
|
+
this.name = "ProjectNotFoundError";
|
|
865
|
+
this.ref = ref;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
class ValidationError extends Error {
|
|
870
|
+
constructor(message) {
|
|
871
|
+
super(message);
|
|
872
|
+
this.name = "ValidationError";
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/lib/recording-create-identity.ts
|
|
877
|
+
var MAX_RECORDING_IDENTITY_LENGTH = 255;
|
|
878
|
+
function validateIdentityValue(value, label, nullMeansAbsent = false) {
|
|
879
|
+
if (value === undefined || nullMeansAbsent && value === null)
|
|
880
|
+
return;
|
|
881
|
+
if (typeof value !== "string") {
|
|
882
|
+
throw new ValidationError(`${label} must be a string`);
|
|
883
|
+
}
|
|
884
|
+
if (value.length === 0) {
|
|
885
|
+
throw new ValidationError(`${label} must not be empty`);
|
|
886
|
+
}
|
|
887
|
+
if (value.length > MAX_RECORDING_IDENTITY_LENGTH) {
|
|
888
|
+
throw new ValidationError(`${label} must not exceed ${MAX_RECORDING_IDENTITY_LENGTH} characters`);
|
|
889
|
+
}
|
|
890
|
+
if (value !== value.trim()) {
|
|
891
|
+
throw new ValidationError(`${label} must not contain leading or trailing whitespace`);
|
|
892
|
+
}
|
|
893
|
+
if (/[\u0000-\u001f\u007f]/.test(value)) {
|
|
894
|
+
throw new ValidationError(`${label} must not contain control characters`);
|
|
895
|
+
}
|
|
896
|
+
if (/[^\u0020-\u007e]/.test(value)) {
|
|
897
|
+
throw new ValidationError(`${label} must contain only printable ASCII characters`);
|
|
898
|
+
}
|
|
899
|
+
return value;
|
|
900
|
+
}
|
|
901
|
+
function recordingCreateIdentity(input, idempotencyKey, options = {}) {
|
|
902
|
+
const bodyId = validateIdentityValue(input.id, "recording id", true);
|
|
903
|
+
const headerKey = validateIdentityValue(idempotencyKey, "idempotency key");
|
|
904
|
+
if (bodyId !== undefined && headerKey !== undefined && bodyId !== headerKey) {
|
|
905
|
+
throw new ValidationError("recording id conflicts with idempotency key");
|
|
906
|
+
}
|
|
907
|
+
const effectiveKey = headerKey ?? bodyId;
|
|
908
|
+
const { id: _runtimeId, ...inputWithoutId } = input;
|
|
909
|
+
const persistedId = options.bindIdempotencyKeyToId === false ? bodyId : effectiveKey;
|
|
910
|
+
return {
|
|
911
|
+
input: persistedId === undefined ? inputWithoutId : { ...inputWithoutId, id: persistedId },
|
|
912
|
+
idempotencyKey: effectiveKey
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
|
|
540
916
|
// src/db/recordings.ts
|
|
541
917
|
function parseRow(row) {
|
|
542
918
|
return {
|
|
@@ -561,20 +937,36 @@ function parseRow(row) {
|
|
|
561
937
|
created_at: row["created_at"]
|
|
562
938
|
};
|
|
563
939
|
}
|
|
564
|
-
function createRecording(input, db) {
|
|
940
|
+
function createRecording(input, db, idempotencyKey) {
|
|
565
941
|
const d = db || getDatabase();
|
|
566
|
-
|
|
567
|
-
const
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
for (const tag of input.tags) {
|
|
574
|
-
insertTag.run(id, tag);
|
|
942
|
+
input = recordingCreateIdentity(input, idempotencyKey).input;
|
|
943
|
+
const id = input.id || shortUuid();
|
|
944
|
+
const create = d.transaction(() => {
|
|
945
|
+
if (input.id) {
|
|
946
|
+
const existing = getRecording(input.id, d);
|
|
947
|
+
if (existing?.id === input.id)
|
|
948
|
+
return existing;
|
|
575
949
|
}
|
|
576
|
-
|
|
577
|
-
|
|
950
|
+
const tagsJson = JSON.stringify(input.tags || []);
|
|
951
|
+
const metadataJson = JSON.stringify(input.metadata || {});
|
|
952
|
+
const insertResult = d.query(`INSERT INTO recordings (id, audio_path, raw_text, processed_text, processing_mode, model_used, enhancement_model, duration_ms, language, tags, agent_id, project_id, session_id, goal, role, task_list_id, machine_id, metadata)
|
|
953
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
954
|
+
ON CONFLICT(id) DO NOTHING`).run(id, input.audio_path || null, input.raw_text, input.processed_text || null, input.processing_mode || "raw", input.model_used || "gpt-4o-transcribe", input.enhancement_model || null, input.duration_ms || 0, input.language || null, tagsJson, input.agent_id || null, input.project_id || null, input.session_id || null, input.goal || null, input.role || null, input.task_list_id || null, input.machine_id || null, metadataJson);
|
|
955
|
+
if (insertResult.changes === 0) {
|
|
956
|
+
const existing = getRecording(id, d);
|
|
957
|
+
if (existing?.id === id)
|
|
958
|
+
return existing;
|
|
959
|
+
throw new Error("recording id conflict could not be read back");
|
|
960
|
+
}
|
|
961
|
+
if (input.tags && input.tags.length > 0) {
|
|
962
|
+
const insertTag = d.query("INSERT OR IGNORE INTO recording_tags (recording_id, tag) VALUES (?, ?)");
|
|
963
|
+
for (const tag of input.tags) {
|
|
964
|
+
insertTag.run(id, tag);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return getRecording(id, d);
|
|
968
|
+
});
|
|
969
|
+
return create();
|
|
578
970
|
}
|
|
579
971
|
function getRecording(id, db) {
|
|
580
972
|
const d = db || getDatabase();
|
|
@@ -586,6 +978,20 @@ function getRecording(id, db) {
|
|
|
586
978
|
}
|
|
587
979
|
function listRecordings(filter, db) {
|
|
588
980
|
const d = db || getDatabase();
|
|
981
|
+
const { where, params } = buildRecordingWhere(filter);
|
|
982
|
+
const limit = filter?.limit || 50;
|
|
983
|
+
const offset = filter?.offset || 0;
|
|
984
|
+
const sql = `SELECT * FROM recordings ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`;
|
|
985
|
+
const rows = d.query(sql).all(...params, limit, offset);
|
|
986
|
+
return rows.map(parseRow);
|
|
987
|
+
}
|
|
988
|
+
function countRecordings(filter, db) {
|
|
989
|
+
const d = db || getDatabase();
|
|
990
|
+
const { where, params } = buildRecordingWhere(filter);
|
|
991
|
+
const row = d.query(`SELECT COUNT(*) as c FROM recordings ${where}`).get(...params);
|
|
992
|
+
return row.c;
|
|
993
|
+
}
|
|
994
|
+
function buildRecordingWhere(filter) {
|
|
589
995
|
const conditions = [];
|
|
590
996
|
const params = [];
|
|
591
997
|
if (filter?.agent_id) {
|
|
@@ -624,12 +1030,7 @@ function listRecordings(filter, db) {
|
|
|
624
1030
|
params.push(filter.until);
|
|
625
1031
|
}
|
|
626
1032
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
627
|
-
|
|
628
|
-
const offset = filter?.offset || 0;
|
|
629
|
-
const sql = `SELECT * FROM recordings ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`;
|
|
630
|
-
params.push(limit, offset);
|
|
631
|
-
const rows = d.query(sql).all(...params);
|
|
632
|
-
return rows.map(parseRow);
|
|
1033
|
+
return { where, params };
|
|
633
1034
|
}
|
|
634
1035
|
function deleteRecording(id, db) {
|
|
635
1036
|
const d = db || getDatabase();
|
|
@@ -702,16 +1103,6 @@ function listProjects(db) {
|
|
|
702
1103
|
return rows.map(parseProject);
|
|
703
1104
|
}
|
|
704
1105
|
|
|
705
|
-
// src/db/errors.ts
|
|
706
|
-
class ProjectNotFoundError extends Error {
|
|
707
|
-
ref;
|
|
708
|
-
constructor(ref) {
|
|
709
|
-
super(`project not found: ${ref}`);
|
|
710
|
-
this.name = "ProjectNotFoundError";
|
|
711
|
-
this.ref = ref;
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
|
|
715
1106
|
// src/db/agents.ts
|
|
716
1107
|
function parseAgent(row) {
|
|
717
1108
|
return {
|
|
@@ -777,7 +1168,7 @@ function setAgentFocus(idOrName, projectId, db) {
|
|
|
777
1168
|
}
|
|
778
1169
|
|
|
779
1170
|
// src/version.ts
|
|
780
|
-
var VERSION = "0.2.
|
|
1171
|
+
var VERSION = "0.2.13";
|
|
781
1172
|
|
|
782
1173
|
// src/db/feedback.ts
|
|
783
1174
|
function saveFeedback(input) {
|
|
@@ -1070,6 +1461,7 @@ function resolveStorageClient(name, env = process.env, fetchImpl) {
|
|
|
1070
1461
|
}
|
|
1071
1462
|
|
|
1072
1463
|
// src/store.ts
|
|
1464
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
1073
1465
|
var APP = "recordings";
|
|
1074
1466
|
function listQuery(filter) {
|
|
1075
1467
|
if (!filter)
|
|
@@ -1079,6 +1471,7 @@ function listQuery(filter) {
|
|
|
1079
1471
|
project_id: filter.project_id,
|
|
1080
1472
|
session_id: filter.session_id,
|
|
1081
1473
|
processing_mode: filter.processing_mode,
|
|
1474
|
+
tags: filter.tags,
|
|
1082
1475
|
search: filter.search,
|
|
1083
1476
|
since: filter.since,
|
|
1084
1477
|
until: filter.until,
|
|
@@ -1095,58 +1488,63 @@ function unwrap(res, key) {
|
|
|
1095
1488
|
var localStore = {
|
|
1096
1489
|
mode: "local",
|
|
1097
1490
|
baseUrl: null,
|
|
1098
|
-
async createRecording(input) {
|
|
1099
|
-
return createRecording(input);
|
|
1491
|
+
async createRecording(input, idempotencyKey) {
|
|
1492
|
+
return withLocalStoreReaderLease(() => createRecording(input, undefined, idempotencyKey));
|
|
1100
1493
|
},
|
|
1101
1494
|
async getRecording(id) {
|
|
1102
|
-
return getRecording(id);
|
|
1495
|
+
return withLocalStoreReaderLease(() => getRecording(id));
|
|
1103
1496
|
},
|
|
1104
1497
|
async listRecordings(filter) {
|
|
1105
|
-
return listRecordings(filter);
|
|
1498
|
+
return withLocalStoreReaderLease(() => listRecordings(filter));
|
|
1499
|
+
},
|
|
1500
|
+
async countRecordings(filter) {
|
|
1501
|
+
return withLocalStoreReaderLease(() => countRecordings(filter));
|
|
1106
1502
|
},
|
|
1107
1503
|
async searchRecordings(query, filter) {
|
|
1108
|
-
return searchRecordings(query, filter);
|
|
1504
|
+
return withLocalStoreReaderLease(() => searchRecordings(query, filter));
|
|
1109
1505
|
},
|
|
1110
1506
|
async deleteRecording(id) {
|
|
1111
|
-
return deleteRecording(id);
|
|
1507
|
+
return withLocalStoreReaderLease(() => deleteRecording(id));
|
|
1112
1508
|
},
|
|
1113
1509
|
async getRecordingStats() {
|
|
1114
|
-
return getRecordingStats();
|
|
1510
|
+
return withLocalStoreReaderLease(() => getRecordingStats());
|
|
1115
1511
|
},
|
|
1116
1512
|
async registerAgent(name, description, role) {
|
|
1117
|
-
return registerAgent(name, description, role);
|
|
1513
|
+
return withLocalStoreReaderLease(() => registerAgent(name, description, role));
|
|
1118
1514
|
},
|
|
1119
1515
|
async getAgent(idOrName) {
|
|
1120
|
-
return getAgent(idOrName);
|
|
1516
|
+
return withLocalStoreReaderLease(() => getAgent(idOrName));
|
|
1121
1517
|
},
|
|
1122
1518
|
async listAgents() {
|
|
1123
|
-
return listAgents();
|
|
1519
|
+
return withLocalStoreReaderLease(() => listAgents());
|
|
1124
1520
|
},
|
|
1125
1521
|
async heartbeatAgent(idOrName) {
|
|
1126
|
-
return heartbeatAgent(idOrName);
|
|
1522
|
+
return withLocalStoreReaderLease(() => heartbeatAgent(idOrName));
|
|
1127
1523
|
},
|
|
1128
1524
|
async setAgentFocus(idOrName, projectId) {
|
|
1129
|
-
return setAgentFocus(idOrName, projectId);
|
|
1525
|
+
return withLocalStoreReaderLease(() => setAgentFocus(idOrName, projectId));
|
|
1130
1526
|
},
|
|
1131
1527
|
async registerProject(name, path, description) {
|
|
1132
|
-
return registerProject(name, path, description);
|
|
1528
|
+
return withLocalStoreReaderLease(() => registerProject(name, path, description));
|
|
1133
1529
|
},
|
|
1134
1530
|
async getProject(idOrPath) {
|
|
1135
|
-
return getProject(idOrPath);
|
|
1531
|
+
return withLocalStoreReaderLease(() => getProject(idOrPath));
|
|
1136
1532
|
},
|
|
1137
1533
|
async listProjects() {
|
|
1138
|
-
return listProjects();
|
|
1534
|
+
return withLocalStoreReaderLease(() => listProjects());
|
|
1139
1535
|
},
|
|
1140
1536
|
async saveFeedback(input) {
|
|
1141
|
-
saveFeedback(input);
|
|
1537
|
+
await withLocalStoreReaderLease(() => saveFeedback(input));
|
|
1142
1538
|
}
|
|
1143
1539
|
};
|
|
1144
1540
|
function apiStore(client) {
|
|
1145
1541
|
return {
|
|
1146
1542
|
mode: "cloud-http",
|
|
1147
1543
|
baseUrl: client.baseUrl,
|
|
1148
|
-
async createRecording(input) {
|
|
1149
|
-
const
|
|
1544
|
+
async createRecording(input, idempotencyKey) {
|
|
1545
|
+
const keyCandidate = idempotencyKey === undefined && (input.id === undefined || input.id === null) ? randomUUID2() : idempotencyKey;
|
|
1546
|
+
const identity = recordingCreateIdentity(input, keyCandidate, { bindIdempotencyKeyToId: false });
|
|
1547
|
+
const res = await client.create("recordings", identity.input, identity.idempotencyKey);
|
|
1150
1548
|
return unwrap(res, "recording");
|
|
1151
1549
|
},
|
|
1152
1550
|
async getRecording(id) {
|
|
@@ -1157,6 +1555,36 @@ function apiStore(client) {
|
|
|
1157
1555
|
const { items } = await client.list("recordings", listQuery(filter));
|
|
1158
1556
|
return items;
|
|
1159
1557
|
},
|
|
1558
|
+
async countRecordings(filter) {
|
|
1559
|
+
const pageLimit = 500;
|
|
1560
|
+
const maxPageRequests = 1e4;
|
|
1561
|
+
let offset = 0;
|
|
1562
|
+
let pageRequests = 0;
|
|
1563
|
+
const seenPageKeys = new Set;
|
|
1564
|
+
while (pageRequests < maxPageRequests) {
|
|
1565
|
+
pageRequests += 1;
|
|
1566
|
+
const { items, raw } = await client.list("recordings", {
|
|
1567
|
+
...listQuery(filter),
|
|
1568
|
+
limit: pageLimit,
|
|
1569
|
+
offset
|
|
1570
|
+
});
|
|
1571
|
+
const count = raw && typeof raw === "object" ? raw.count : undefined;
|
|
1572
|
+
if (typeof count !== "number" || !Number.isFinite(count)) {
|
|
1573
|
+
throw new Error("Recordings API response is missing a valid count");
|
|
1574
|
+
}
|
|
1575
|
+
if (count > items.length)
|
|
1576
|
+
return count;
|
|
1577
|
+
if (items.length === 0)
|
|
1578
|
+
return offset;
|
|
1579
|
+
offset += items.length;
|
|
1580
|
+
const pageKey = recordingPageFingerprint(items);
|
|
1581
|
+
if (seenPageKeys.has(pageKey)) {
|
|
1582
|
+
throw new Error("Recordings API ignored pagination while counting legacy results");
|
|
1583
|
+
}
|
|
1584
|
+
seenPageKeys.add(pageKey);
|
|
1585
|
+
}
|
|
1586
|
+
throw new Error(`Recordings API exceeded ${maxPageRequests} pages while counting legacy results`);
|
|
1587
|
+
},
|
|
1160
1588
|
async searchRecordings(query, filter) {
|
|
1161
1589
|
const { items } = await client.list("recordings", listQuery({ ...filter ?? {}, search: query }));
|
|
1162
1590
|
return items;
|
|
@@ -1238,6 +1666,17 @@ function apiStore(client) {
|
|
|
1238
1666
|
}
|
|
1239
1667
|
};
|
|
1240
1668
|
}
|
|
1669
|
+
function recordingPageFingerprint(items) {
|
|
1670
|
+
const hash = createHash("sha256");
|
|
1671
|
+
const ids = items.map((item) => String(item.id ?? "")).sort();
|
|
1672
|
+
for (const id of ids) {
|
|
1673
|
+
hash.update(String(id.length));
|
|
1674
|
+
hash.update(":");
|
|
1675
|
+
hash.update(id);
|
|
1676
|
+
hash.update(";");
|
|
1677
|
+
}
|
|
1678
|
+
return `${items.length}:${hash.digest("hex")}`;
|
|
1679
|
+
}
|
|
1241
1680
|
var cached = null;
|
|
1242
1681
|
function getStore(env = process.env) {
|
|
1243
1682
|
if (env === process.env && cached)
|
|
@@ -1255,17 +1694,20 @@ function __resetStore() {
|
|
|
1255
1694
|
import OpenAI from "openai";
|
|
1256
1695
|
import { createReadStream } from "fs";
|
|
1257
1696
|
var _client = null;
|
|
1697
|
+
var _clientApiKey = null;
|
|
1258
1698
|
function getClient(config) {
|
|
1259
|
-
if (_client)
|
|
1260
|
-
return _client;
|
|
1261
1699
|
if (!config.openai_api_key) {
|
|
1262
1700
|
throw new TranscriptionError("OpenAI API key not configured. Set OPENAI_API_KEY env var or add to ~/.secrets");
|
|
1263
1701
|
}
|
|
1702
|
+
if (_client && _clientApiKey === config.openai_api_key)
|
|
1703
|
+
return _client;
|
|
1264
1704
|
_client = new OpenAI({ apiKey: config.openai_api_key });
|
|
1705
|
+
_clientApiKey = config.openai_api_key;
|
|
1265
1706
|
return _client;
|
|
1266
1707
|
}
|
|
1267
1708
|
function resetClient() {
|
|
1268
1709
|
_client = null;
|
|
1710
|
+
_clientApiKey = null;
|
|
1269
1711
|
}
|
|
1270
1712
|
async function transcribeAudio(audioPath, config, options = {}) {
|
|
1271
1713
|
const client = getClient(config);
|
|
@@ -1276,7 +1718,7 @@ async function transcribeAudio(audioPath, config, options = {}) {
|
|
|
1276
1718
|
file: stream,
|
|
1277
1719
|
model: config.transcription_model,
|
|
1278
1720
|
language: config.language || undefined,
|
|
1279
|
-
prompt: buildVerbatimPrompt(options.prompt),
|
|
1721
|
+
prompt: buildVerbatimPrompt(options.prompt ?? config.transcription_prompt),
|
|
1280
1722
|
response_format: "json"
|
|
1281
1723
|
});
|
|
1282
1724
|
stream.destroy();
|
|
@@ -1303,7 +1745,7 @@ async function transcribeBuffer(buffer, filename, config, options = {}) {
|
|
|
1303
1745
|
file,
|
|
1304
1746
|
model: config.transcription_model,
|
|
1305
1747
|
language: config.language || undefined,
|
|
1306
|
-
prompt: buildVerbatimPrompt(options.prompt),
|
|
1748
|
+
prompt: buildVerbatimPrompt(options.prompt ?? config.transcription_prompt),
|
|
1307
1749
|
response_format: "json"
|
|
1308
1750
|
});
|
|
1309
1751
|
const durationMs = Date.now() - startTime;
|
|
@@ -1360,18 +1802,21 @@ function getMimeType(filename) {
|
|
|
1360
1802
|
// src/lib/enhancer.ts
|
|
1361
1803
|
import OpenAI2 from "openai";
|
|
1362
1804
|
var _enhancementClient = null;
|
|
1805
|
+
var _enhancementClientApiKey = null;
|
|
1363
1806
|
function getEnhancementClient(config) {
|
|
1364
|
-
if (_enhancementClient)
|
|
1365
|
-
return _enhancementClient;
|
|
1366
1807
|
const key = config.enhancement_api_key || config.openai_api_key;
|
|
1367
1808
|
if (!key) {
|
|
1368
1809
|
throw new EnhancementError("API key not configured for enhancement. Set OPENAI_API_KEY or RECORDINGS_ENHANCEMENT_KEY");
|
|
1369
1810
|
}
|
|
1811
|
+
if (_enhancementClient && _enhancementClientApiKey === key)
|
|
1812
|
+
return _enhancementClient;
|
|
1370
1813
|
_enhancementClient = new OpenAI2({ apiKey: key });
|
|
1814
|
+
_enhancementClientApiKey = key;
|
|
1371
1815
|
return _enhancementClient;
|
|
1372
1816
|
}
|
|
1373
1817
|
function resetEnhancementClient() {
|
|
1374
1818
|
_enhancementClient = null;
|
|
1819
|
+
_enhancementClientApiKey = null;
|
|
1375
1820
|
}
|
|
1376
1821
|
function needsEnhancement(text, config) {
|
|
1377
1822
|
const lower = text.toLowerCase().trim();
|
|
@@ -1417,6 +1862,7 @@ function extractInstruction(text, trigger) {
|
|
|
1417
1862
|
}
|
|
1418
1863
|
async function enhanceText(rawText, instruction, config, systemPrompt) {
|
|
1419
1864
|
const client = getEnhancementClient(config);
|
|
1865
|
+
const model = resolveTranscriberModel(config);
|
|
1420
1866
|
let basePrompt = `You are a writing assistant. The user has dictated speech that needs to be transformed into polished output.
|
|
1421
1867
|
|
|
1422
1868
|
Rules:
|
|
@@ -1437,13 +1883,14 @@ Rules:
|
|
|
1437
1883
|
Keyword Transformations:
|
|
1438
1884
|
${transformRules}`;
|
|
1439
1885
|
}
|
|
1440
|
-
const
|
|
1886
|
+
const transcriberPrompt = combinePrompts(config.transcriber_prompt, systemPrompt);
|
|
1887
|
+
const fullPrompt = transcriberPrompt ? `${basePrompt}
|
|
1441
1888
|
|
|
1442
|
-
|
|
1443
|
-
${
|
|
1889
|
+
Transcriber instructions:
|
|
1890
|
+
${transcriberPrompt}` : basePrompt;
|
|
1444
1891
|
try {
|
|
1445
1892
|
const response = await client.chat.completions.create({
|
|
1446
|
-
model
|
|
1893
|
+
model,
|
|
1447
1894
|
messages: [
|
|
1448
1895
|
{
|
|
1449
1896
|
role: "system",
|
|
@@ -1461,7 +1908,7 @@ ${systemPrompt}` : basePrompt;
|
|
|
1461
1908
|
return {
|
|
1462
1909
|
original: rawText,
|
|
1463
1910
|
enhanced,
|
|
1464
|
-
model
|
|
1911
|
+
model,
|
|
1465
1912
|
reasoning: null
|
|
1466
1913
|
};
|
|
1467
1914
|
} catch (error) {
|
|
@@ -1469,40 +1916,64 @@ ${systemPrompt}` : basePrompt;
|
|
|
1469
1916
|
throw new EnhancementError(`Enhancement failed: ${describeTranscriptionFailure(msg)}`);
|
|
1470
1917
|
}
|
|
1471
1918
|
}
|
|
1472
|
-
async function processText(rawText, config,
|
|
1473
|
-
const
|
|
1474
|
-
|
|
1475
|
-
|
|
1919
|
+
async function processText(rawText, config, systemPromptOrOptions, legacyOptions) {
|
|
1920
|
+
const options = {
|
|
1921
|
+
...normalizeProcessTextOptions(systemPromptOrOptions),
|
|
1922
|
+
...legacyOptions
|
|
1923
|
+
};
|
|
1924
|
+
const postProcessingMode = options.force ? "always" : options.postProcessingMode ?? normalizePostProcessingMode(config.post_processing_mode, config.auto_enhance === false ? "off" : "auto");
|
|
1925
|
+
if (postProcessingMode === "off") {
|
|
1926
|
+
return {
|
|
1927
|
+
text: rawText,
|
|
1928
|
+
mode: "raw",
|
|
1929
|
+
enhancement_model: null,
|
|
1930
|
+
post_processing_mode: "off",
|
|
1931
|
+
enhancement_reason: null
|
|
1932
|
+
};
|
|
1476
1933
|
}
|
|
1477
|
-
const detection =
|
|
1478
|
-
|
|
1479
|
-
|
|
1934
|
+
const detection = postProcessingMode === "always" ? {
|
|
1935
|
+
needs: true,
|
|
1936
|
+
reason: "Always-on post-processing",
|
|
1937
|
+
instruction: rawText
|
|
1938
|
+
} : needsEnhancement(rawText, config);
|
|
1939
|
+
if (!detection.needs) {
|
|
1940
|
+
return {
|
|
1941
|
+
text: rawText,
|
|
1942
|
+
mode: "raw",
|
|
1943
|
+
enhancement_model: null,
|
|
1944
|
+
post_processing_mode: postProcessingMode,
|
|
1945
|
+
enhancement_reason: detection.reason
|
|
1946
|
+
};
|
|
1480
1947
|
}
|
|
1481
|
-
const
|
|
1482
|
-
const result = await enhanceText(rawText, instruction, config, systemPrompt);
|
|
1948
|
+
const result = await enhanceText(rawText, detection.instruction, config, options.systemPrompt);
|
|
1483
1949
|
return {
|
|
1484
1950
|
text: result.enhanced,
|
|
1485
1951
|
mode: "enhanced",
|
|
1486
|
-
enhancement_model: result.model
|
|
1952
|
+
enhancement_model: result.model,
|
|
1953
|
+
post_processing_mode: postProcessingMode,
|
|
1954
|
+
enhancement_reason: detection.reason
|
|
1487
1955
|
};
|
|
1488
1956
|
}
|
|
1957
|
+
function resolveTranscriberModel(config) {
|
|
1958
|
+
return config.transcriber_model || config.enhancement_model;
|
|
1959
|
+
}
|
|
1960
|
+
function normalizeProcessTextOptions(value) {
|
|
1961
|
+
if (typeof value === "string")
|
|
1962
|
+
return { systemPrompt: value };
|
|
1963
|
+
return value ?? {};
|
|
1964
|
+
}
|
|
1965
|
+
function combinePrompts(...prompts) {
|
|
1966
|
+
return prompts.map((prompt) => prompt?.trim() ?? "").filter(Boolean).join(`
|
|
1967
|
+
|
|
1968
|
+
`);
|
|
1969
|
+
}
|
|
1489
1970
|
// src/lib/recorder.ts
|
|
1490
1971
|
import { spawn } from "child_process";
|
|
1491
|
-
import { join as
|
|
1972
|
+
import { join as join3 } from "path";
|
|
1492
1973
|
import { existsSync as existsSync2 } from "fs";
|
|
1493
1974
|
var _recordProcess = null;
|
|
1494
1975
|
var _currentFile = null;
|
|
1495
1976
|
async function checkRecordingDeps() {
|
|
1496
|
-
try {
|
|
1497
|
-
const proc = Bun.spawn(["which", "sox"], {
|
|
1498
|
-
stdout: "pipe",
|
|
1499
|
-
stderr: "pipe"
|
|
1500
|
-
});
|
|
1501
|
-
await proc.exited;
|
|
1502
|
-
if (proc.exitCode === 0) {
|
|
1503
|
-
return { available: true, tool: "sox", message: "sox is available" };
|
|
1504
|
-
}
|
|
1505
|
-
} catch {}
|
|
1506
1977
|
try {
|
|
1507
1978
|
const proc = Bun.spawn(["which", "rec"], {
|
|
1508
1979
|
stdout: "pipe",
|
|
@@ -1513,20 +1984,6 @@ async function checkRecordingDeps() {
|
|
|
1513
1984
|
return { available: true, tool: "rec", message: "rec is available" };
|
|
1514
1985
|
}
|
|
1515
1986
|
} catch {}
|
|
1516
|
-
try {
|
|
1517
|
-
const proc = Bun.spawn(["which", "ffmpeg"], {
|
|
1518
|
-
stdout: "pipe",
|
|
1519
|
-
stderr: "pipe"
|
|
1520
|
-
});
|
|
1521
|
-
await proc.exited;
|
|
1522
|
-
if (proc.exitCode === 0) {
|
|
1523
|
-
return {
|
|
1524
|
-
available: true,
|
|
1525
|
-
tool: "ffmpeg",
|
|
1526
|
-
message: "ffmpeg is available"
|
|
1527
|
-
};
|
|
1528
|
-
}
|
|
1529
|
-
} catch {}
|
|
1530
1987
|
return {
|
|
1531
1988
|
available: false,
|
|
1532
1989
|
tool: "none",
|
|
@@ -1539,11 +1996,20 @@ function startRecording(config) {
|
|
|
1539
1996
|
}
|
|
1540
1997
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1541
1998
|
const filename = `recording-${timestamp}.${config.audio_format}`;
|
|
1542
|
-
const filepath =
|
|
1999
|
+
const filepath = join3(config.audio_dir, filename);
|
|
1543
2000
|
const args = buildRecordArgs(filepath, config);
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
2001
|
+
const releaseLease = isGlobalRecordingsStatePath(filepath) ? acquireLocalStoreReaderLease() : () => {};
|
|
2002
|
+
let recordProcess;
|
|
2003
|
+
try {
|
|
2004
|
+
recordProcess = spawn(args[0], args.slice(1), {
|
|
2005
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
2006
|
+
});
|
|
2007
|
+
} catch (error) {
|
|
2008
|
+
releaseLease();
|
|
2009
|
+
throw error;
|
|
2010
|
+
}
|
|
2011
|
+
_recordProcess = recordProcess;
|
|
2012
|
+
_currentFile = filepath;
|
|
1547
2013
|
if (config.max_recording_seconds > 0) {
|
|
1548
2014
|
setTimeout(() => {
|
|
1549
2015
|
if (_recordProcess && _currentFile === filepath) {
|
|
@@ -1552,14 +2018,18 @@ function startRecording(config) {
|
|
|
1552
2018
|
}
|
|
1553
2019
|
}, config.max_recording_seconds * 1000);
|
|
1554
2020
|
}
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
2021
|
+
recordProcess.on("error", (err) => {
|
|
2022
|
+
if (_recordProcess === recordProcess) {
|
|
2023
|
+
_recordProcess = null;
|
|
2024
|
+
_currentFile = null;
|
|
2025
|
+
}
|
|
2026
|
+
releaseLease();
|
|
1559
2027
|
throw new RecordingError(`Recording process error: ${err.message}`);
|
|
1560
2028
|
});
|
|
1561
|
-
|
|
1562
|
-
_recordProcess
|
|
2029
|
+
recordProcess.on("exit", () => {
|
|
2030
|
+
if (_recordProcess === recordProcess)
|
|
2031
|
+
_recordProcess = null;
|
|
2032
|
+
releaseLease();
|
|
1563
2033
|
});
|
|
1564
2034
|
return filepath;
|
|
1565
2035
|
}
|
|
@@ -1600,7 +2070,7 @@ function buildRecordArgs(filepath, config) {
|
|
|
1600
2070
|
async function recordDuration(seconds, config) {
|
|
1601
2071
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
1602
2072
|
const filename = `recording-${timestamp}.${config.audio_format}`;
|
|
1603
|
-
const filepath =
|
|
2073
|
+
const filepath = join3(config.audio_dir, filename);
|
|
1604
2074
|
const args = [
|
|
1605
2075
|
"rec",
|
|
1606
2076
|
"-r",
|
|
@@ -1614,16 +2084,21 @@ async function recordDuration(seconds, config) {
|
|
|
1614
2084
|
"0",
|
|
1615
2085
|
seconds.toString()
|
|
1616
2086
|
];
|
|
1617
|
-
const
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
const
|
|
1624
|
-
|
|
2087
|
+
const releaseLease = isGlobalRecordingsStatePath(filepath) ? acquireLocalStoreReaderLease() : () => {};
|
|
2088
|
+
try {
|
|
2089
|
+
const proc = Bun.spawn(args, {
|
|
2090
|
+
stdout: "pipe",
|
|
2091
|
+
stderr: "pipe"
|
|
2092
|
+
});
|
|
2093
|
+
const exitCode = await proc.exited;
|
|
2094
|
+
if (exitCode !== 0 && !existsSync2(filepath)) {
|
|
2095
|
+
const stderr = await new Response(proc.stderr).text();
|
|
2096
|
+
throw new RecordingError(`Recording failed (exit ${exitCode}): ${stderr}`);
|
|
2097
|
+
}
|
|
2098
|
+
return filepath;
|
|
2099
|
+
} finally {
|
|
2100
|
+
releaseLease();
|
|
1625
2101
|
}
|
|
1626
|
-
return filepath;
|
|
1627
2102
|
}
|
|
1628
2103
|
export {
|
|
1629
2104
|
transcribeBuffer,
|
|
@@ -1636,6 +2111,9 @@ export {
|
|
|
1636
2111
|
resetClient,
|
|
1637
2112
|
recordDuration,
|
|
1638
2113
|
processText,
|
|
2114
|
+
normalizePostProcessingMode,
|
|
2115
|
+
normalizePostProcessingConfig,
|
|
2116
|
+
normalizeModelSlots,
|
|
1639
2117
|
needsEnhancement,
|
|
1640
2118
|
loadConfig,
|
|
1641
2119
|
isRecording,
|
|
@@ -1655,6 +2133,9 @@ export {
|
|
|
1655
2133
|
RecordingError,
|
|
1656
2134
|
HasnaHttpError,
|
|
1657
2135
|
EnhancementError,
|
|
2136
|
+
DEFAULT_TRANSCRIPTION_MODEL,
|
|
2137
|
+
DEFAULT_REALTIME_TRANSCRIPTION_MODEL,
|
|
2138
|
+
DEFAULT_REALTIME_SESSION_MODEL,
|
|
1658
2139
|
DEFAULT_CONFIG,
|
|
1659
2140
|
APP
|
|
1660
2141
|
};
|