@remnic/capture-audio 9.63.0 → 9.63.2
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/dist/{chunk-MFWH245M.js → chunk-4Z3DZYEB.js} +229 -62
- package/dist/chunk-4Z3DZYEB.js.map +1 -0
- package/dist/cli-bin.js +1 -1
- package/dist/index.d.ts +177 -144
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
- package/dist/chunk-MFWH245M.js.map +0 -1
package/dist/cli-bin.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ declare const CAPTURE_AUDIO_VERSION = "9.14.0";
|
|
|
12
12
|
declare const DEFAULT_HOST = "127.0.0.1";
|
|
13
13
|
declare const DEFAULT_PORT = 4340;
|
|
14
14
|
/** Spool schema version, persisted in the `meta` table. */
|
|
15
|
-
declare const SPOOL_SCHEMA_VERSION =
|
|
15
|
+
declare const SPOOL_SCHEMA_VERSION = 3;
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Error taxonomy for @remnic/capture-audio.
|
|
@@ -142,6 +142,166 @@ declare function encodeCursor(startedAtUtc: string, id: string): string;
|
|
|
142
142
|
/** Absent cursor → null (first page); malformed cursor → 400. */
|
|
143
143
|
declare function decodeCursor(value: string | null | undefined): Cursor | null;
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Native capture helper resolver + supervised process runner (issue #1897,
|
|
147
|
+
* "audio native macOS helper" slice — Node side only).
|
|
148
|
+
*
|
|
149
|
+
* The native recorder is the ONE shared macOS helper shipped by #2138
|
|
150
|
+
* (`remnic-capture-helper`), driven here through its `audio-capture`
|
|
151
|
+
* subcommand. It emits one JSONL `ChunkEvent` per recorded WAV chunk on
|
|
152
|
+
* stdout. This module is deliberately à-la-carte, mirroring the VAD/STT
|
|
153
|
+
* adapters and the screen daemon's helper seam:
|
|
154
|
+
*
|
|
155
|
+
* - The helper ships as an OPTIONAL, per-platform package
|
|
156
|
+
* (`@remnic/capture-native-darwin-arm64` / `-x64`) that exports a
|
|
157
|
+
* `helperBinaryPath` and declares the same binary under `bin`. It is a
|
|
158
|
+
* peer dependency, never a runtime dependency, so `@remnic/capture-audio`
|
|
159
|
+
* installs and works on any platform without it.
|
|
160
|
+
* - The package specifier is COMPUTED from `process.platform`/`arch` so a
|
|
161
|
+
* static importer never bundles a foreign-arch binary, and resolution uses
|
|
162
|
+
* Node module resolution (`require.resolve`).
|
|
163
|
+
* - `REMNIC_CAPTURE_HELPER_BIN` overrides resolution with an explicit binary
|
|
164
|
+
* path (manual installs and the hardware-free test seam, which points it at
|
|
165
|
+
* a fake script emitting canned JSON).
|
|
166
|
+
* - A missing optional package reports the EXACT install command instead of a
|
|
167
|
+
* raw resolver error.
|
|
168
|
+
*
|
|
169
|
+
* The runner is the sole owner of the child process: it spawns the helper,
|
|
170
|
+
* parses stdout strictly line-by-line, reports validated events to a callback,
|
|
171
|
+
* reports stderr/errors separately, and restarts only UNEXPECTED exits with
|
|
172
|
+
* bounded exponential backoff. It never writes the Spool and never invents a
|
|
173
|
+
* conversation — the processing/assembly layer owns eventual Spool writes
|
|
174
|
+
* downstream of the validated events this runner surfaces.
|
|
175
|
+
*/
|
|
176
|
+
/** One recorded audio chunk, as emitted by the native helper on stdout (JSONL). */
|
|
177
|
+
interface ChunkEvent {
|
|
178
|
+
path: string;
|
|
179
|
+
channel: "mic" | "system";
|
|
180
|
+
startedAtUtc: string;
|
|
181
|
+
endedAtUtc: string;
|
|
182
|
+
device: string | null;
|
|
183
|
+
}
|
|
184
|
+
/** Which channels the `audio-capture` subcommand records. */
|
|
185
|
+
type ChannelSelection = "mic" | "system" | "both";
|
|
186
|
+
/** A resolved native helper: its source specifier and the on-disk binary path. */
|
|
187
|
+
interface HelperResolution {
|
|
188
|
+
specifier: string;
|
|
189
|
+
binaryPath: string;
|
|
190
|
+
}
|
|
191
|
+
/** The narrow child-process surface the runner depends on (injectable for tests). */
|
|
192
|
+
interface HelperChild {
|
|
193
|
+
stdout: {
|
|
194
|
+
on(event: "data", listener: (chunk: Buffer | string) => void): unknown;
|
|
195
|
+
};
|
|
196
|
+
stderr: {
|
|
197
|
+
on(event: "data", listener: (chunk: Buffer | string) => void): unknown;
|
|
198
|
+
};
|
|
199
|
+
once(event: "error", listener: (err: Error) => void): unknown;
|
|
200
|
+
once(event: "close", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
|
|
201
|
+
kill(signal?: NodeJS.Signals): boolean;
|
|
202
|
+
readonly killed?: boolean;
|
|
203
|
+
readonly pid?: number;
|
|
204
|
+
}
|
|
205
|
+
/** Spawns the helper binary. Defaults to a `node:child_process` adapter. */
|
|
206
|
+
type HelperSpawn = (binaryPath: string, args: string[]) => HelperChild;
|
|
207
|
+
/** An opaque restart-timer token returned by `scheduleRestart`. */
|
|
208
|
+
type RestartTimer = unknown;
|
|
209
|
+
interface ResolveHelperDeps {
|
|
210
|
+
platform?: NodeJS.Platform;
|
|
211
|
+
arch?: string;
|
|
212
|
+
/** `require.resolve`-style resolver; defaults to this module's require. */
|
|
213
|
+
resolve?: (specifier: string) => string;
|
|
214
|
+
readFile?: (file: string) => string;
|
|
215
|
+
/** Environment source for the `REMNIC_CAPTURE_HELPER_BIN` override. */
|
|
216
|
+
env?: NodeJS.ProcessEnv;
|
|
217
|
+
}
|
|
218
|
+
interface NativeRunnerOptions {
|
|
219
|
+
/** Directory the helper writes WAV chunks into (`audio-capture --out`). */
|
|
220
|
+
outDir: string;
|
|
221
|
+
chunkSeconds: number;
|
|
222
|
+
/** Channels to record; defaults to "both". */
|
|
223
|
+
channel?: ChannelSelection;
|
|
224
|
+
/** Optional CoreAudio microphone device UID (`--device`). */
|
|
225
|
+
device?: string | null;
|
|
226
|
+
/** Called once per validated ChunkEvent. */
|
|
227
|
+
onChunk: (event: ChunkEvent) => void;
|
|
228
|
+
/** Called for a rejected stdout line or a spawn/child error. */
|
|
229
|
+
onError?: (error: Error) => void;
|
|
230
|
+
/** Called once per complete stderr line. */
|
|
231
|
+
onStderr?: (line: string) => void;
|
|
232
|
+
/** Pre-resolved helper; when absent the runner resolves it lazily on `start()`. */
|
|
233
|
+
resolution?: HelperResolution;
|
|
234
|
+
resolveBinary?: (deps: ResolveHelperDeps) => HelperResolution;
|
|
235
|
+
spawn?: HelperSpawn;
|
|
236
|
+
/** Max consecutive unexpected restarts before giving up (default 5). */
|
|
237
|
+
maxRestarts?: number;
|
|
238
|
+
/** First backoff delay in ms (default 500). */
|
|
239
|
+
baseBackoffMs?: number;
|
|
240
|
+
/** Backoff ceiling in ms (default 30000). */
|
|
241
|
+
maxBackoffMs?: number;
|
|
242
|
+
scheduleRestart?: (fn: () => void, delayMs: number) => RestartTimer;
|
|
243
|
+
cancelRestart?: (timer: RestartTimer) => void;
|
|
244
|
+
}
|
|
245
|
+
/** A running native-capture supervisor. */
|
|
246
|
+
interface NativeCaptureRunner {
|
|
247
|
+
start(): void;
|
|
248
|
+
/** Stop the helper (SIGTERM) and resolve once it exits and its final chunk is read. */
|
|
249
|
+
stop(): Promise<void>;
|
|
250
|
+
/** True between a `start()` and its matching `stop()`. */
|
|
251
|
+
readonly running: boolean;
|
|
252
|
+
}
|
|
253
|
+
/** The env var that overrides package resolution with an explicit binary path. */
|
|
254
|
+
declare const HELPER_BIN_ENV = "REMNIC_CAPTURE_HELPER_BIN";
|
|
255
|
+
/**
|
|
256
|
+
* Compute the optional native-helper package specifier for a platform/arch.
|
|
257
|
+
* The helper is macOS-only and hardware-gated; every other platform (and any
|
|
258
|
+
* unsupported macOS architecture) throws loudly rather than resolving to a
|
|
259
|
+
* package that cannot exist.
|
|
260
|
+
*/
|
|
261
|
+
declare function helperPackageSpecifier(platform: NodeJS.Platform | string, arch: string): string;
|
|
262
|
+
/**
|
|
263
|
+
* Resolve the native helper binary. Order: explicit `REMNIC_CAPTURE_HELPER_BIN`
|
|
264
|
+
* override, then the computed platform package's declared executable (resolved
|
|
265
|
+
* via Node module resolution, identical to its `helperBinaryPath` export).
|
|
266
|
+
* Throws a CaptureConfigError naming the exact install command when the
|
|
267
|
+
* optional package is not installed.
|
|
268
|
+
*/
|
|
269
|
+
declare function resolveHelperBinary(deps?: ResolveHelperDeps): HelperResolution;
|
|
270
|
+
/** Build the `audio-capture` argv from runner options (#2138 helper contract). */
|
|
271
|
+
declare function buildHelperArgs(opts: Pick<NativeRunnerOptions, "outDir" | "chunkSeconds" | "channel" | "device">): string[];
|
|
272
|
+
/** Parse and validate one JSONL line into a ChunkEvent; throws on anything malformed. */
|
|
273
|
+
declare function parseChunkEvent(line: string): ChunkEvent;
|
|
274
|
+
/**
|
|
275
|
+
* Run the helper's one-shot `device-enumerate` subcommand and return the parsed
|
|
276
|
+
* device list. Bounded, argv-only, and injectable for tests. Throws a
|
|
277
|
+
* CaptureInputError on a nonzero exit, empty output, or invalid JSON.
|
|
278
|
+
*/
|
|
279
|
+
declare function enumerateDevices(binaryPath: string, spawn?: HelperSpawn, timeoutMs?: number): Promise<unknown[]>;
|
|
280
|
+
/**
|
|
281
|
+
* Create a supervised native-capture runner. Dependency-injectable: pass
|
|
282
|
+
* `spawn`, `resolution`/`resolveBinary`, and `scheduleRestart`/`cancelRestart`
|
|
283
|
+
* to drive it deterministically in tests.
|
|
284
|
+
*/
|
|
285
|
+
declare function createNativeCaptureRunner(options: NativeRunnerOptions): NativeCaptureRunner;
|
|
286
|
+
|
|
287
|
+
/** Ceiling on chunks held in the reorder buffer. */
|
|
288
|
+
declare const MAX_BUFFERED_CHUNKS = 512;
|
|
289
|
+
/** Consecutive apply failures before a chunk is parked. */
|
|
290
|
+
declare const QUARANTINE_AFTER_FAILURES = 3;
|
|
291
|
+
type PendingChunkReason = "evicted" | "quarantined";
|
|
292
|
+
interface PendingChunkInput {
|
|
293
|
+
id: string;
|
|
294
|
+
wavPath: string;
|
|
295
|
+
startedAtUtc: string;
|
|
296
|
+
endedAtUtc: string;
|
|
297
|
+
channel: ChunkEvent["channel"];
|
|
298
|
+
device: string | null;
|
|
299
|
+
reason: PendingChunkReason;
|
|
300
|
+
}
|
|
301
|
+
interface PendingChunkRecord extends PendingChunkInput {
|
|
302
|
+
createdAtUtc: string;
|
|
303
|
+
}
|
|
304
|
+
|
|
145
305
|
/**
|
|
146
306
|
* SQLite spool — the daemon's local buffer of captured conversations.
|
|
147
307
|
*
|
|
@@ -162,6 +322,7 @@ declare function decodeCursor(value: string | null | undefined): Cursor | null;
|
|
|
162
322
|
* connector never ingests half a meeting and pagination is deterministic
|
|
163
323
|
* even when two conversations share a start timestamp.
|
|
164
324
|
*/
|
|
325
|
+
|
|
165
326
|
type ConversationState = "capturing" | "final";
|
|
166
327
|
type ChunkStatus = "pending" | "transcribed" | "failed" | "deleted";
|
|
167
328
|
interface SegmentInput {
|
|
@@ -408,6 +569,9 @@ declare class Spool {
|
|
|
408
569
|
readSpeakerClusters(): SpeakerClusterRow[];
|
|
409
570
|
listSpeakers(): SpeakerRow[];
|
|
410
571
|
pendingChunkCount(): number;
|
|
572
|
+
recordPendingChunk(input: PendingChunkInput): void;
|
|
573
|
+
listPendingChunks(reason?: PendingChunkReason): PendingChunkRecord[];
|
|
574
|
+
deletePendingChunk(id: string): void;
|
|
411
575
|
stats(): {
|
|
412
576
|
conversations: number;
|
|
413
577
|
segments: number;
|
|
@@ -863,148 +1027,6 @@ declare class SpeakerClusterer {
|
|
|
863
1027
|
clusters(): SpeakerCluster[];
|
|
864
1028
|
}
|
|
865
1029
|
|
|
866
|
-
/**
|
|
867
|
-
* Native capture helper resolver + supervised process runner (issue #1897,
|
|
868
|
-
* "audio native macOS helper" slice — Node side only).
|
|
869
|
-
*
|
|
870
|
-
* The native recorder is the ONE shared macOS helper shipped by #2138
|
|
871
|
-
* (`remnic-capture-helper`), driven here through its `audio-capture`
|
|
872
|
-
* subcommand. It emits one JSONL `ChunkEvent` per recorded WAV chunk on
|
|
873
|
-
* stdout. This module is deliberately à-la-carte, mirroring the VAD/STT
|
|
874
|
-
* adapters and the screen daemon's helper seam:
|
|
875
|
-
*
|
|
876
|
-
* - The helper ships as an OPTIONAL, per-platform package
|
|
877
|
-
* (`@remnic/capture-native-darwin-arm64` / `-x64`) that exports a
|
|
878
|
-
* `helperBinaryPath` and declares the same binary under `bin`. It is a
|
|
879
|
-
* peer dependency, never a runtime dependency, so `@remnic/capture-audio`
|
|
880
|
-
* installs and works on any platform without it.
|
|
881
|
-
* - The package specifier is COMPUTED from `process.platform`/`arch` so a
|
|
882
|
-
* static importer never bundles a foreign-arch binary, and resolution uses
|
|
883
|
-
* Node module resolution (`require.resolve`).
|
|
884
|
-
* - `REMNIC_CAPTURE_HELPER_BIN` overrides resolution with an explicit binary
|
|
885
|
-
* path (manual installs and the hardware-free test seam, which points it at
|
|
886
|
-
* a fake script emitting canned JSON).
|
|
887
|
-
* - A missing optional package reports the EXACT install command instead of a
|
|
888
|
-
* raw resolver error.
|
|
889
|
-
*
|
|
890
|
-
* The runner is the sole owner of the child process: it spawns the helper,
|
|
891
|
-
* parses stdout strictly line-by-line, reports validated events to a callback,
|
|
892
|
-
* reports stderr/errors separately, and restarts only UNEXPECTED exits with
|
|
893
|
-
* bounded exponential backoff. It never writes the Spool and never invents a
|
|
894
|
-
* conversation — the processing/assembly layer owns eventual Spool writes
|
|
895
|
-
* downstream of the validated events this runner surfaces.
|
|
896
|
-
*/
|
|
897
|
-
/** One recorded audio chunk, as emitted by the native helper on stdout (JSONL). */
|
|
898
|
-
interface ChunkEvent {
|
|
899
|
-
path: string;
|
|
900
|
-
channel: "mic" | "system";
|
|
901
|
-
startedAtUtc: string;
|
|
902
|
-
endedAtUtc: string;
|
|
903
|
-
device: string | null;
|
|
904
|
-
}
|
|
905
|
-
/** Which channels the `audio-capture` subcommand records. */
|
|
906
|
-
type ChannelSelection = "mic" | "system" | "both";
|
|
907
|
-
/** A resolved native helper: its source specifier and the on-disk binary path. */
|
|
908
|
-
interface HelperResolution {
|
|
909
|
-
specifier: string;
|
|
910
|
-
binaryPath: string;
|
|
911
|
-
}
|
|
912
|
-
/** The narrow child-process surface the runner depends on (injectable for tests). */
|
|
913
|
-
interface HelperChild {
|
|
914
|
-
stdout: {
|
|
915
|
-
on(event: "data", listener: (chunk: Buffer | string) => void): unknown;
|
|
916
|
-
};
|
|
917
|
-
stderr: {
|
|
918
|
-
on(event: "data", listener: (chunk: Buffer | string) => void): unknown;
|
|
919
|
-
};
|
|
920
|
-
once(event: "error", listener: (err: Error) => void): unknown;
|
|
921
|
-
once(event: "close", listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown;
|
|
922
|
-
kill(signal?: NodeJS.Signals): boolean;
|
|
923
|
-
readonly killed?: boolean;
|
|
924
|
-
readonly pid?: number;
|
|
925
|
-
}
|
|
926
|
-
/** Spawns the helper binary. Defaults to a `node:child_process` adapter. */
|
|
927
|
-
type HelperSpawn = (binaryPath: string, args: string[]) => HelperChild;
|
|
928
|
-
/** An opaque restart-timer token returned by `scheduleRestart`. */
|
|
929
|
-
type RestartTimer = unknown;
|
|
930
|
-
interface ResolveHelperDeps {
|
|
931
|
-
platform?: NodeJS.Platform;
|
|
932
|
-
arch?: string;
|
|
933
|
-
/** `require.resolve`-style resolver; defaults to this module's require. */
|
|
934
|
-
resolve?: (specifier: string) => string;
|
|
935
|
-
readFile?: (file: string) => string;
|
|
936
|
-
/** Environment source for the `REMNIC_CAPTURE_HELPER_BIN` override. */
|
|
937
|
-
env?: NodeJS.ProcessEnv;
|
|
938
|
-
}
|
|
939
|
-
interface NativeRunnerOptions {
|
|
940
|
-
/** Directory the helper writes WAV chunks into (`audio-capture --out`). */
|
|
941
|
-
outDir: string;
|
|
942
|
-
chunkSeconds: number;
|
|
943
|
-
/** Channels to record; defaults to "both". */
|
|
944
|
-
channel?: ChannelSelection;
|
|
945
|
-
/** Optional CoreAudio microphone device UID (`--device`). */
|
|
946
|
-
device?: string | null;
|
|
947
|
-
/** Called once per validated ChunkEvent. */
|
|
948
|
-
onChunk: (event: ChunkEvent) => void;
|
|
949
|
-
/** Called for a rejected stdout line or a spawn/child error. */
|
|
950
|
-
onError?: (error: Error) => void;
|
|
951
|
-
/** Called once per complete stderr line. */
|
|
952
|
-
onStderr?: (line: string) => void;
|
|
953
|
-
/** Pre-resolved helper; when absent the runner resolves it lazily on `start()`. */
|
|
954
|
-
resolution?: HelperResolution;
|
|
955
|
-
resolveBinary?: (deps: ResolveHelperDeps) => HelperResolution;
|
|
956
|
-
spawn?: HelperSpawn;
|
|
957
|
-
/** Max consecutive unexpected restarts before giving up (default 5). */
|
|
958
|
-
maxRestarts?: number;
|
|
959
|
-
/** First backoff delay in ms (default 500). */
|
|
960
|
-
baseBackoffMs?: number;
|
|
961
|
-
/** Backoff ceiling in ms (default 30000). */
|
|
962
|
-
maxBackoffMs?: number;
|
|
963
|
-
scheduleRestart?: (fn: () => void, delayMs: number) => RestartTimer;
|
|
964
|
-
cancelRestart?: (timer: RestartTimer) => void;
|
|
965
|
-
}
|
|
966
|
-
/** A running native-capture supervisor. */
|
|
967
|
-
interface NativeCaptureRunner {
|
|
968
|
-
start(): void;
|
|
969
|
-
/** Stop the helper (SIGTERM) and resolve once it exits and its final chunk is read. */
|
|
970
|
-
stop(): Promise<void>;
|
|
971
|
-
/** True between a `start()` and its matching `stop()`. */
|
|
972
|
-
readonly running: boolean;
|
|
973
|
-
}
|
|
974
|
-
/** The env var that overrides package resolution with an explicit binary path. */
|
|
975
|
-
declare const HELPER_BIN_ENV = "REMNIC_CAPTURE_HELPER_BIN";
|
|
976
|
-
/**
|
|
977
|
-
* Compute the optional native-helper package specifier for a platform/arch.
|
|
978
|
-
* The helper is macOS-only and hardware-gated; every other platform (and any
|
|
979
|
-
* unsupported macOS architecture) throws loudly rather than resolving to a
|
|
980
|
-
* package that cannot exist.
|
|
981
|
-
*/
|
|
982
|
-
declare function helperPackageSpecifier(platform: NodeJS.Platform | string, arch: string): string;
|
|
983
|
-
/**
|
|
984
|
-
* Resolve the native helper binary. Order: explicit `REMNIC_CAPTURE_HELPER_BIN`
|
|
985
|
-
* override, then the computed platform package's declared executable (resolved
|
|
986
|
-
* via Node module resolution, identical to its `helperBinaryPath` export).
|
|
987
|
-
* Throws a CaptureConfigError naming the exact install command when the
|
|
988
|
-
* optional package is not installed.
|
|
989
|
-
*/
|
|
990
|
-
declare function resolveHelperBinary(deps?: ResolveHelperDeps): HelperResolution;
|
|
991
|
-
/** Build the `audio-capture` argv from runner options (#2138 helper contract). */
|
|
992
|
-
declare function buildHelperArgs(opts: Pick<NativeRunnerOptions, "outDir" | "chunkSeconds" | "channel" | "device">): string[];
|
|
993
|
-
/** Parse and validate one JSONL line into a ChunkEvent; throws on anything malformed. */
|
|
994
|
-
declare function parseChunkEvent(line: string): ChunkEvent;
|
|
995
|
-
/**
|
|
996
|
-
* Run the helper's one-shot `device-enumerate` subcommand and return the parsed
|
|
997
|
-
* device list. Bounded, argv-only, and injectable for tests. Throws a
|
|
998
|
-
* CaptureInputError on a nonzero exit, empty output, or invalid JSON.
|
|
999
|
-
*/
|
|
1000
|
-
declare function enumerateDevices(binaryPath: string, spawn?: HelperSpawn, timeoutMs?: number): Promise<unknown[]>;
|
|
1001
|
-
/**
|
|
1002
|
-
* Create a supervised native-capture runner. Dependency-injectable: pass
|
|
1003
|
-
* `spawn`, `resolution`/`resolveBinary`, and `scheduleRestart`/`cancelRestart`
|
|
1004
|
-
* to drive it deterministically in tests.
|
|
1005
|
-
*/
|
|
1006
|
-
declare function createNativeCaptureRunner(options: NativeRunnerOptions): NativeCaptureRunner;
|
|
1007
|
-
|
|
1008
1030
|
/**
|
|
1009
1031
|
* Chunk processor (issue #1897) — turns completed native WAV chunk events
|
|
1010
1032
|
* into durable, replay-safe conversations in the spool.
|
|
@@ -1073,9 +1095,20 @@ interface ChunkProcessor {
|
|
|
1073
1095
|
/** Drain, then flip open conversations to `final`; returns the count closed. */
|
|
1074
1096
|
finalize(): Promise<number>;
|
|
1075
1097
|
}
|
|
1098
|
+
|
|
1076
1099
|
declare function chunkStableId(event: ChunkEvent): string;
|
|
1077
1100
|
declare function createChunkProcessor(deps: ChunkProcessorDeps): ChunkProcessor;
|
|
1078
1101
|
|
|
1102
|
+
interface OrphanScanInput {
|
|
1103
|
+
rawDirectory: string;
|
|
1104
|
+
spool: Spool;
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Rebuild chunk events from durable pending rows and leftover WAVs so a
|
|
1108
|
+
* restart can feed them through the live processor (issue #2379).
|
|
1109
|
+
*/
|
|
1110
|
+
declare function scanOrphanedChunks(input: OrphanScanInput): ChunkEvent[];
|
|
1111
|
+
|
|
1079
1112
|
/**
|
|
1080
1113
|
* Live capture wiring (issue #1897) — assembles the native helper runner and
|
|
1081
1114
|
* the chunk processor into one start/stop unit the daemon drives.
|
|
@@ -1217,4 +1250,4 @@ interface EnrollSelfResult {
|
|
|
1217
1250
|
*/
|
|
1218
1251
|
declare function enrollSelf(input: EnrollSelfInput): EnrollSelfResult;
|
|
1219
1252
|
|
|
1220
|
-
export { type AssembledConversation, type AssemblerOptions, type AssemblyAppendInput, type AssemblyAppendResult, type AssemblySegment, CAPTURE_AUDIO_VERSION, CaptureConfigError, CaptureInputError, type CapturePaths, type ChannelSelection, type ChunkEvent, type ChunkProcessor, type ChunkProcessorDeps, type ChunkStatus, type ChunkTranscribeInput, type CliIo, ConversationAssembler, type ConversationInput, type ConversationPage, type ConversationState, type Cursor, DEFAULT_CONVERSATION_GAP_MINUTES, DEFAULT_HOST, DEFAULT_PORT, DEFAULT_SERVICE_LABEL, DESKTOP_SOURCE_ID, type DaemonConfig, type DaemonConversation, type DaemonDeps, type DaemonHandle, type DaemonSegment, type DedupSegment, DesktopDaemonError, type DeviceConfig, type DiarizationConfig, type Embedding, type EnrollSelfInput, type EnrollSelfResult, HELPER_BIN_ENV, type HelperResolution, type HelperSpawn, type LiveCapture, type LiveCaptureOptions, type ModelDownloadInput, type ModelDownloadResult, type NativeCaptureRunner, type NativeRunnerOptions, type PidRecord, type QueryFinalOptions, REPLAY_COMMIT_BATCH, type ReplayResult, type ResolveHelperDeps, SELF_SPEAKER_ID, SPOOL_SCHEMA_VERSION, type SegmentInput, type ServicePlan, type ServiceSpec, type SherpaOnnxModule, type SileroVadInput, type SpeakerCluster, type SpeakerClusterRow, SpeakerClusterer, type SpeakerInput, type SpeakerRow, Spool, type SttConfig, type TranscribedSegment, type VadConfig, type WhisperRunResult, type WhisperTranscriptionInput, assembleConversations, assertValidTimezone, bearerFromHeader, buildHelperArgs, buildWhisperArgs, captureBaseDir, capturePaths, chunkStableId, cosineSimilarity, createChunkProcessor, createDesktopConnector, createLiveCapture, createNativeCaptureRunner, createRequestHandler, createSileroVad, daemonConversationToWearable, decodeCursor, dedupeCrossChannel, defaultDaemonConfig, downloadWhisperModel, encodeCursor, enrollSelf, ensureDesktopConnectorRegistered, enumerateDevices, generateToken, helperPackageSpecifier, ingestReplayDir, ingestReplayDirResponsive, installService, isProcessAlive, loadDaemonConfig, loadOrCreateToken, loadSherpaOnnx, parseChunkEvent, parseDaemonConfig, parseLimit, parseTranscriptDate, parseWhisperJson, planService, pruneExpiredRawAudio, readPidFile, readPidRecord, removePidFile, removePidFileIfOwner, renderLaunchAgent, renderSystemdUnit, resolveCaptureAudioToken, resolveHelperBinary, resolveModelPath, runCapture, runWhisperCli, serializeDaemonConfig, sileroVadConfig, startDaemon, superviseReplay, tokensMatch, transcribeWithWhisper, uninstallService, wearableConnectorRegistration, whisperModelUrl, wordJaccard, writePidFile };
|
|
1253
|
+
export { type AssembledConversation, type AssemblerOptions, type AssemblyAppendInput, type AssemblyAppendResult, type AssemblySegment, CAPTURE_AUDIO_VERSION, CaptureConfigError, CaptureInputError, type CapturePaths, type ChannelSelection, type ChunkEvent, type ChunkProcessor, type ChunkProcessorDeps, type ChunkStatus, type ChunkTranscribeInput, type CliIo, ConversationAssembler, type ConversationInput, type ConversationPage, type ConversationState, type Cursor, DEFAULT_CONVERSATION_GAP_MINUTES, DEFAULT_HOST, DEFAULT_PORT, DEFAULT_SERVICE_LABEL, DESKTOP_SOURCE_ID, type DaemonConfig, type DaemonConversation, type DaemonDeps, type DaemonHandle, type DaemonSegment, type DedupSegment, DesktopDaemonError, type DeviceConfig, type DiarizationConfig, type Embedding, type EnrollSelfInput, type EnrollSelfResult, HELPER_BIN_ENV, type HelperResolution, type HelperSpawn, type LiveCapture, type LiveCaptureOptions, MAX_BUFFERED_CHUNKS, type ModelDownloadInput, type ModelDownloadResult, type NativeCaptureRunner, type NativeRunnerOptions, type PidRecord, QUARANTINE_AFTER_FAILURES, type QueryFinalOptions, REPLAY_COMMIT_BATCH, type ReplayResult, type ResolveHelperDeps, SELF_SPEAKER_ID, SPOOL_SCHEMA_VERSION, type SegmentInput, type ServicePlan, type ServiceSpec, type SherpaOnnxModule, type SileroVadInput, type SpeakerCluster, type SpeakerClusterRow, SpeakerClusterer, type SpeakerInput, type SpeakerRow, Spool, type SttConfig, type TranscribedSegment, type VadConfig, type WhisperRunResult, type WhisperTranscriptionInput, assembleConversations, assertValidTimezone, bearerFromHeader, buildHelperArgs, buildWhisperArgs, captureBaseDir, capturePaths, chunkStableId, cosineSimilarity, createChunkProcessor, createDesktopConnector, createLiveCapture, createNativeCaptureRunner, createRequestHandler, createSileroVad, daemonConversationToWearable, decodeCursor, dedupeCrossChannel, defaultDaemonConfig, downloadWhisperModel, encodeCursor, enrollSelf, ensureDesktopConnectorRegistered, enumerateDevices, generateToken, helperPackageSpecifier, ingestReplayDir, ingestReplayDirResponsive, installService, isProcessAlive, loadDaemonConfig, loadOrCreateToken, loadSherpaOnnx, parseChunkEvent, parseDaemonConfig, parseLimit, parseTranscriptDate, parseWhisperJson, planService, pruneExpiredRawAudio, readPidFile, readPidRecord, removePidFile, removePidFileIfOwner, renderLaunchAgent, renderSystemdUnit, resolveCaptureAudioToken, resolveHelperBinary, resolveModelPath, runCapture, runWhisperCli, scanOrphanedChunks, serializeDaemonConfig, sileroVadConfig, startDaemon, superviseReplay, tokensMatch, transcribeWithWhisper, uninstallService, wearableConnectorRegistration, whisperModelUrl, wordJaccard, writePidFile };
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
DEFAULT_PORT,
|
|
10
10
|
DEFAULT_SERVICE_LABEL,
|
|
11
11
|
HELPER_BIN_ENV,
|
|
12
|
+
MAX_BUFFERED_CHUNKS,
|
|
13
|
+
QUARANTINE_AFTER_FAILURES,
|
|
12
14
|
REPLAY_COMMIT_BATCH,
|
|
13
15
|
SELF_SPEAKER_ID,
|
|
14
16
|
SPOOL_SCHEMA_VERSION,
|
|
@@ -61,6 +63,7 @@ import {
|
|
|
61
63
|
resolveModelPath,
|
|
62
64
|
runCapture,
|
|
63
65
|
runWhisperCli,
|
|
66
|
+
scanOrphanedChunks,
|
|
64
67
|
serializeDaemonConfig,
|
|
65
68
|
startDaemon,
|
|
66
69
|
superviseReplay,
|
|
@@ -70,7 +73,7 @@ import {
|
|
|
70
73
|
whisperModelUrl,
|
|
71
74
|
wordJaccard,
|
|
72
75
|
writePidFile
|
|
73
|
-
} from "./chunk-
|
|
76
|
+
} from "./chunk-4Z3DZYEB.js";
|
|
74
77
|
|
|
75
78
|
// src/vad.ts
|
|
76
79
|
import { statSync } from "fs";
|
|
@@ -334,6 +337,8 @@ export {
|
|
|
334
337
|
DESKTOP_SOURCE_ID,
|
|
335
338
|
DesktopDaemonError,
|
|
336
339
|
HELPER_BIN_ENV,
|
|
340
|
+
MAX_BUFFERED_CHUNKS,
|
|
341
|
+
QUARANTINE_AFTER_FAILURES,
|
|
337
342
|
REPLAY_COMMIT_BATCH,
|
|
338
343
|
SELF_SPEAKER_ID,
|
|
339
344
|
SPOOL_SCHEMA_VERSION,
|
|
@@ -390,6 +395,7 @@ export {
|
|
|
390
395
|
resolveModelPath,
|
|
391
396
|
runCapture,
|
|
392
397
|
runWhisperCli,
|
|
398
|
+
scanOrphanedChunks,
|
|
393
399
|
serializeDaemonConfig,
|
|
394
400
|
sileroVadConfig,
|
|
395
401
|
startDaemon,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/vad.ts","../src/connector.ts"],"sourcesContent":["import { statSync } from \"node:fs\";\n\nimport { CaptureConfigError } from \"./errors.js\";\nimport { expandTilde } from \"./paths.js\";\n\n\nexport interface SileroVadInput {\n modelPath: string;\n minSpeechMs: number;\n minSilenceMs?: number;\n maxSpeechMs?: number;\n threshold?: number;\n threads?: number;\n}\n\nexport interface SherpaOnnxModule {\n Vad: new (config: unknown, bufferSeconds: number) => unknown;\n}\n\nexport function sileroVadConfig(input: SileroVadInput): { config: object; bufferSeconds: number } {\n if (typeof input.modelPath !== \"string\" || input.modelPath.trim() === \"\") {\n throw new CaptureConfigError(\"Silero VAD modelPath must be a non-empty string\");\n }\n if (!Number.isFinite(input.minSpeechMs) || input.minSpeechMs <= 0) {\n throw new CaptureConfigError(\"Silero VAD minSpeechMs must be positive\");\n }\n const minSilenceMs = input.minSilenceMs ?? 500;\n const maxSpeechMs = input.maxSpeechMs ?? 30_000;\n const threshold = input.threshold ?? 0.5;\n const threads = input.threads ?? 1;\n if (!Number.isFinite(minSilenceMs) || minSilenceMs < 0 || !Number.isFinite(maxSpeechMs) || maxSpeechMs <= 0) {\n throw new CaptureConfigError(\"Silero VAD duration settings are invalid\");\n }\n if (maxSpeechMs < input.minSpeechMs) {\n throw new CaptureConfigError(\"Silero VAD maxSpeechMs must be greater than or equal to minSpeechMs\");\n }\n if (!Number.isFinite(threshold) || threshold <= 0 || threshold >= 1) {\n throw new CaptureConfigError(\"Silero VAD threshold must be between 0 and 1\");\n }\n if (!Number.isInteger(threads) || threads <= 0) {\n throw new CaptureConfigError(\"Silero VAD threads must be a positive integer\");\n }\n return {\n config: {\n sileroVad: {\n model: expandTilde(input.modelPath),\n threshold,\n minSpeechDuration: input.minSpeechMs / 1_000,\n minSilenceDuration: minSilenceMs / 1_000,\n maxSpeechDuration: maxSpeechMs / 1_000,\n windowSize: 512,\n },\n sampleRate: 16_000,\n debug: false,\n numThreads: threads,\n },\n bufferSeconds: 60,\n };\n}\n\nexport function resolveSherpaExport(module: unknown): SherpaOnnxModule | null {\n const candidates = [module, (module as { default?: unknown } | null)?.default];\n for (const candidate of candidates) {\n if (candidate && typeof candidate === \"object\" && typeof (candidate as SherpaOnnxModule).Vad === \"function\") {\n return candidate as SherpaOnnxModule;\n }\n }\n return null;\n}\n\nexport async function loadSherpaOnnx(\n importModule: (specifier: string) => Promise<unknown> = (specifier) => import(specifier),\n): Promise<SherpaOnnxModule> {\n // Computed specifier keeps the optional sherpa-onnx-node peer out of the static\n // dependency graph so the package builds and runs without it installed.\n const specifier = \"sherpa-onnx-\" + \"node\";\n let module: unknown;\n try {\n module = await importModule(specifier);\n } catch (error) {\n // A genuinely absent package surfaces as ERR_MODULE_NOT_FOUND naming the\n // specifier as a package, with no require stack. An installed-but-broken\n // package (missing native .node, dlopen failure) has a different code\n // and/or a require stack, so it must NOT be reported as \"not installed\".\n const err = error as NodeJS.ErrnoException & { requireStack?: unknown };\n const message = error instanceof Error ? error.message : \"\";\n const packageMissing =\n err.code === \"ERR_MODULE_NOT_FOUND\" &&\n err.requireStack === undefined &&\n message.includes(`package '${specifier}'`);\n if (packageMissing) {\n throw new CaptureConfigError(\n \"Silero VAD requires optional dependency sherpa-onnx-node; install it before enabling VAD\",\n );\n }\n // Installed but unusable. Report that distinctly, but keep the message\n // operator-safe: never echo the raw error (it can leak absolute native\n // library paths), per the CaptureConfigError contract.\n throw new CaptureConfigError(\n \"Silero VAD could not load the installed sherpa-onnx-node native runtime; verify its native build and shared-library dependencies\",\n );\n }\n const resolved = resolveSherpaExport(module);\n if (!resolved) {\n throw new CaptureConfigError(\"sherpa-onnx-node does not expose the Vad API required by capture-audio\");\n }\n return resolved;\n}\n\nfunction isRegularFile(filePath: string): boolean {\n try {\n return statSync(filePath).isFile();\n } catch {\n return false;\n }\n}\n\nexport async function createSileroVad(\n input: SileroVadInput,\n load: () => Promise<SherpaOnnxModule> = loadSherpaOnnx,\n exists: (path: string) => boolean = isRegularFile,\n): Promise<unknown> {\n const { config, bufferSeconds } = sileroVadConfig(input);\n const modelPath = expandTilde(input.modelPath);\n if (!exists(modelPath)) {\n throw new CaptureConfigError(`Silero VAD model not found at ${modelPath}; set vad.modelPath to a readable model file`);\n }\n const { Vad } = await load();\n return new Vad(config, bufferSeconds);\n}\n","/**\n * `desktop` wearable source connector (issue #1897, component 4).\n *\n * À-la-carte optional companion of @remnic/core: installing core alone\n * never pulls this in; core discovers it at runtime via a\n * computed-specifier dynamic import (registry entry {id:\"desktop\",\n * suffix:\"capture-audio\"}) or via a direct import of @remnic/capture-audio,\n * which self-registers idempotently.\n *\n * The connector is a pure API client + normalizer over the capture-audio\n * daemon's loopback HTTP API: no file IO beyond reading the local token,\n * no memory writes, no pipeline behavior (all of that stays in core so\n * desktop audio gets the same cleanup/corrections/trust gating as every\n * other wearable source).\n *\n * Token resolution (in order): settings.apiKey (config) ->\n * REMNIC_CAPTURE_AUDIO_TOKEN env -> the daemon's local token file\n * (~/.remnic/capture/token) when the base URL is loopback.\n */\n\nimport { readFileSync } from \"node:fs\";\n\nimport {\n registerWearableConnector,\n getWearableConnector,\n type WearableAuthCheck,\n type WearableConnectorFactoryOptions,\n type WearableConnectorRegistration,\n type WearableConversation,\n type WearableFetchOptions,\n type WearableFetchPage,\n type WearableSourceConnector,\n type WearableTranscriptSegment,\n} from \"@remnic/core\";\n\nimport { DEFAULT_HOST, DEFAULT_PORT } from \"./constants.js\";\nimport { capturePaths } from \"./paths.js\";\nimport { isLoopbackHost } from \"./util.js\";\nimport type { ConversationPage, DaemonConversation, DaemonSegment } from \"./spool.js\";\n\nexport const DESKTOP_SOURCE_ID = \"desktop\";\nexport const DESKTOP_DISPLAY_NAME = \"Desktop audio\";\nconst DEFAULT_BASE_URL = `http://${DEFAULT_HOST}:${DEFAULT_PORT}`;\n\n/** Bounded probe timeout so a wedged daemon reports unreachable instead of hanging. */\nconst PROBE_TIMEOUT_MS = 15_000;\n\nfunction withTimeout(signal: AbortSignal | undefined): AbortSignal {\n const timeout = AbortSignal.timeout(PROBE_TIMEOUT_MS);\n return signal ? AbortSignal.any([signal, timeout]) : timeout;\n}\n\n/** Error raised for a genuine backend failure (never for an empty day). */\nexport class DesktopDaemonError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"DesktopDaemonError\";\n }\n}\n\nfunction baseUrlIsLoopback(baseUrl: string): boolean {\n try {\n return isLoopbackHost(new URL(baseUrl).hostname);\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve the daemon bearer token. The local token file is read ONLY for\n * a loopback base URL — a remote reader must supply the token explicitly\n * (config/env), never inherit this machine's local token.\n */\nexport function resolveCaptureAudioToken(\n configured: string | undefined,\n baseUrl: string,\n env: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n if (typeof configured === \"string\" && configured.trim().length > 0) return configured.trim();\n const fromEnv = env.REMNIC_CAPTURE_AUDIO_TOKEN;\n if (typeof fromEnv === \"string\" && fromEnv.trim().length > 0) return fromEnv.trim();\n if (baseUrlIsLoopback(baseUrl)) {\n try {\n const token = readFileSync(capturePaths().tokenPath, \"utf8\").trim();\n if (token.length > 0) return token;\n } catch {\n // no local token file — treat as unauthenticated (health will 401)\n }\n }\n return undefined;\n}\n\nfunction daemonSegmentToWearable(seg: DaemonSegment): WearableTranscriptSegment {\n return {\n text: seg.textRaw,\n // The wearables speaker registry owns naming; the connector only\n // supplies the stable per-source key (spk_<n> | self | \"unknown\").\n speakerKey: seg.speakerKey ?? \"unknown\",\n isWearer: seg.isWearer,\n startIso: seg.startUtc,\n endIso: seg.endUtc,\n };\n}\n\nexport function daemonConversationToWearable(conv: DaemonConversation): WearableConversation {\n return {\n id: conv.id,\n source: DESKTOP_SOURCE_ID,\n startIso: conv.startedAtUtc,\n endIso: conv.endedAtUtc ?? undefined,\n segments: conv.segments.map(daemonSegmentToWearable),\n };\n}\n\ninterface DesktopClientOptions {\n baseUrl: string;\n /** Resolved per request so env / token-file rotation applies without a rebuild. */\n getToken: () => string | undefined;\n}\n\n/** Linear trailing-slash trim (avoids a backtracking regex on the base URL). */\nfunction trimTrailingSlashes(value: string): string {\n let end = value.length;\n while (end > 0 && value.charCodeAt(end - 1) === 47 /* \"/\" */) end--;\n return value.slice(0, end);\n}\n\nclass DesktopClient {\n #baseUrl: string;\n #getToken: () => string | undefined;\n\n constructor(options: DesktopClientOptions) {\n this.#baseUrl = trimTrailingSlashes(options.baseUrl);\n this.#getToken = options.getToken;\n }\n\n #headers(): Record<string, string> {\n const token = this.#getToken();\n return token ? { authorization: `Bearer ${token}` } : {};\n }\n\n async verifyAuth(signal?: AbortSignal): Promise<WearableAuthCheck> {\n let res: Response;\n try {\n res = await fetch(`${this.#baseUrl}/v1/health`, { headers: this.#headers(), signal: withTimeout(signal) });\n } catch (err) {\n // Honor caller cancellation; do not mislabel a deliberate abort.\n if (signal?.aborted) throw err;\n // Timeout / connection refused / DNS / offline: the daemon isn't\n // reachable. This is NOT an auth failure and must never throw (AC2).\n return { ok: false, detail: \"unreachable\" };\n }\n if (res.status === 401) return { ok: false, detail: \"unauthorized\" };\n if (!res.ok) return { ok: false, detail: `HTTP ${res.status}` };\n let body: { ok?: unknown; version?: unknown };\n try {\n body = (await res.json()) as { ok?: unknown; version?: unknown };\n } catch (err) {\n // Honor caller cancellation during the body read; a non-JSON body\n // otherwise just reads as unhealthy (never throws for that, AC2).\n if (signal?.aborted) throw err;\n body = {};\n }\n if (body.ok === true) {\n return { ok: true, detail: typeof body.version === \"string\" ? `capture-audio ${body.version}` : undefined };\n }\n return { ok: false, detail: \"unhealthy\" };\n }\n\n async fetchConversations(opts: WearableFetchOptions): Promise<WearableFetchPage> {\n let res: Response;\n try {\n const url = new URL(`${this.#baseUrl}/v1/conversations`);\n url.searchParams.set(\"date\", opts.date);\n url.searchParams.set(\"timezone\", opts.timezone);\n if (opts.cursor) url.searchParams.set(\"cursor\", opts.cursor);\n res = await fetch(url, { headers: this.#headers(), signal: withTimeout(opts.signal) });\n } catch (err) {\n // Honor caller cancellation instead of reporting a daemon fault.\n if (opts.signal?.aborted) throw err;\n // Timeout, offline daemon, or a malformed configured baseUrl all\n // surface consistently as unreachable (never a raw TypeError).\n throw new DesktopDaemonError(\n `desktop capture daemon unreachable at ${this.#baseUrl} (is remnic-capture-audio running?)`,\n );\n }\n if (!res.ok) {\n // A non-2xx is a backend/auth failure, distinct from an empty day.\n throw new DesktopDaemonError(`desktop capture daemon returned HTTP ${res.status}`);\n }\n let page: ConversationPage;\n try {\n page = (await res.json()) as ConversationPage;\n } catch (err) {\n // Honor caller cancellation that lands mid-body-read.\n if (opts.signal?.aborted) throw err;\n // A 200 with an empty/non-JSON body is a backend fault, not an empty day.\n throw new DesktopDaemonError(\"desktop capture daemon returned a non-JSON conversations response\");\n }\n if (!page || !Array.isArray(page.conversations)) {\n throw new DesktopDaemonError(\"desktop capture daemon returned a malformed conversations page\");\n }\n return {\n conversations: page.conversations.map(daemonConversationToWearable),\n nextCursor: page.nextCursor ?? null,\n };\n }\n}\n\nexport function createDesktopConnector(options: WearableConnectorFactoryOptions): WearableSourceConnector {\n const baseUrl = options.settings.baseUrl?.trim() || DEFAULT_BASE_URL;\n // One client per connector, but the token is resolved per request (below)\n // so env / on-disk token rotation takes effect without a rebuild.\n let client: DesktopClient | null = null;\n const getClient = (): DesktopClient => {\n if (!client) {\n client = new DesktopClient({ baseUrl, getToken: () => resolveCaptureAudioToken(options.settings.apiKey, baseUrl) });\n }\n return client;\n };\n return {\n id: DESKTOP_SOURCE_ID,\n displayName: DESKTOP_DISPLAY_NAME,\n verifyAuth: (signal?: AbortSignal) => getClient().verifyAuth(signal),\n fetchConversations: (opts: WearableFetchOptions) => getClient().fetchConversations(opts),\n };\n}\n\nexport const wearableConnectorRegistration: WearableConnectorRegistration = {\n id: DESKTOP_SOURCE_ID,\n displayName: DESKTOP_DISPLAY_NAME,\n factory: createDesktopConnector,\n};\n\n/** Idempotently register the desktop connector with the core registry. */\nexport function ensureDesktopConnectorRegistered(): boolean {\n if (getWearableConnector(DESKTOP_SOURCE_ID) !== undefined) return false;\n registerWearableConnector(wearableConnectorRegistration);\n return true;\n}\n\nensureDesktopConnectorRegistered();\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/vad.ts","../src/connector.ts"],"sourcesContent":["import { statSync } from \"node:fs\";\n\nimport { CaptureConfigError } from \"./errors.js\";\nimport { expandTilde } from \"./paths.js\";\n\n\nexport interface SileroVadInput {\n modelPath: string;\n minSpeechMs: number;\n minSilenceMs?: number;\n maxSpeechMs?: number;\n threshold?: number;\n threads?: number;\n}\n\nexport interface SherpaOnnxModule {\n Vad: new (config: unknown, bufferSeconds: number) => unknown;\n}\n\nexport function sileroVadConfig(input: SileroVadInput): { config: object; bufferSeconds: number } {\n if (typeof input.modelPath !== \"string\" || input.modelPath.trim() === \"\") {\n throw new CaptureConfigError(\"Silero VAD modelPath must be a non-empty string\");\n }\n if (!Number.isFinite(input.minSpeechMs) || input.minSpeechMs <= 0) {\n throw new CaptureConfigError(\"Silero VAD minSpeechMs must be positive\");\n }\n const minSilenceMs = input.minSilenceMs ?? 500;\n const maxSpeechMs = input.maxSpeechMs ?? 30_000;\n const threshold = input.threshold ?? 0.5;\n const threads = input.threads ?? 1;\n if (!Number.isFinite(minSilenceMs) || minSilenceMs < 0 || !Number.isFinite(maxSpeechMs) || maxSpeechMs <= 0) {\n throw new CaptureConfigError(\"Silero VAD duration settings are invalid\");\n }\n if (maxSpeechMs < input.minSpeechMs) {\n throw new CaptureConfigError(\"Silero VAD maxSpeechMs must be greater than or equal to minSpeechMs\");\n }\n if (!Number.isFinite(threshold) || threshold <= 0 || threshold >= 1) {\n throw new CaptureConfigError(\"Silero VAD threshold must be between 0 and 1\");\n }\n if (!Number.isInteger(threads) || threads <= 0) {\n throw new CaptureConfigError(\"Silero VAD threads must be a positive integer\");\n }\n return {\n config: {\n sileroVad: {\n model: expandTilde(input.modelPath),\n threshold,\n minSpeechDuration: input.minSpeechMs / 1_000,\n minSilenceDuration: minSilenceMs / 1_000,\n maxSpeechDuration: maxSpeechMs / 1_000,\n windowSize: 512,\n },\n sampleRate: 16_000,\n debug: false,\n numThreads: threads,\n },\n bufferSeconds: 60,\n };\n}\n\nexport function resolveSherpaExport(module: unknown): SherpaOnnxModule | null {\n const candidates = [module, (module as { default?: unknown } | null)?.default];\n for (const candidate of candidates) {\n if (candidate && typeof candidate === \"object\" && typeof (candidate as SherpaOnnxModule).Vad === \"function\") {\n return candidate as SherpaOnnxModule;\n }\n }\n return null;\n}\n\nexport async function loadSherpaOnnx(\n importModule: (specifier: string) => Promise<unknown> = (specifier) => import(specifier),\n): Promise<SherpaOnnxModule> {\n // Computed specifier keeps the optional sherpa-onnx-node peer out of the static\n // dependency graph so the package builds and runs without it installed.\n const specifier = \"sherpa-onnx-\" + \"node\";\n let module: unknown;\n try {\n module = await importModule(specifier);\n } catch (error) {\n // A genuinely absent package surfaces as ERR_MODULE_NOT_FOUND naming the\n // specifier as a package, with no require stack. An installed-but-broken\n // package (missing native .node, dlopen failure) has a different code\n // and/or a require stack, so it must NOT be reported as \"not installed\".\n const err = error as NodeJS.ErrnoException & { requireStack?: unknown };\n const message = error instanceof Error ? error.message : \"\";\n const packageMissing =\n err.code === \"ERR_MODULE_NOT_FOUND\" &&\n err.requireStack === undefined &&\n message.includes(`package '${specifier}'`);\n if (packageMissing) {\n throw new CaptureConfigError(\n \"Silero VAD requires optional dependency sherpa-onnx-node; install it before enabling VAD\",\n );\n }\n // Installed but unusable. Report that distinctly, but keep the message\n // operator-safe: never echo the raw error (it can leak absolute native\n // library paths), per the CaptureConfigError contract.\n throw new CaptureConfigError(\n \"Silero VAD could not load the installed sherpa-onnx-node native runtime; verify its native build and shared-library dependencies\",\n );\n }\n const resolved = resolveSherpaExport(module);\n if (!resolved) {\n throw new CaptureConfigError(\"sherpa-onnx-node does not expose the Vad API required by capture-audio\");\n }\n return resolved;\n}\n\nfunction isRegularFile(filePath: string): boolean {\n try {\n return statSync(filePath).isFile();\n } catch {\n return false;\n }\n}\n\nexport async function createSileroVad(\n input: SileroVadInput,\n load: () => Promise<SherpaOnnxModule> = loadSherpaOnnx,\n exists: (path: string) => boolean = isRegularFile,\n): Promise<unknown> {\n const { config, bufferSeconds } = sileroVadConfig(input);\n const modelPath = expandTilde(input.modelPath);\n if (!exists(modelPath)) {\n throw new CaptureConfigError(`Silero VAD model not found at ${modelPath}; set vad.modelPath to a readable model file`);\n }\n const { Vad } = await load();\n return new Vad(config, bufferSeconds);\n}\n","/**\n * `desktop` wearable source connector (issue #1897, component 4).\n *\n * À-la-carte optional companion of @remnic/core: installing core alone\n * never pulls this in; core discovers it at runtime via a\n * computed-specifier dynamic import (registry entry {id:\"desktop\",\n * suffix:\"capture-audio\"}) or via a direct import of @remnic/capture-audio,\n * which self-registers idempotently.\n *\n * The connector is a pure API client + normalizer over the capture-audio\n * daemon's loopback HTTP API: no file IO beyond reading the local token,\n * no memory writes, no pipeline behavior (all of that stays in core so\n * desktop audio gets the same cleanup/corrections/trust gating as every\n * other wearable source).\n *\n * Token resolution (in order): settings.apiKey (config) ->\n * REMNIC_CAPTURE_AUDIO_TOKEN env -> the daemon's local token file\n * (~/.remnic/capture/token) when the base URL is loopback.\n */\n\nimport { readFileSync } from \"node:fs\";\n\nimport {\n registerWearableConnector,\n getWearableConnector,\n type WearableAuthCheck,\n type WearableConnectorFactoryOptions,\n type WearableConnectorRegistration,\n type WearableConversation,\n type WearableFetchOptions,\n type WearableFetchPage,\n type WearableSourceConnector,\n type WearableTranscriptSegment,\n} from \"@remnic/core\";\n\nimport { DEFAULT_HOST, DEFAULT_PORT } from \"./constants.js\";\nimport { capturePaths } from \"./paths.js\";\nimport { isLoopbackHost } from \"./util.js\";\nimport type { ConversationPage, DaemonConversation, DaemonSegment } from \"./spool.js\";\n\nexport const DESKTOP_SOURCE_ID = \"desktop\";\nexport const DESKTOP_DISPLAY_NAME = \"Desktop audio\";\nconst DEFAULT_BASE_URL = `http://${DEFAULT_HOST}:${DEFAULT_PORT}`;\n\n/** Bounded probe timeout so a wedged daemon reports unreachable instead of hanging. */\nconst PROBE_TIMEOUT_MS = 15_000;\n\nfunction withTimeout(signal: AbortSignal | undefined): AbortSignal {\n const timeout = AbortSignal.timeout(PROBE_TIMEOUT_MS);\n return signal ? AbortSignal.any([signal, timeout]) : timeout;\n}\n\n/** Error raised for a genuine backend failure (never for an empty day). */\nexport class DesktopDaemonError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"DesktopDaemonError\";\n }\n}\n\nfunction baseUrlIsLoopback(baseUrl: string): boolean {\n try {\n return isLoopbackHost(new URL(baseUrl).hostname);\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve the daemon bearer token. The local token file is read ONLY for\n * a loopback base URL — a remote reader must supply the token explicitly\n * (config/env), never inherit this machine's local token.\n */\nexport function resolveCaptureAudioToken(\n configured: string | undefined,\n baseUrl: string,\n env: NodeJS.ProcessEnv = process.env,\n): string | undefined {\n if (typeof configured === \"string\" && configured.trim().length > 0) return configured.trim();\n const fromEnv = env.REMNIC_CAPTURE_AUDIO_TOKEN;\n if (typeof fromEnv === \"string\" && fromEnv.trim().length > 0) return fromEnv.trim();\n if (baseUrlIsLoopback(baseUrl)) {\n try {\n const token = readFileSync(capturePaths().tokenPath, \"utf8\").trim();\n if (token.length > 0) return token;\n } catch {\n // no local token file — treat as unauthenticated (health will 401)\n }\n }\n return undefined;\n}\n\nfunction daemonSegmentToWearable(seg: DaemonSegment): WearableTranscriptSegment {\n return {\n text: seg.textRaw,\n // The wearables speaker registry owns naming; the connector only\n // supplies the stable per-source key (spk_<n> | self | \"unknown\").\n speakerKey: seg.speakerKey ?? \"unknown\",\n isWearer: seg.isWearer,\n startIso: seg.startUtc,\n endIso: seg.endUtc,\n };\n}\n\nexport function daemonConversationToWearable(conv: DaemonConversation): WearableConversation {\n return {\n id: conv.id,\n source: DESKTOP_SOURCE_ID,\n startIso: conv.startedAtUtc,\n endIso: conv.endedAtUtc ?? undefined,\n segments: conv.segments.map(daemonSegmentToWearable),\n };\n}\n\ninterface DesktopClientOptions {\n baseUrl: string;\n /** Resolved per request so env / token-file rotation applies without a rebuild. */\n getToken: () => string | undefined;\n}\n\n/** Linear trailing-slash trim (avoids a backtracking regex on the base URL). */\nfunction trimTrailingSlashes(value: string): string {\n let end = value.length;\n while (end > 0 && value.charCodeAt(end - 1) === 47 /* \"/\" */) end--;\n return value.slice(0, end);\n}\n\nclass DesktopClient {\n #baseUrl: string;\n #getToken: () => string | undefined;\n\n constructor(options: DesktopClientOptions) {\n this.#baseUrl = trimTrailingSlashes(options.baseUrl);\n this.#getToken = options.getToken;\n }\n\n #headers(): Record<string, string> {\n const token = this.#getToken();\n return token ? { authorization: `Bearer ${token}` } : {};\n }\n\n async verifyAuth(signal?: AbortSignal): Promise<WearableAuthCheck> {\n let res: Response;\n try {\n res = await fetch(`${this.#baseUrl}/v1/health`, { headers: this.#headers(), signal: withTimeout(signal) });\n } catch (err) {\n // Honor caller cancellation; do not mislabel a deliberate abort.\n if (signal?.aborted) throw err;\n // Timeout / connection refused / DNS / offline: the daemon isn't\n // reachable. This is NOT an auth failure and must never throw (AC2).\n return { ok: false, detail: \"unreachable\" };\n }\n if (res.status === 401) return { ok: false, detail: \"unauthorized\" };\n if (!res.ok) return { ok: false, detail: `HTTP ${res.status}` };\n let body: { ok?: unknown; version?: unknown };\n try {\n body = (await res.json()) as { ok?: unknown; version?: unknown };\n } catch (err) {\n // Honor caller cancellation during the body read; a non-JSON body\n // otherwise just reads as unhealthy (never throws for that, AC2).\n if (signal?.aborted) throw err;\n body = {};\n }\n if (body.ok === true) {\n return { ok: true, detail: typeof body.version === \"string\" ? `capture-audio ${body.version}` : undefined };\n }\n return { ok: false, detail: \"unhealthy\" };\n }\n\n async fetchConversations(opts: WearableFetchOptions): Promise<WearableFetchPage> {\n let res: Response;\n try {\n const url = new URL(`${this.#baseUrl}/v1/conversations`);\n url.searchParams.set(\"date\", opts.date);\n url.searchParams.set(\"timezone\", opts.timezone);\n if (opts.cursor) url.searchParams.set(\"cursor\", opts.cursor);\n res = await fetch(url, { headers: this.#headers(), signal: withTimeout(opts.signal) });\n } catch (err) {\n // Honor caller cancellation instead of reporting a daemon fault.\n if (opts.signal?.aborted) throw err;\n // Timeout, offline daemon, or a malformed configured baseUrl all\n // surface consistently as unreachable (never a raw TypeError).\n throw new DesktopDaemonError(\n `desktop capture daemon unreachable at ${this.#baseUrl} (is remnic-capture-audio running?)`,\n );\n }\n if (!res.ok) {\n // A non-2xx is a backend/auth failure, distinct from an empty day.\n throw new DesktopDaemonError(`desktop capture daemon returned HTTP ${res.status}`);\n }\n let page: ConversationPage;\n try {\n page = (await res.json()) as ConversationPage;\n } catch (err) {\n // Honor caller cancellation that lands mid-body-read.\n if (opts.signal?.aborted) throw err;\n // A 200 with an empty/non-JSON body is a backend fault, not an empty day.\n throw new DesktopDaemonError(\"desktop capture daemon returned a non-JSON conversations response\");\n }\n if (!page || !Array.isArray(page.conversations)) {\n throw new DesktopDaemonError(\"desktop capture daemon returned a malformed conversations page\");\n }\n return {\n conversations: page.conversations.map(daemonConversationToWearable),\n nextCursor: page.nextCursor ?? null,\n };\n }\n}\n\nexport function createDesktopConnector(options: WearableConnectorFactoryOptions): WearableSourceConnector {\n const baseUrl = options.settings.baseUrl?.trim() || DEFAULT_BASE_URL;\n // One client per connector, but the token is resolved per request (below)\n // so env / on-disk token rotation takes effect without a rebuild.\n let client: DesktopClient | null = null;\n const getClient = (): DesktopClient => {\n if (!client) {\n client = new DesktopClient({ baseUrl, getToken: () => resolveCaptureAudioToken(options.settings.apiKey, baseUrl) });\n }\n return client;\n };\n return {\n id: DESKTOP_SOURCE_ID,\n displayName: DESKTOP_DISPLAY_NAME,\n verifyAuth: (signal?: AbortSignal) => getClient().verifyAuth(signal),\n fetchConversations: (opts: WearableFetchOptions) => getClient().fetchConversations(opts),\n };\n}\n\nexport const wearableConnectorRegistration: WearableConnectorRegistration = {\n id: DESKTOP_SOURCE_ID,\n displayName: DESKTOP_DISPLAY_NAME,\n factory: createDesktopConnector,\n};\n\n/** Idempotently register the desktop connector with the core registry. */\nexport function ensureDesktopConnectorRegistered(): boolean {\n if (getWearableConnector(DESKTOP_SOURCE_ID) !== undefined) return false;\n registerWearableConnector(wearableConnectorRegistration);\n return true;\n}\n\nensureDesktopConnectorRegistered();\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/capture-audio",
|
|
3
|
-
"version": "9.63.
|
|
3
|
+
"version": "9.63.2",
|
|
4
4
|
"description": "Desktop audio capture daemon for Remnic — local spool, loopback HTTP API, and replay ingestion for the wearable source `desktop` (à-la-carte)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,13 +29,13 @@
|
|
|
29
29
|
"tsup": "^8.0.0",
|
|
30
30
|
"typescript": "^5.7.0",
|
|
31
31
|
"tsx": "^4.0.0",
|
|
32
|
-
"@remnic/core": "9.63.
|
|
32
|
+
"@remnic/core": "9.63.2"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@remnic/core": "^9.63.
|
|
35
|
+
"@remnic/core": "^9.63.2",
|
|
36
36
|
"sherpa-onnx-node": "*",
|
|
37
|
-
"@remnic/capture-native-darwin-arm64": "^9.63.
|
|
38
|
-
"@remnic/capture-native-darwin-x64": "^9.63.
|
|
37
|
+
"@remnic/capture-native-darwin-arm64": "^9.63.2",
|
|
38
|
+
"@remnic/capture-native-darwin-x64": "^9.63.2"
|
|
39
39
|
},
|
|
40
40
|
"peerDependenciesMeta": {
|
|
41
41
|
"sherpa-onnx-node": {
|
|
@@ -67,6 +67,6 @@
|
|
|
67
67
|
"build": "tsup src/index.ts src/cli-bin.ts --format esm --dts",
|
|
68
68
|
"precheck-types": "node ../../scripts/ensure-bench-build-deps.mjs",
|
|
69
69
|
"check-types": "tsc --noEmit",
|
|
70
|
-
"test": "NODE_OPTIONS=\"${NODE_OPTIONS:+$NODE_OPTIONS }--conditions=remnic-source\" tsx --test src/config.test.ts src/validate.test.ts src/token.test.ts src/spool.test.ts src/replay.test.ts src/daemon.test.ts src/cli.test.ts src/stt.test.ts src/model.test.ts src/janitor.test.ts src/vad.test.ts src/dedup.test.ts src/assembly.test.ts src/diarization.test.ts src/connector.test.ts src/e2e.test.ts src/native.test.ts src/processor.test.ts src/capture.test.ts src/service.test.ts src/enroll.test.ts"
|
|
70
|
+
"test": "NODE_OPTIONS=\"${NODE_OPTIONS:+$NODE_OPTIONS }--conditions=remnic-source\" tsx --test src/config.test.ts src/validate.test.ts src/token.test.ts src/spool.test.ts src/replay.test.ts src/daemon.test.ts src/cli.test.ts src/stt.test.ts src/model.test.ts src/janitor.test.ts src/vad.test.ts src/dedup.test.ts src/assembly.test.ts src/diarization.test.ts src/connector.test.ts src/e2e.test.ts src/native.test.ts src/processor.test.ts src/processor-policy.test.ts src/capture.test.ts src/service.test.ts src/enroll.test.ts"
|
|
71
71
|
}
|
|
72
72
|
}
|