@remnic/capture-audio 9.63.1 → 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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
var CAPTURE_AUDIO_VERSION = "9.14.0";
|
|
5
5
|
var DEFAULT_HOST = "127.0.0.1";
|
|
6
6
|
var DEFAULT_PORT = 4340;
|
|
7
|
-
var SPOOL_SCHEMA_VERSION =
|
|
7
|
+
var SPOOL_SCHEMA_VERSION = 3;
|
|
8
8
|
var MAX_CONVERSATIONS_LIMIT = 500;
|
|
9
9
|
var DEFAULT_CONVERSATIONS_LIMIT = 50;
|
|
10
10
|
|
|
@@ -921,6 +921,30 @@ async function pruneExpiredRawAudio(rawDirectory, retentionMs, nowMs = Date.now(
|
|
|
921
921
|
return removed.sort();
|
|
922
922
|
}
|
|
923
923
|
|
|
924
|
+
// src/buffer-policy.ts
|
|
925
|
+
var MAX_BUFFERED_CHUNKS = 512;
|
|
926
|
+
var QUARANTINE_AFTER_FAILURES = 3;
|
|
927
|
+
var MAX_QUARANTINED_CHUNKS = 64;
|
|
928
|
+
var ChunkApplyError = class extends Error {
|
|
929
|
+
chunkId;
|
|
930
|
+
constructor(chunkId, cause) {
|
|
931
|
+
super(cause instanceof Error ? cause.message : String(cause));
|
|
932
|
+
this.name = "ChunkApplyError";
|
|
933
|
+
this.chunkId = chunkId;
|
|
934
|
+
if (cause instanceof Error) this.cause = cause;
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
function spansOverlap(left, right) {
|
|
938
|
+
return left.startMs < right.endMs && right.startMs < left.endMs;
|
|
939
|
+
}
|
|
940
|
+
function isReleaseEligible(candidate, threshold, held) {
|
|
941
|
+
if (candidate.endMs > threshold) return false;
|
|
942
|
+
for (const other of held) {
|
|
943
|
+
if (spansOverlap(candidate, other)) return false;
|
|
944
|
+
}
|
|
945
|
+
return true;
|
|
946
|
+
}
|
|
947
|
+
|
|
924
948
|
// src/spool.ts
|
|
925
949
|
import { chmodSync as chmodSync2 } from "fs";
|
|
926
950
|
import { DatabaseSync } from "sqlite";
|
|
@@ -971,6 +995,16 @@ CREATE TABLE IF NOT EXISTS applied_chunks (
|
|
|
971
995
|
conversation_id TEXT NOT NULL,
|
|
972
996
|
applied_at_utc TEXT NOT NULL
|
|
973
997
|
);
|
|
998
|
+
CREATE TABLE IF NOT EXISTS pending_chunks (
|
|
999
|
+
id TEXT PRIMARY KEY,
|
|
1000
|
+
wav_path TEXT NOT NULL,
|
|
1001
|
+
started_at_utc TEXT NOT NULL,
|
|
1002
|
+
ended_at_utc TEXT NOT NULL,
|
|
1003
|
+
channel TEXT NOT NULL,
|
|
1004
|
+
device TEXT,
|
|
1005
|
+
reason TEXT NOT NULL,
|
|
1006
|
+
created_at_utc TEXT NOT NULL
|
|
1007
|
+
);
|
|
974
1008
|
CREATE INDEX IF NOT EXISTS idx_conv_keyset ON conversations(started_at_utc, id);
|
|
975
1009
|
CREATE INDEX IF NOT EXISTS idx_seg_conv ON segments(conversation_id, ordinal);
|
|
976
1010
|
`;
|
|
@@ -1504,6 +1538,42 @@ var Spool = class {
|
|
|
1504
1538
|
const row = this.#db.prepare("SELECT COUNT(*) AS n FROM chunks WHERE status = 'pending'").get();
|
|
1505
1539
|
return row.n;
|
|
1506
1540
|
}
|
|
1541
|
+
recordPendingChunk(input) {
|
|
1542
|
+
this.#db.prepare(
|
|
1543
|
+
"INSERT INTO pending_chunks(id, wav_path, started_at_utc, ended_at_utc, channel, device, reason, created_at_utc) VALUES (?,?,?,?,?,?,?,?) ON CONFLICT(id) DO UPDATE SET wav_path = excluded.wav_path, started_at_utc = excluded.started_at_utc, ended_at_utc = excluded.ended_at_utc, channel = excluded.channel, device = excluded.device, reason = excluded.reason, created_at_utc = excluded.created_at_utc"
|
|
1544
|
+
).run(
|
|
1545
|
+
input.id,
|
|
1546
|
+
input.wavPath,
|
|
1547
|
+
canonicalInstant(input.startedAtUtc, "pendingChunk.startedAtUtc"),
|
|
1548
|
+
canonicalInstant(input.endedAtUtc, "pendingChunk.endedAtUtc"),
|
|
1549
|
+
input.channel,
|
|
1550
|
+
input.device,
|
|
1551
|
+
input.reason,
|
|
1552
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
1553
|
+
);
|
|
1554
|
+
if (input.reason !== "quarantined") return;
|
|
1555
|
+
const extra = this.#db.prepare(
|
|
1556
|
+
"SELECT id FROM pending_chunks WHERE reason = 'quarantined' ORDER BY created_at_utc ASC, id ASC"
|
|
1557
|
+
).all();
|
|
1558
|
+
const overflow = extra.length - MAX_QUARANTINED_CHUNKS;
|
|
1559
|
+
if (overflow <= 0) return;
|
|
1560
|
+
const drop = this.#db.prepare("DELETE FROM pending_chunks WHERE id = ?");
|
|
1561
|
+
for (let i = 0; i < overflow; i++) {
|
|
1562
|
+
const id = extra[i]?.id;
|
|
1563
|
+
if (id !== void 0) drop.run(id);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
listPendingChunks(reason) {
|
|
1567
|
+
const rows = reason === void 0 ? this.#db.prepare(
|
|
1568
|
+
"SELECT id, wav_path AS wavPath, started_at_utc AS startedAtUtc, ended_at_utc AS endedAtUtc, channel, device, reason, created_at_utc AS createdAtUtc FROM pending_chunks ORDER BY created_at_utc ASC, id ASC"
|
|
1569
|
+
).all() : this.#db.prepare(
|
|
1570
|
+
"SELECT id, wav_path AS wavPath, started_at_utc AS startedAtUtc, ended_at_utc AS endedAtUtc, channel, device, reason, created_at_utc AS createdAtUtc FROM pending_chunks WHERE reason = ? ORDER BY created_at_utc ASC, id ASC"
|
|
1571
|
+
).all(reason);
|
|
1572
|
+
return rows;
|
|
1573
|
+
}
|
|
1574
|
+
deletePendingChunk(id) {
|
|
1575
|
+
this.#db.prepare("DELETE FROM pending_chunks WHERE id = ?").run(id);
|
|
1576
|
+
}
|
|
1507
1577
|
stats() {
|
|
1508
1578
|
const count = (table) => this.#db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get().n;
|
|
1509
1579
|
return { conversations: count("conversations"), segments: count("segments"), chunks: count("chunks") };
|
|
@@ -2290,7 +2360,6 @@ function transcriptManifestKey(chunkId) {
|
|
|
2290
2360
|
function chunkStableId(event) {
|
|
2291
2361
|
return `chk_${createHash2("sha1").update(event.path).digest("hex")}`;
|
|
2292
2362
|
}
|
|
2293
|
-
var MAX_BUFFERED_CHUNKS = 512;
|
|
2294
2363
|
function instantMs(value, field) {
|
|
2295
2364
|
const ms = Date.parse(value);
|
|
2296
2365
|
if (Number.isNaN(ms)) {
|
|
@@ -2314,7 +2383,7 @@ function lastEndMs(event, raw) {
|
|
|
2314
2383
|
}
|
|
2315
2384
|
function createChunkProcessor(deps) {
|
|
2316
2385
|
let tail = Promise.resolve();
|
|
2317
|
-
|
|
2386
|
+
const failCounts = /* @__PURE__ */ new Map();
|
|
2318
2387
|
let openConversationId = null;
|
|
2319
2388
|
const retainedChunks = /* @__PURE__ */ new Map();
|
|
2320
2389
|
function isHeldForReplay(conversationId) {
|
|
@@ -2397,6 +2466,17 @@ function createChunkProcessor(deps) {
|
|
|
2397
2466
|
} catch {
|
|
2398
2467
|
}
|
|
2399
2468
|
};
|
|
2469
|
+
function persistPending(entry, reason) {
|
|
2470
|
+
deps.spool.recordPendingChunk({
|
|
2471
|
+
id: entry.chunkId,
|
|
2472
|
+
wavPath: entry.event.path,
|
|
2473
|
+
startedAtUtc: entry.event.startedAtUtc,
|
|
2474
|
+
endedAtUtc: entry.event.endedAtUtc,
|
|
2475
|
+
channel: entry.event.channel,
|
|
2476
|
+
device: entry.event.device,
|
|
2477
|
+
reason
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2400
2480
|
async function process2(event) {
|
|
2401
2481
|
const chunkId = chunkStableId(event);
|
|
2402
2482
|
if (processedThisRun.has(chunkId) || bufferedIds.has(chunkId)) return;
|
|
@@ -2437,6 +2517,7 @@ function createChunkProcessor(deps) {
|
|
|
2437
2517
|
const evicted = buffer.shift();
|
|
2438
2518
|
if (evicted !== void 0) {
|
|
2439
2519
|
bufferedIds.delete(evicted.chunkId);
|
|
2520
|
+
persistPending(evicted, "evicted");
|
|
2440
2521
|
report(
|
|
2441
2522
|
new Error(
|
|
2442
2523
|
`reorder buffer is full (${MAX_BUFFERED_CHUNKS}); chunk ${evicted.chunkId} was dropped with its raw audio retained`
|
|
@@ -2466,19 +2547,18 @@ function createChunkProcessor(deps) {
|
|
|
2466
2547
|
return built;
|
|
2467
2548
|
}
|
|
2468
2549
|
async function applyBatch(batch, progress) {
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
}
|
|
2550
|
+
deps.assembler.rewind([]);
|
|
2551
|
+
openConversationId = null;
|
|
2552
|
+
const ownPrefix = batch.flatMap((entry) => deps.spool.conversationIdsForChunk(entry.chunkId)).map((id) => deps.spool.capturingConversationById(id)).find((conversation) => conversation !== null);
|
|
2553
|
+
const prior = ownPrefix ?? deps.spool.latestCapturingConversation();
|
|
2554
|
+
if (prior) {
|
|
2555
|
+
deps.assembler.resume(prior);
|
|
2556
|
+
openConversationId = prior.id;
|
|
2557
|
+
}
|
|
2558
|
+
const capturingNow = new Set(deps.spool.capturingConversationIds());
|
|
2559
|
+
for (const chunkId of deps.spool.incompleteChunkIds()) {
|
|
2560
|
+
const held = heldFor(chunkId, capturingNow);
|
|
2561
|
+
if (held.size > 0) retainedChunks.set(chunkId, held);
|
|
2482
2562
|
}
|
|
2483
2563
|
const resumable = resumableConversations();
|
|
2484
2564
|
for (const entry of batch) trackRetention(entry.chunkId, !isFullyProcessed(entry), resumable);
|
|
@@ -2512,7 +2592,11 @@ function createChunkProcessor(deps) {
|
|
|
2512
2592
|
);
|
|
2513
2593
|
if (deps.embed) {
|
|
2514
2594
|
for (const { entry, item } of fresh) {
|
|
2515
|
-
|
|
2595
|
+
try {
|
|
2596
|
+
item.seg.embedding = await deps.embed(entry.event, item.raw);
|
|
2597
|
+
} catch (error) {
|
|
2598
|
+
throw new ChunkApplyError(entry.chunkId, error);
|
|
2599
|
+
}
|
|
2516
2600
|
}
|
|
2517
2601
|
}
|
|
2518
2602
|
const runs = [];
|
|
@@ -2530,16 +2614,20 @@ function createChunkProcessor(deps) {
|
|
|
2530
2614
|
if (openConversationId !== null && openConversationId !== run.id) {
|
|
2531
2615
|
if (finalizeConv(openConversationId)) progress.persisted = true;
|
|
2532
2616
|
}
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2617
|
+
try {
|
|
2618
|
+
deps.spool.appendAssembledSegments({
|
|
2619
|
+
idempotencyKey: key,
|
|
2620
|
+
chunkId: key,
|
|
2621
|
+
conversationId: run.id,
|
|
2622
|
+
startedAtUtc: run.startedAtUtc,
|
|
2623
|
+
state: "capturing",
|
|
2624
|
+
device: event.device,
|
|
2625
|
+
wavPath: event.path,
|
|
2626
|
+
segments: [item.seg]
|
|
2627
|
+
});
|
|
2628
|
+
} catch (error) {
|
|
2629
|
+
throw new ChunkApplyError(chunkId, error);
|
|
2630
|
+
}
|
|
2543
2631
|
progress.persisted = true;
|
|
2544
2632
|
openConversationId = run.id;
|
|
2545
2633
|
}
|
|
@@ -2555,6 +2643,8 @@ function createChunkProcessor(deps) {
|
|
|
2555
2643
|
processedThisRun.add(entry.chunkId);
|
|
2556
2644
|
if (!fullyProcessed) continue;
|
|
2557
2645
|
deps.spool.markChunkComplete(entry.chunkId, openConversationId ?? "-");
|
|
2646
|
+
deps.spool.deletePendingChunk(entry.chunkId);
|
|
2647
|
+
failCounts.delete(entry.chunkId);
|
|
2558
2648
|
try {
|
|
2559
2649
|
await deps.cleanupRawAudio(entry.event);
|
|
2560
2650
|
} catch (err) {
|
|
@@ -2591,11 +2681,12 @@ function createChunkProcessor(deps) {
|
|
|
2591
2681
|
let finalFailure;
|
|
2592
2682
|
for (; ; ) {
|
|
2593
2683
|
const threshold = flushAll ? Number.POSITIVE_INFINITY : watermarkSourceMs - reorderWindowMs;
|
|
2684
|
+
const held = buffer.filter((entry) => entry.endMs > threshold);
|
|
2594
2685
|
const batch = [];
|
|
2595
2686
|
let manifestBlocked = false;
|
|
2596
2687
|
for (let i = buffer.length - 1; i >= 0; i--) {
|
|
2597
2688
|
const candidate = buffer[i];
|
|
2598
|
-
if (candidate
|
|
2689
|
+
if (!isReleaseEligible(candidate, threshold, held)) continue;
|
|
2599
2690
|
const manifest = candidate.manifestRecorded ? true : recordTranscriptManifest(candidate);
|
|
2600
2691
|
if (manifest === "conflict") {
|
|
2601
2692
|
buffer.splice(i, 1);
|
|
@@ -2629,6 +2720,7 @@ function createChunkProcessor(deps) {
|
|
|
2629
2720
|
}
|
|
2630
2721
|
if (batch.length === 0) {
|
|
2631
2722
|
if (flushAll && buffer.length > 0) {
|
|
2723
|
+
for (const entry of buffer) persistPending(entry, "evicted");
|
|
2632
2724
|
throw new Error("buffered chunks could not be released; they are retained for replay");
|
|
2633
2725
|
}
|
|
2634
2726
|
return;
|
|
@@ -2636,28 +2728,38 @@ function createChunkProcessor(deps) {
|
|
|
2636
2728
|
batch.sort(
|
|
2637
2729
|
(left, right) => left.startMs - right.startMs || left.endMs - right.endMs || (left.chunkId < right.chunkId ? -1 : left.chunkId > right.chunkId ? 1 : 0)
|
|
2638
2730
|
);
|
|
2639
|
-
const checkpoint = {
|
|
2640
|
-
assembler: deps.assembler.checkpoint(),
|
|
2641
|
-
recovered,
|
|
2642
|
-
openConversationId
|
|
2643
|
-
};
|
|
2644
2731
|
const progress = { persisted: false };
|
|
2645
2732
|
try {
|
|
2646
2733
|
await applyBatch(batch, progress);
|
|
2647
2734
|
} catch (error) {
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2735
|
+
deps.assembler.rewind([]);
|
|
2736
|
+
openConversationId = null;
|
|
2737
|
+
let quarantinedId;
|
|
2738
|
+
if (error instanceof ChunkApplyError) {
|
|
2739
|
+
const failures = (failCounts.get(error.chunkId) ?? 0) + 1;
|
|
2740
|
+
failCounts.set(error.chunkId, failures);
|
|
2741
|
+
if (failures >= QUARANTINE_AFTER_FAILURES) {
|
|
2742
|
+
const poisoned = batch.find((entry) => entry.chunkId === error.chunkId);
|
|
2743
|
+
if (poisoned !== void 0) {
|
|
2744
|
+
persistPending(poisoned, "quarantined");
|
|
2745
|
+
retainedChunks.delete(poisoned.chunkId);
|
|
2746
|
+
quarantinedId = poisoned.chunkId;
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2652
2749
|
}
|
|
2653
2750
|
for (const entry of batch) {
|
|
2654
2751
|
if (processedThisRun.has(entry.chunkId) || bufferedIds.has(entry.chunkId)) continue;
|
|
2752
|
+
if (entry.chunkId === quarantinedId) continue;
|
|
2655
2753
|
buffer.push(entry);
|
|
2656
2754
|
bufferedIds.add(entry.chunkId);
|
|
2657
2755
|
}
|
|
2658
|
-
if (flushAll)
|
|
2756
|
+
if (flushAll) {
|
|
2757
|
+
for (const entry of buffer) persistPending(entry, "evicted");
|
|
2758
|
+
finalFailure ??= error;
|
|
2759
|
+
}
|
|
2659
2760
|
report(error, batch[0].event);
|
|
2660
2761
|
if (finalFailure !== void 0) throw finalFailure;
|
|
2762
|
+
if (quarantinedId !== void 0) continue;
|
|
2661
2763
|
return;
|
|
2662
2764
|
}
|
|
2663
2765
|
deps.assembler.pruneFinalized();
|
|
@@ -2699,6 +2801,65 @@ function createChunkProcessor(deps) {
|
|
|
2699
2801
|
return { enqueue, drain, finalize };
|
|
2700
2802
|
}
|
|
2701
2803
|
|
|
2804
|
+
// src/orphan-scan.ts
|
|
2805
|
+
import { existsSync as existsSync2, lstatSync as lstatSync3, readdirSync as readdirSync2 } from "fs";
|
|
2806
|
+
import path8 from "path";
|
|
2807
|
+
var DEFAULT_CHUNK_MS = 3e4;
|
|
2808
|
+
function scanOrphanedChunks(input) {
|
|
2809
|
+
const recovered = /* @__PURE__ */ new Map();
|
|
2810
|
+
const quarantined = new Set(input.spool.listPendingChunks("quarantined").map((row) => row.id));
|
|
2811
|
+
for (const row of input.spool.listPendingChunks("evicted")) {
|
|
2812
|
+
if (quarantined.has(row.id)) continue;
|
|
2813
|
+
if (input.spool.isChunkApplied(`${row.id}:done`)) continue;
|
|
2814
|
+
if (!existsSync2(row.wavPath)) continue;
|
|
2815
|
+
recovered.set(row.id, {
|
|
2816
|
+
path: row.wavPath,
|
|
2817
|
+
channel: row.channel,
|
|
2818
|
+
startedAtUtc: row.startedAtUtc,
|
|
2819
|
+
endedAtUtc: row.endedAtUtc,
|
|
2820
|
+
device: row.device
|
|
2821
|
+
});
|
|
2822
|
+
}
|
|
2823
|
+
let root;
|
|
2824
|
+
try {
|
|
2825
|
+
root = lstatSync3(input.rawDirectory);
|
|
2826
|
+
} catch (error) {
|
|
2827
|
+
if (error.code === "ENOENT") {
|
|
2828
|
+
return [...recovered.values()].sort(byStart);
|
|
2829
|
+
}
|
|
2830
|
+
throw error;
|
|
2831
|
+
}
|
|
2832
|
+
if (root.isSymbolicLink() || !root.isDirectory()) return [...recovered.values()].sort(byStart);
|
|
2833
|
+
for (const name of readdirSync2(input.rawDirectory)) {
|
|
2834
|
+
if (!name.endsWith(".wav")) continue;
|
|
2835
|
+
const location = path8.join(input.rawDirectory, name);
|
|
2836
|
+
let stat;
|
|
2837
|
+
try {
|
|
2838
|
+
stat = lstatSync3(location);
|
|
2839
|
+
} catch (error) {
|
|
2840
|
+
if (error.code === "ENOENT") continue;
|
|
2841
|
+
throw error;
|
|
2842
|
+
}
|
|
2843
|
+
if (!stat.isFile()) continue;
|
|
2844
|
+
const event = {
|
|
2845
|
+
path: location,
|
|
2846
|
+
channel: "mic",
|
|
2847
|
+
startedAtUtc: new Date(stat.mtimeMs - DEFAULT_CHUNK_MS).toISOString(),
|
|
2848
|
+
endedAtUtc: new Date(stat.mtimeMs).toISOString(),
|
|
2849
|
+
device: null
|
|
2850
|
+
};
|
|
2851
|
+
const id = chunkStableId(event);
|
|
2852
|
+
if (quarantined.has(id) || recovered.has(id)) continue;
|
|
2853
|
+
if (input.spool.isChunkApplied(`${id}:done`)) continue;
|
|
2854
|
+
recovered.set(id, event);
|
|
2855
|
+
}
|
|
2856
|
+
return [...recovered.values()].sort(byStart);
|
|
2857
|
+
}
|
|
2858
|
+
function byStart(left, right) {
|
|
2859
|
+
if (left.startedAtUtc !== right.startedAtUtc) return left.startedAtUtc < right.startedAtUtc ? -1 : 1;
|
|
2860
|
+
return left.path < right.path ? -1 : left.path > right.path ? 1 : 0;
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2702
2863
|
// src/stt.ts
|
|
2703
2864
|
import { statSync as statSync2 } from "fs";
|
|
2704
2865
|
import { spawn } from "child_process";
|
|
@@ -2814,7 +2975,7 @@ function runWhisperCli(command, args) {
|
|
|
2814
2975
|
// src/capture.ts
|
|
2815
2976
|
import { realpathSync } from "fs";
|
|
2816
2977
|
import { rm as rm3 } from "fs/promises";
|
|
2817
|
-
import
|
|
2978
|
+
import path9 from "path";
|
|
2818
2979
|
function createLiveCapture(options) {
|
|
2819
2980
|
const { spool, config, outDir, defaultModelPath } = options;
|
|
2820
2981
|
const resolveModel = options.resolveModel ?? (() => resolveModelPath(config.stt.modelPath ?? void 0, defaultModelPath));
|
|
@@ -2825,23 +2986,23 @@ function createLiveCapture(options) {
|
|
|
2825
2986
|
threads: config.stt.threads,
|
|
2826
2987
|
run: runWhisperCli
|
|
2827
2988
|
}));
|
|
2828
|
-
const rawBase =
|
|
2989
|
+
const rawBase = path9.resolve(outDir);
|
|
2829
2990
|
const realOrResolved = (p) => {
|
|
2830
2991
|
try {
|
|
2831
|
-
return realpathSync(
|
|
2992
|
+
return realpathSync(path9.resolve(p));
|
|
2832
2993
|
} catch {
|
|
2833
|
-
return
|
|
2994
|
+
return path9.resolve(p);
|
|
2834
2995
|
}
|
|
2835
2996
|
};
|
|
2836
2997
|
const rawBaseReal = realOrResolved(rawBase);
|
|
2837
2998
|
const withinRawDir = (p) => {
|
|
2838
2999
|
const real = realOrResolved(p);
|
|
2839
|
-
return real === rawBaseReal || real.startsWith(rawBaseReal +
|
|
3000
|
+
return real === rawBaseReal || real.startsWith(rawBaseReal + path9.sep);
|
|
2840
3001
|
};
|
|
2841
3002
|
const cleanupRawAudio = options.cleanupRawAudio ?? (async (event) => {
|
|
2842
3003
|
if (config.rawRetentionHours > 0) return;
|
|
2843
3004
|
if (!withinRawDir(event.path)) return;
|
|
2844
|
-
await rm3(
|
|
3005
|
+
await rm3(path9.resolve(event.path), { force: true });
|
|
2845
3006
|
});
|
|
2846
3007
|
const assembler = new ConversationAssembler({
|
|
2847
3008
|
gapMinutes: config.conversationGapMinutes,
|
|
@@ -2881,7 +3042,7 @@ function createLiveCapture(options) {
|
|
|
2881
3042
|
options.onError?.(new CaptureInputError(`native helper chunk path escapes the capture directory: ${event.path}`));
|
|
2882
3043
|
return;
|
|
2883
3044
|
}
|
|
2884
|
-
processor.enqueue({ ...event, path:
|
|
3045
|
+
processor.enqueue({ ...event, path: path9.resolve(event.path) });
|
|
2885
3046
|
},
|
|
2886
3047
|
...options.onError ? { onError: options.onError } : {},
|
|
2887
3048
|
...options.onStderr ? { onStderr: options.onStderr } : {},
|
|
@@ -2898,6 +3059,9 @@ function createLiveCapture(options) {
|
|
|
2898
3059
|
processor,
|
|
2899
3060
|
start() {
|
|
2900
3061
|
resolveModel();
|
|
3062
|
+
for (const event of scanOrphanedChunks({ rawDirectory: outDir, spool })) {
|
|
3063
|
+
processor.enqueue(event);
|
|
3064
|
+
}
|
|
2901
3065
|
runner.start();
|
|
2902
3066
|
},
|
|
2903
3067
|
async stop() {
|
|
@@ -2943,7 +3107,7 @@ function enrollSelf(input) {
|
|
|
2943
3107
|
}
|
|
2944
3108
|
|
|
2945
3109
|
// src/service.ts
|
|
2946
|
-
import
|
|
3110
|
+
import path10 from "path";
|
|
2947
3111
|
var DEFAULT_SERVICE_LABEL = "com.remnic.capture-audio";
|
|
2948
3112
|
var SYSTEMD_UNIT_NAME = "remnic-capture-audio.service";
|
|
2949
3113
|
function xmlEscape(value) {
|
|
@@ -3024,7 +3188,7 @@ WantedBy=default.target
|
|
|
3024
3188
|
function planService(deps) {
|
|
3025
3189
|
const label = validateLabel(deps.spec.label ?? DEFAULT_SERVICE_LABEL);
|
|
3026
3190
|
if (deps.platform === "darwin") {
|
|
3027
|
-
const target =
|
|
3191
|
+
const target = path10.join(deps.home, "Library", "LaunchAgents", `${label}.plist`);
|
|
3028
3192
|
return {
|
|
3029
3193
|
platform: deps.platform,
|
|
3030
3194
|
path: target,
|
|
@@ -3034,7 +3198,7 @@ function planService(deps) {
|
|
|
3034
3198
|
}
|
|
3035
3199
|
if (deps.platform === "linux") {
|
|
3036
3200
|
const unitName = deps.spec.label ? `${label}.service` : SYSTEMD_UNIT_NAME;
|
|
3037
|
-
const target =
|
|
3201
|
+
const target = path10.join(deps.home, ".config", "systemd", "user", unitName);
|
|
3038
3202
|
return {
|
|
3039
3203
|
platform: deps.platform,
|
|
3040
3204
|
path: target,
|
|
@@ -3049,7 +3213,7 @@ function installService(deps) {
|
|
|
3049
3213
|
if (!deps.force && deps.exists?.(plan.path)) {
|
|
3050
3214
|
throw new CaptureConfigError(`a capture-audio service is already installed at ${plan.path} (use --force to replace)`);
|
|
3051
3215
|
}
|
|
3052
|
-
deps.mkdir(
|
|
3216
|
+
deps.mkdir(path10.dirname(plan.path));
|
|
3053
3217
|
deps.writeFile(plan.path, plan.contents);
|
|
3054
3218
|
return plan;
|
|
3055
3219
|
}
|
|
@@ -3062,8 +3226,8 @@ function uninstallService(deps) {
|
|
|
3062
3226
|
|
|
3063
3227
|
// src/cli.ts
|
|
3064
3228
|
import { spawn as spawn2 } from "child_process";
|
|
3065
|
-
import { chmodSync as chmodSync3, existsSync as
|
|
3066
|
-
import
|
|
3229
|
+
import { chmodSync as chmodSync3, existsSync as existsSync3, mkdirSync as mkdirSync3, openSync, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
3230
|
+
import path11 from "path";
|
|
3067
3231
|
import { setTimeout as delay } from "timers/promises";
|
|
3068
3232
|
import { homedir } from "os";
|
|
3069
3233
|
var VALUE_FLAGS = {
|
|
@@ -3130,7 +3294,7 @@ function resolvePaths(flags, env) {
|
|
|
3130
3294
|
return capturePaths(baseDir);
|
|
3131
3295
|
}
|
|
3132
3296
|
function loadConfigOrDefault(paths, stderr) {
|
|
3133
|
-
if (
|
|
3297
|
+
if (existsSync3(paths.configPath)) return loadDaemonConfig(paths.configPath);
|
|
3134
3298
|
stderr(`no config at ${paths.configPath}; using defaults (run \`init\` to customize)`);
|
|
3135
3299
|
return defaultDaemonConfig();
|
|
3136
3300
|
}
|
|
@@ -3158,7 +3322,7 @@ function recordHealthUrl(record, paths, stderr) {
|
|
|
3158
3322
|
return healthUrlFor(config.host, config.port);
|
|
3159
3323
|
}
|
|
3160
3324
|
function tokenHeader(paths) {
|
|
3161
|
-
if (!
|
|
3325
|
+
if (!existsSync3(paths.tokenPath)) return {};
|
|
3162
3326
|
return { authorization: `Bearer ${readFileSync6(paths.tokenPath, "utf8").trim()}` };
|
|
3163
3327
|
}
|
|
3164
3328
|
function ensurePrivateDir(dir) {
|
|
@@ -3237,7 +3401,7 @@ async function superviseReplay(spool, replayDir, io, signal) {
|
|
|
3237
3401
|
}
|
|
3238
3402
|
function cmdInit(paths, flags, stdout) {
|
|
3239
3403
|
ensurePrivateDir(paths.baseDir);
|
|
3240
|
-
if (
|
|
3404
|
+
if (existsSync3(paths.configPath) && flags.force !== true) {
|
|
3241
3405
|
stdout(`config already exists at ${paths.configPath} (use --force to overwrite)`);
|
|
3242
3406
|
} else {
|
|
3243
3407
|
writeFileSync3(paths.configPath, serializeDaemonConfig(defaultDaemonConfig()), "utf8");
|
|
@@ -3250,13 +3414,13 @@ function cmdInit(paths, flags, stdout) {
|
|
|
3250
3414
|
}
|
|
3251
3415
|
async function cmdDownloadModel(paths, flags, stdout, downloadModel) {
|
|
3252
3416
|
if (typeof flags.model !== "string") throw new CaptureInputError("flag --model requires a value");
|
|
3253
|
-
const result = await downloadModel({ model: flags.model, directory:
|
|
3417
|
+
const result = await downloadModel({ model: flags.model, directory: path11.join(paths.baseDir, "models") });
|
|
3254
3418
|
stdout(`${result.downloaded ? "downloaded" : "model already present"} ${flags.model} to ${result.path}`);
|
|
3255
3419
|
return 0;
|
|
3256
3420
|
}
|
|
3257
3421
|
async function cmdJanitor(paths, stdout, stderr) {
|
|
3258
3422
|
const config = loadConfigOrDefault(paths, stderr);
|
|
3259
|
-
const removed = await pruneExpiredRawAudio(
|
|
3423
|
+
const removed = await pruneExpiredRawAudio(path11.join(paths.baseDir, "raw"), config.rawRetentionHours * 60 * 60 * 1e3);
|
|
3260
3424
|
stdout(`janitor: removed ${removed.length} expired raw audio file(s)`);
|
|
3261
3425
|
return 0;
|
|
3262
3426
|
}
|
|
@@ -3333,13 +3497,13 @@ async function cmdStart(paths, flags, env, stdout, stderr, spawnArgvPrefix) {
|
|
|
3333
3497
|
let live = null;
|
|
3334
3498
|
if (flags.capture === true) {
|
|
3335
3499
|
try {
|
|
3336
|
-
const rawDir =
|
|
3500
|
+
const rawDir = path11.join(paths.baseDir, "raw");
|
|
3337
3501
|
mkdirSync3(rawDir, { recursive: true });
|
|
3338
3502
|
live = createLiveCapture({
|
|
3339
3503
|
spool,
|
|
3340
3504
|
config,
|
|
3341
3505
|
outDir: rawDir,
|
|
3342
|
-
defaultModelPath:
|
|
3506
|
+
defaultModelPath: path11.join(paths.baseDir, "models", "ggml-base.bin"),
|
|
3343
3507
|
onError: (e) => stderr(`capture: ${describeError(e)}`),
|
|
3344
3508
|
onStderr: (l) => stderr(`helper: ${l}`)
|
|
3345
3509
|
});
|
|
@@ -3504,7 +3668,7 @@ function cmdInstallService(paths, flags, env, stdout, spawnArgvPrefix) {
|
|
|
3504
3668
|
platform,
|
|
3505
3669
|
home,
|
|
3506
3670
|
spec,
|
|
3507
|
-
exists:
|
|
3671
|
+
exists: existsSync3,
|
|
3508
3672
|
remove: (f) => rmSync2(f, { force: true })
|
|
3509
3673
|
});
|
|
3510
3674
|
stdout(removed ? `removed ${plan2.path}` : `no capture-audio service installed at ${plan2.path}`);
|
|
@@ -3516,7 +3680,7 @@ function cmdInstallService(paths, flags, env, stdout, spawnArgvPrefix) {
|
|
|
3516
3680
|
home,
|
|
3517
3681
|
spec,
|
|
3518
3682
|
force: flags.force === true,
|
|
3519
|
-
exists:
|
|
3683
|
+
exists: existsSync3,
|
|
3520
3684
|
mkdir: (dir) => mkdirSync3(dir, { recursive: true }),
|
|
3521
3685
|
writeFile: (file, contents) => writeFileSync3(file, contents, { mode: 420 })
|
|
3522
3686
|
});
|
|
@@ -3540,7 +3704,7 @@ function cmdEnrollSelf(paths, flags, stdout) {
|
|
|
3540
3704
|
}
|
|
3541
3705
|
}
|
|
3542
3706
|
function cmdLogs(paths, flags, stdout) {
|
|
3543
|
-
if (!
|
|
3707
|
+
if (!existsSync3(paths.logPath)) {
|
|
3544
3708
|
stdout(`no log file at ${paths.logPath}`);
|
|
3545
3709
|
return 0;
|
|
3546
3710
|
}
|
|
@@ -3666,6 +3830,8 @@ export {
|
|
|
3666
3830
|
whisperModelUrl,
|
|
3667
3831
|
downloadWhisperModel,
|
|
3668
3832
|
pruneExpiredRawAudio,
|
|
3833
|
+
MAX_BUFFERED_CHUNKS,
|
|
3834
|
+
QUARANTINE_AFTER_FAILURES,
|
|
3669
3835
|
Spool,
|
|
3670
3836
|
assembleConversations,
|
|
3671
3837
|
DEFAULT_CONVERSATION_GAP_MINUTES,
|
|
@@ -3683,6 +3849,7 @@ export {
|
|
|
3683
3849
|
dedupeCrossChannel,
|
|
3684
3850
|
chunkStableId,
|
|
3685
3851
|
createChunkProcessor,
|
|
3852
|
+
scanOrphanedChunks,
|
|
3686
3853
|
parseWhisperJson,
|
|
3687
3854
|
resolveModelPath,
|
|
3688
3855
|
buildWhisperArgs,
|
|
@@ -3700,4 +3867,4 @@ export {
|
|
|
3700
3867
|
superviseReplay,
|
|
3701
3868
|
runCapture
|
|
3702
3869
|
};
|
|
3703
|
-
//# sourceMappingURL=chunk-
|
|
3870
|
+
//# sourceMappingURL=chunk-4Z3DZYEB.js.map
|