@hraness/sponge 0.2.6 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -9
- package/bin/sponge.js +8 -7
- package/dist/contracts/index.js +24 -6
- package/dist/contracts/validate.js +106 -29
- package/dist/migrations/008.d.ts +3 -0
- package/dist/migrations/008.js +5 -0
- package/dist/migrations/009.d.ts +3 -0
- package/dist/migrations/009.js +8 -0
- package/dist/migrations/010.d.ts +3 -0
- package/dist/migrations/010.js +11 -0
- package/dist/migrations/index.d.ts +15 -0
- package/dist/migrations/index.js +39 -0
- package/dist/src/acquisition/capture.js +87 -28
- package/dist/src/acquisition/cleanup.d.ts +0 -4
- package/dist/src/acquisition/cleanup.js +3 -5
- package/dist/src/acquisition/errors.js +17 -2
- package/dist/src/acquisition/files.d.ts +0 -1
- package/dist/src/acquisition/files.js +14 -11
- package/dist/src/acquisition/index.d.ts +2 -3
- package/dist/src/acquisition/index.js +1 -2
- package/dist/src/acquisition/lock.js +1 -1
- package/dist/src/acquisition/normalize.js +57 -18
- package/dist/src/acquisition/pins.js +22 -4
- package/dist/src/acquisition/process.js +67 -29
- package/dist/src/acquisition/runtime.d.ts +5 -7
- package/dist/src/acquisition/runtime.js +129 -68
- package/dist/src/acquisition/verify-helper.js +10 -3
- package/dist/src/annotations/quote.js +40 -12
- package/dist/src/annotations/store.d.ts +0 -1
- package/dist/src/annotations/store.js +66 -14
- package/dist/src/app/article.d.ts +2 -1
- package/dist/src/app/article.js +66 -70
- package/dist/src/app/bundle-files.d.ts +0 -4
- package/dist/src/app/bundle-files.js +11 -14
- package/dist/src/app/markdown.js +18 -6
- package/dist/src/app/source.d.ts +0 -4
- package/dist/src/app/source.js +18 -14
- package/dist/src/cli/arguments.d.ts +2 -8
- package/dist/src/cli/arguments.js +67 -80
- package/dist/src/cli/commands.d.ts +42 -0
- package/dist/src/cli/commands.js +795 -0
- package/dist/src/cli/config.js +12 -9
- package/dist/src/cli/digest.d.ts +1 -1
- package/dist/src/cli/digest.js +38 -16
- package/dist/src/cli/import.js +25 -4
- package/dist/src/cli/indexing.d.ts +2 -0
- package/dist/src/cli/indexing.js +26 -9
- package/dist/src/cli/main.js +47 -386
- package/dist/src/cli/portability.d.ts +0 -2
- package/dist/src/cli/portability.js +13 -8
- package/dist/src/cli/queue.js +12 -8
- package/dist/src/cli/runtimes.d.ts +1 -3
- package/dist/src/cli/runtimes.js +13 -6
- package/dist/src/cli/terminal.js +1 -1
- package/dist/src/cli/worker.d.ts +7 -0
- package/dist/src/cli/worker.js +54 -15
- package/dist/src/derivations/kinds.d.ts +34 -0
- package/dist/src/derivations/kinds.js +14 -0
- package/dist/src/derivations/types.d.ts +48 -0
- package/dist/src/derivations/types.js +1 -0
- package/dist/src/digest/derivation.d.ts +25 -0
- package/dist/src/digest/derivation.js +124 -0
- package/dist/src/digest/editorial.d.ts +9 -46
- package/dist/src/digest/editorial.js +57 -49
- package/dist/src/digest/endpoint.js +63 -27
- package/dist/src/digest/pipeline.d.ts +49 -28
- package/dist/src/digest/pipeline.js +343 -100
- package/dist/src/digest/schema.d.ts +11 -25
- package/dist/src/digest/schema.js +86 -18
- package/dist/src/errors.d.ts +46 -0
- package/dist/src/errors.js +117 -0
- package/dist/src/identity/query-policy.d.ts +0 -5
- package/dist/src/identity/query-policy.js +61 -9
- package/dist/src/identity/uri.d.ts +6 -5
- package/dist/src/identity/uri.js +37 -18
- package/dist/src/identity/visibility.d.ts +11 -1
- package/dist/src/identity/visibility.js +81 -17
- package/dist/src/importers/apply.js +104 -36
- package/dist/src/importers/custody.js +27 -9
- package/dist/src/importers/inspect.js +125 -38
- package/dist/src/importers/jungle-digest.d.ts +3 -6
- package/dist/src/importers/jungle-digest.js +196 -69
- package/dist/src/importers/jungle-store.d.ts +15 -6
- package/dist/src/importers/jungle-store.js +122 -28
- package/dist/src/importers/jungle.d.ts +14 -14
- package/dist/src/importers/jungle.js +136 -31
- package/dist/src/importers/manifest.js +57 -21
- package/dist/src/importers/verify-helper.js +18 -4
- package/dist/src/indexing/image.js +29 -10
- package/dist/src/indexing/index.d.ts +37 -6
- package/dist/src/indexing/index.js +360 -124
- package/dist/src/indexing/jobs.js +156 -47
- package/dist/src/indexing/prune-run.d.ts +2 -0
- package/dist/src/indexing/prune-run.js +69 -26
- package/dist/src/indexing/prune.d.ts +12 -3
- package/dist/src/indexing/prune.js +85 -35
- package/dist/src/indexing/runner.d.ts +2 -1
- package/dist/src/indexing/runner.js +83 -40
- package/dist/src/indexing/upgrade.d.ts +9 -0
- package/dist/src/indexing/upgrade.js +79 -0
- package/dist/src/inference/asr/bridge.js +6 -2
- package/dist/src/inference/asr/protocol.js +109 -19
- package/dist/src/inference/asr/runtime.js +58 -21
- package/dist/src/inference/asr/windows.js +9 -5
- package/dist/src/inference/embedding/index.d.ts +2 -6
- package/dist/src/inference/embedding/index.js +89 -29
- package/dist/src/ingest/attempt.d.ts +29 -0
- package/dist/src/ingest/attempt.js +55 -0
- package/dist/src/media/capture-helper.js +7 -2
- package/dist/src/media/capture.d.ts +2 -1
- package/dist/src/media/capture.js +124 -43
- package/dist/src/media/handler.d.ts +4 -2
- package/dist/src/media/handler.js +88 -70
- package/dist/src/media/transcribe.js +63 -21
- package/dist/src/metadata/article.js +49 -17
- package/dist/src/models/artifacts.d.ts +10 -8
- package/dist/src/models/artifacts.js +237 -106
- package/dist/src/models/catalog.d.ts +2 -5
- package/dist/src/models/catalog.js +94 -25
- package/dist/src/models/download.js +55 -15
- package/dist/src/models/runtime.d.ts +0 -2
- package/dist/src/models/runtime.js +69 -33
- package/dist/src/pdf/capture-helper.js +3 -1
- package/dist/src/pdf/capture.d.ts +2 -1
- package/dist/src/pdf/capture.js +95 -27
- package/dist/src/pdf/errors.js +12 -1
- package/dist/src/pdf/handler.d.ts +2 -1
- package/dist/src/pdf/handler.js +70 -71
- package/dist/src/pdf/index.d.ts +1 -3
- package/dist/src/pdf/index.js +0 -2
- package/dist/src/pdf/input.js +12 -7
- package/dist/src/pdf/normalize.js +80 -21
- package/dist/src/pdf/source-identity.js +3 -2
- package/dist/src/pdf/source.js +12 -8
- package/dist/src/pdf/verify-helper.js +15 -3
- package/dist/src/portability/backup.d.ts +2 -9
- package/dist/src/portability/backup.js +106 -22
- package/dist/src/portability/database.js +51 -17
- package/dist/src/portability/export.js +35 -9
- package/dist/src/portability/files.d.ts +1 -4
- package/dist/src/portability/files.js +127 -67
- package/dist/src/portability/gc.js +65 -22
- package/dist/src/publish/component.js +1 -1
- package/dist/src/publish/feed.d.ts +2 -2
- package/dist/src/publish/feed.js +75 -29
- package/dist/src/publish/identity.d.ts +5 -1
- package/dist/src/publish/identity.js +31 -11
- package/dist/src/publish/index.d.ts +2 -3
- package/dist/src/publish/index.js +34 -21
- package/dist/src/publish/projection.d.ts +2 -9
- package/dist/src/publish/projection.js +21 -14
- package/dist/src/search/extension.js +3 -1
- package/dist/src/search/hybrid.d.ts +3 -8
- package/dist/src/search/hybrid.js +45 -11
- package/dist/src/search/lexical.js +18 -4
- package/dist/src/search/query.js +56 -25
- package/dist/src/search/vector.js +37 -10
- package/dist/src/service/launch-agent.d.ts +5 -3
- package/dist/src/service/launch-agent.js +272 -83
- package/dist/src/service/lifetime.js +47 -14
- package/dist/src/service/loop.d.ts +3 -3
- package/dist/src/service/loop.js +47 -19
- package/dist/src/store/annotations.d.ts +32 -0
- package/dist/src/store/annotations.js +44 -0
- package/dist/src/store/backups.d.ts +18 -0
- package/dist/src/store/backups.js +110 -0
- package/dist/src/store/blobs.d.ts +24 -0
- package/dist/src/store/blobs.js +139 -0
- package/dist/src/store/changes.d.ts +5 -12
- package/dist/src/store/changes.js +108 -56
- package/dist/src/store/context.d.ts +15 -0
- package/dist/src/store/context.js +19 -0
- package/dist/src/store/derivations.d.ts +42 -0
- package/dist/src/store/derivations.js +104 -0
- package/dist/src/store/files.d.ts +0 -2
- package/dist/src/store/files.js +19 -24
- package/dist/src/store/ids.d.ts +30 -0
- package/dist/src/store/ids.js +34 -0
- package/dist/src/store/imports.d.ts +26 -0
- package/dist/src/store/imports.js +57 -0
- package/dist/src/store/index.d.ts +3 -186
- package/dist/src/store/index.js +2 -809
- package/dist/src/store/lease.d.ts +20 -0
- package/dist/src/store/lease.js +62 -0
- package/dist/src/store/library.d.ts +75 -0
- package/dist/src/store/library.js +174 -0
- package/dist/src/store/open.d.ts +7 -0
- package/dist/src/store/open.js +183 -0
- package/dist/src/store/queue.d.ts +65 -0
- package/dist/src/store/queue.js +327 -0
- package/dist/src/store/revisions.d.ts +12 -0
- package/dist/src/store/revisions.js +189 -0
- package/dist/src/store/search-index.d.ts +63 -0
- package/dist/src/store/search-index.js +190 -0
- package/dist/src/store/sync-ready.d.ts +6 -0
- package/dist/src/store/sync-ready.js +121 -0
- package/dist/src/store/types.d.ts +1 -5
- package/dist/src/store/types.js +1 -8
- package/dist/src/util/canonical.d.ts +2 -0
- package/dist/src/util/canonical.js +17 -0
- package/dist/src/util/exact.d.ts +2 -0
- package/dist/src/util/exact.js +9 -0
- package/dist/src/util/fsync.d.ts +4 -0
- package/dist/src/util/fsync.js +22 -0
- package/dist/src/util/hash.d.ts +4 -0
- package/dist/src/util/hash.js +8 -0
- package/dist/src/worker/basic.d.ts +0 -1
- package/dist/src/worker/basic.js +23 -7
- package/dist/src/worker/custody.d.ts +4 -1
- package/dist/src/worker/custody.js +111 -38
- package/dist/src/worker/engine.d.ts +2 -1
- package/dist/src/worker/engine.js +42 -20
- package/dist/src/worker/identity.js +13 -5
- package/dist/src/worker/journal.d.ts +1 -1
- package/dist/src/worker/journal.js +54 -24
- package/dist/src/worker/lock.d.ts +18 -0
- package/dist/src/worker/lock.js +196 -0
- package/dist/src/worker/runtime-gc.d.ts +4 -3
- package/dist/src/worker/runtime-gc.js +68 -41
- package/dist/src/worker/supervisor-child.js +19 -6
- package/dist/src/worker/supervisor.js +41 -8
- package/native/runtime-manifest.json +4 -4
- package/package.json +10 -2
- package/dist/src/digest/store.d.ts +0 -57
- package/dist/src/digest/store.js +0 -79
- package/dist/src/store/foundation.d.ts +0 -7
- package/dist/src/store/foundation.js +0 -7
|
@@ -1,54 +1,68 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { StoreError } from '../store/index.js';
|
|
3
2
|
import { acquireWorkerCustody, proveAttemptCleanup } from '../worker/custody.js';
|
|
4
3
|
import { supervisedRunner } from '../worker/supervisor.js';
|
|
5
4
|
import { directory, writeJournal } from '../worker/journal.js';
|
|
6
|
-
import { embedBatch, embeddingFingerprint, encodeEmbeddingRequest, resolveIndexSpace, projectVector } from '../inference/embedding/index.js';
|
|
5
|
+
import { embedBatch, embeddingFingerprint, encodeEmbeddingRequest, resolveIndexSpace, projectVector, } from '../inference/embedding/index.js';
|
|
7
6
|
import { deriveEmbeddingImage } from './image.js';
|
|
8
7
|
import { failure } from '../acquisition/errors.js';
|
|
9
|
-
|
|
8
|
+
import { SpongeError, codeOf, publicCode } from '../errors.js';
|
|
9
|
+
/** Same durable worker lock/owner/supervisor as acquisition, with sequential bounded batches. Only inputs whose content the space
|
|
10
|
+
* has not embedded run: a delta after captures, or everything new for a new fingerprint. Captures during a run join it. */
|
|
10
11
|
export async function drainIndexQueue(store, options) {
|
|
11
12
|
const custody = await acquireWorkerCustody(store.root);
|
|
12
13
|
let owner, settled = true;
|
|
13
14
|
const results = [];
|
|
14
15
|
try {
|
|
15
|
-
owner = store.acquireOwner(custody.incarnation);
|
|
16
|
+
owner = store.queue.acquireOwner(custody.incarnation);
|
|
16
17
|
while (!options.signal?.aborted) {
|
|
17
|
-
const attempt = store.
|
|
18
|
+
const attempt = store.index.claim(owner, options.jobId);
|
|
18
19
|
if (!attempt)
|
|
19
20
|
break;
|
|
20
21
|
settled = false;
|
|
21
22
|
const attemptDirectory = await directory(join(custody.directory, 'attempts', attempt.attemptId)), scratchRoot = await directory(join(store.root, 'staging', attempt.attemptId));
|
|
22
|
-
await writeJournal(join(scratchRoot, 'ownership.json'), {
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
await writeJournal(join(scratchRoot, 'ownership.json'), {
|
|
24
|
+
schemaVersion: 1,
|
|
25
|
+
requestId: attempt.attemptId,
|
|
26
|
+
ownerIncarnation: attempt.ownerIncarnation,
|
|
27
|
+
});
|
|
28
|
+
const checkpoint = async (stage) => {
|
|
29
|
+
store.index.checkpoint(attempt, stage);
|
|
30
|
+
await writeJournal(join(attemptDirectory, 'checkpoint.json'), { schemaVersion: 1, ...attempt, stage });
|
|
31
|
+
await options.onCheckpoint?.(attempt, stage);
|
|
32
|
+
};
|
|
33
|
+
const controller = new AbortController(), cancel = () => {
|
|
34
|
+
store.queue.cancel(attempt.jobId);
|
|
35
|
+
controller.abort();
|
|
36
|
+
};
|
|
25
37
|
options.signal?.addEventListener('abort', cancel, { once: true });
|
|
26
|
-
const poll = setInterval(() => {
|
|
27
|
-
|
|
38
|
+
const poll = setInterval(() => {
|
|
39
|
+
try {
|
|
40
|
+
if (store.queue.cancelRequested(attempt.jobId))
|
|
41
|
+
controller.abort();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
28
44
|
controller.abort();
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
controller.abort();
|
|
32
|
-
} }, 100);
|
|
45
|
+
}
|
|
46
|
+
}, 100);
|
|
33
47
|
const runProcess = supervisedRunner(custody.directory, custody.incarnation, attempt.attemptId);
|
|
34
48
|
let result;
|
|
35
49
|
try {
|
|
36
50
|
if (options.signal?.aborted)
|
|
37
51
|
cancel();
|
|
38
52
|
await checkpoint('acquiring');
|
|
39
|
-
const job = store.
|
|
53
|
+
const job = store.index.job(attempt.jobId);
|
|
40
54
|
if (!job)
|
|
41
|
-
throw new
|
|
42
|
-
const generationId = String(job.generation_id)
|
|
55
|
+
throw new SpongeError('unavailable');
|
|
56
|
+
const generationId = String(job.generation_id);
|
|
57
|
+
store.index.refresh(attempt);
|
|
58
|
+
const status = store.index.status(generationId), space = resolveIndexSpace(options.runtime.helperSha256, status.fingerprint);
|
|
43
59
|
if (!space)
|
|
44
|
-
throw new
|
|
60
|
+
throw new SpongeError('integrity');
|
|
45
61
|
await checkpoint('indexing');
|
|
46
62
|
for (;;) {
|
|
47
|
-
if (controller.signal.aborted || store.
|
|
48
|
-
throw new
|
|
49
|
-
|
|
50
|
-
throw Object.assign(Error('stale-index'), { code: 'stale-index' });
|
|
51
|
-
const inputs = store.indexInputs(generationId, 16);
|
|
63
|
+
if (controller.signal.aborted || store.queue.cancelRequested(attempt.jobId))
|
|
64
|
+
throw new SpongeError('cancelled');
|
|
65
|
+
const inputs = store.index.inputs(generationId, 16);
|
|
52
66
|
if (!inputs.length)
|
|
53
67
|
break;
|
|
54
68
|
const batch = [];
|
|
@@ -57,35 +71,64 @@ export async function drainIndexQueue(store, options) {
|
|
|
57
71
|
batch.push({ kind: 'document', text: input.text });
|
|
58
72
|
continue;
|
|
59
73
|
}
|
|
60
|
-
const item = store.read(input.itemId), asset = item?.assets.find(a => a.sha256 === input.assetSha256);
|
|
74
|
+
const item = store.read(input.itemId), asset = item?.assets.find((a) => a.sha256 === input.assetSha256);
|
|
61
75
|
if (!item || item.revisionId !== input.revisionId || !asset)
|
|
62
|
-
throw new
|
|
63
|
-
const derived = await (options.deriveImage ?? deriveEmbeddingImage)(store.
|
|
76
|
+
throw new SpongeError('unavailable');
|
|
77
|
+
const derived = await (options.deriveImage ?? deriveEmbeddingImage)(store.blobs.read(asset), {
|
|
78
|
+
scratchRoot,
|
|
79
|
+
runProcess,
|
|
80
|
+
signal: controller.signal,
|
|
81
|
+
});
|
|
64
82
|
batch.push({ kind: 'image', png: derived.png });
|
|
65
83
|
}
|
|
66
|
-
const expected = encodeEmbeddingRequest(batch).inputHashes, embedded = await (options.embed ?? embedBatch)(batch, {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
const expected = encodeEmbeddingRequest(batch).inputHashes, embedded = await (options.embed ?? embedBatch)(batch, {
|
|
85
|
+
runtime: options.runtime,
|
|
86
|
+
scratchRoot,
|
|
87
|
+
runProcess,
|
|
88
|
+
signal: controller.signal,
|
|
89
|
+
});
|
|
90
|
+
if (embedded.fingerprint !== embeddingFingerprint(options.runtime.helperSha256) ||
|
|
91
|
+
embedded.inputHashes.length !== expected.length ||
|
|
92
|
+
embedded.inputHashes.some((hash, i) => hash !== expected[i]))
|
|
93
|
+
throw new SpongeError('integrity');
|
|
94
|
+
store.index.writeAttemptBatch(attempt, status.fingerprint, inputs.map((input, i) => ({
|
|
95
|
+
...input,
|
|
96
|
+
inputHash: expected[i],
|
|
97
|
+
vector: projectVector(embedded.vectors[i], space.dimensions),
|
|
98
|
+
})));
|
|
99
|
+
await options.onBatch?.(attempt, store.index.status(generationId).completed);
|
|
71
100
|
}
|
|
72
101
|
await checkpoint('promoting');
|
|
73
|
-
store.
|
|
102
|
+
store.index.complete(attempt);
|
|
74
103
|
result = { ok: true, jobId: attempt.jobId, generationId, cleanup: 'settled' };
|
|
75
104
|
}
|
|
76
105
|
catch (error) {
|
|
77
|
-
const
|
|
78
|
-
store.
|
|
79
|
-
const known = [
|
|
80
|
-
|
|
106
|
+
const reason = codeOf(error), code = publicCode(reason);
|
|
107
|
+
store.index.fail(attempt, reason);
|
|
108
|
+
const known = [
|
|
109
|
+
'integrity',
|
|
110
|
+
'unavailable',
|
|
111
|
+
'unsupported',
|
|
112
|
+
'cancelled',
|
|
113
|
+
'limit-exceeded',
|
|
114
|
+
'cleanup-required',
|
|
115
|
+
'runtime-missing',
|
|
116
|
+
'owner-busy',
|
|
117
|
+
];
|
|
118
|
+
result = {
|
|
119
|
+
...failure(attempt.attemptId, known.includes(code) ? code : 'unavailable', 'inference'),
|
|
120
|
+
cleanup: 'pending',
|
|
121
|
+
};
|
|
81
122
|
}
|
|
82
123
|
finally {
|
|
83
124
|
clearInterval(poll);
|
|
84
125
|
options.signal?.removeEventListener('abort', cancel);
|
|
85
126
|
}
|
|
86
|
-
settled =
|
|
127
|
+
settled =
|
|
128
|
+
!(result.ok === false && result.code === 'cleanup-required') &&
|
|
129
|
+
(await proveAttemptCleanup(custody.directory, custody.incarnation, attempt.attemptId));
|
|
87
130
|
if (settled) {
|
|
88
|
-
store.
|
|
131
|
+
store.index.settle(attempt);
|
|
89
132
|
result.cleanup = 'settled';
|
|
90
133
|
}
|
|
91
134
|
else
|
|
@@ -96,13 +139,13 @@ export async function drainIndexQueue(store, options) {
|
|
|
96
139
|
break;
|
|
97
140
|
}
|
|
98
141
|
if (!settled)
|
|
99
|
-
throw
|
|
142
|
+
throw new SpongeError('cleanup-required');
|
|
100
143
|
return results;
|
|
101
144
|
}
|
|
102
145
|
finally {
|
|
103
146
|
if (owner && settled) {
|
|
104
147
|
try {
|
|
105
|
-
store.releaseOwner(owner);
|
|
148
|
+
store.queue.releaseOwner(owner);
|
|
106
149
|
}
|
|
107
150
|
catch {
|
|
108
151
|
settled = false;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
/**
|
|
3
|
+
* Migration 10's code step. Each generation's JSON coverage becomes `index_items` and `index_inputs` rows keyed by content hash,
|
|
4
|
+
* and its embedded vectors move to `embeddings` under (fingerprint, content hash), so a vector shared by two generations or two
|
|
5
|
+
* items is stored once. All or nothing per generation: one unverifiable input leaves that generation's JSON coverage in place,
|
|
6
|
+
* where it never reads (as before). Then the copy of `vectors` is dropped and active generations catch up with captures made since they
|
|
7
|
+
* were built: their delta is queued, not a rebuild.
|
|
8
|
+
*/
|
|
9
|
+
export declare function incrementalIndex(db: DatabaseSync): void;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { chunkDocument, chunkHash, coverageMarker, documentHash, imageHash, trackLibrary } from './index.js';
|
|
2
|
+
const parse = (text) => {
|
|
3
|
+
try {
|
|
4
|
+
return JSON.parse(String(text));
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
/** Content key of one schema-9 coverage input, or undefined when its chunk hash no longer verifies (such a generation already could not build). */
|
|
11
|
+
function contentOf(db, input, chunks) {
|
|
12
|
+
if (input.kind === 'image')
|
|
13
|
+
return typeof input.assetSha256 === 'string' &&
|
|
14
|
+
input.derivationHash === imageHash(input.assetSha256) &&
|
|
15
|
+
input.sourceId === JSON.stringify([input.revisionId, 'image', input.assetSha256])
|
|
16
|
+
? input.derivationHash
|
|
17
|
+
: undefined;
|
|
18
|
+
if (input.kind !== 'document' ||
|
|
19
|
+
typeof input.representationId !== 'string' ||
|
|
20
|
+
!Number.isSafeInteger(input.ordinal) ||
|
|
21
|
+
input.sourceId !== JSON.stringify([input.revisionId, 'document', input.representationId, input.ordinal]))
|
|
22
|
+
return undefined;
|
|
23
|
+
if (!chunks.has(input.representationId)) {
|
|
24
|
+
const markdown = db
|
|
25
|
+
.prepare('SELECT markdown FROM representations WHERE id=? AND revision_id=?')
|
|
26
|
+
.get(input.representationId, input.revisionId)?.markdown;
|
|
27
|
+
chunks.set(input.representationId, typeof markdown === 'string' ? chunkDocument(markdown) : undefined);
|
|
28
|
+
}
|
|
29
|
+
const text = chunks.get(input.representationId)?.[input.ordinal];
|
|
30
|
+
return text !== undefined && input.derivationHash === chunkHash(input.representationId, input.ordinal, text)
|
|
31
|
+
? documentHash(text)
|
|
32
|
+
: undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Migration 10's code step. Each generation's JSON coverage becomes `index_items` and `index_inputs` rows keyed by content hash,
|
|
36
|
+
* and its embedded vectors move to `embeddings` under (fingerprint, content hash), so a vector shared by two generations or two
|
|
37
|
+
* items is stored once. All or nothing per generation: one unverifiable input leaves that generation's JSON coverage in place,
|
|
38
|
+
* where it never reads (as before). Then the copy of `vectors` is dropped and active generations catch up with captures made since they
|
|
39
|
+
* were built: their delta is queued, not a rebuild.
|
|
40
|
+
*/
|
|
41
|
+
export function incrementalIndex(db) {
|
|
42
|
+
const chunks = new Map();
|
|
43
|
+
for (const g of db.prepare('SELECT id,fingerprint,coverage_json FROM index_generations ORDER BY rowid').all()) {
|
|
44
|
+
const coverage = parse(g.coverage_json);
|
|
45
|
+
if (!coverage || coverage.version !== 1 || !Array.isArray(coverage.revisions) || !Array.isArray(coverage.inputs))
|
|
46
|
+
continue;
|
|
47
|
+
const revisions = coverage.revisions, inputs = coverage.inputs, content = new Map(), images = new Set(inputs.filter((i) => i?.kind === 'image').map((i) => i.sourceId));
|
|
48
|
+
if (!revisions.every((r) => typeof r?.itemId === 'string' &&
|
|
49
|
+
typeof r.revisionId === 'string' &&
|
|
50
|
+
db.prepare('SELECT 1 FROM revisions WHERE id=? AND item_id=?').get(r.revisionId, r.itemId)))
|
|
51
|
+
continue;
|
|
52
|
+
for (const input of inputs) {
|
|
53
|
+
const key = input && typeof input.sourceId === 'string' && typeof input.revisionId === 'string'
|
|
54
|
+
? contentOf(db, input, chunks)
|
|
55
|
+
: undefined;
|
|
56
|
+
if (!key || content.has(input.sourceId))
|
|
57
|
+
break;
|
|
58
|
+
content.set(input.sourceId, key);
|
|
59
|
+
}
|
|
60
|
+
if (content.size !== inputs.length)
|
|
61
|
+
continue;
|
|
62
|
+
for (const r of revisions)
|
|
63
|
+
db.prepare('INSERT OR IGNORE INTO index_items VALUES(?,?,?)').run(g.id, r.itemId, r.revisionId);
|
|
64
|
+
for (const input of inputs)
|
|
65
|
+
db.prepare('INSERT INTO index_inputs VALUES(?,?,?,?)').run(g.id, input.sourceId, input.revisionId, content.get(input.sourceId));
|
|
66
|
+
// A document vector is the embedding of its content exactly when its input hash is that content hash; first writer wins.
|
|
67
|
+
for (const v of db
|
|
68
|
+
.prepare('SELECT source_id,input_hash,vector FROM temp.vectors_v9 WHERE generation_id=? AND input_hash IS NOT NULL ORDER BY rowid')
|
|
69
|
+
.all(g.id)) {
|
|
70
|
+
const source = String(v.source_id), key = content.get(source);
|
|
71
|
+
if (!key || (!images.has(source) && v.input_hash !== key))
|
|
72
|
+
continue;
|
|
73
|
+
db.prepare('INSERT OR IGNORE INTO embeddings VALUES(?,?,?,?)').run(g.fingerprint, key, v.input_hash, v.vector);
|
|
74
|
+
}
|
|
75
|
+
db.prepare('UPDATE index_generations SET coverage_json=? WHERE id=?').run(coverageMarker, g.id);
|
|
76
|
+
}
|
|
77
|
+
db.exec('DROP TABLE temp.vectors_v9');
|
|
78
|
+
trackLibrary(db);
|
|
79
|
+
}
|
|
@@ -8,8 +8,12 @@ try {
|
|
|
8
8
|
child.stderr.pipe(process.stderr);
|
|
9
9
|
child.stdin.on('error', () => { });
|
|
10
10
|
child.stdin.end(JSON.stringify(config.request) + '\n');
|
|
11
|
-
child.once('error', () => {
|
|
12
|
-
|
|
11
|
+
child.once('error', () => {
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
});
|
|
14
|
+
child.once('close', (code) => {
|
|
15
|
+
process.exitCode = code ?? 1;
|
|
16
|
+
});
|
|
13
17
|
}
|
|
14
18
|
catch {
|
|
15
19
|
process.exitCode = 1;
|
|
@@ -1,14 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export const
|
|
4
|
-
|
|
1
|
+
import { sha256 } from '../../util/hash.js';
|
|
2
|
+
import { SpongeError } from '../../errors.js';
|
|
3
|
+
export const asrIdentity = {
|
|
4
|
+
engine: 'Cactus-Compute/needle3@93e88ede867e4311c39151285e74df513e4b835b',
|
|
5
|
+
model: 'Cactus-Compute/whistle@ca5287601bef25af26dcf2e1b2bdc0843a7c19e5',
|
|
6
|
+
modelSha256: 'b6e02f048568ac5d01a2042556c658061e699acbc0aa2a1439f52f3d461dffeb',
|
|
7
|
+
modelBytes: 16919407,
|
|
8
|
+
};
|
|
9
|
+
export const asrLimits = {
|
|
10
|
+
outputBytes: 4 * 1024 ** 2,
|
|
11
|
+
frameBytes: 4 * 1024 ** 2,
|
|
12
|
+
words: 30000,
|
|
13
|
+
maximumDurationMs: 3600000,
|
|
14
|
+
windowDurationMs: 60000,
|
|
15
|
+
overlapMs: 4000,
|
|
16
|
+
deadlineMs: 180000,
|
|
17
|
+
};
|
|
18
|
+
const fail = () => {
|
|
19
|
+
throw new SpongeError('integrity');
|
|
20
|
+
};
|
|
5
21
|
const obj = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : fail();
|
|
6
22
|
const finite = (v, min = 0, max = Number.MAX_SAFE_INTEGER) => typeof v === 'number' && Number.isFinite(v) && v >= min && v <= max ? v : fail();
|
|
7
|
-
function time(v) {
|
|
8
|
-
|
|
23
|
+
function time(v) {
|
|
24
|
+
const t = obj(v);
|
|
25
|
+
if (!Number.isSafeInteger(t.value) || !Number.isSafeInteger(t.timescale) || t.timescale <= 0)
|
|
26
|
+
fail();
|
|
27
|
+
return finite(t.value / t.timescale, 0, asrLimits.maximumDurationMs / 1000);
|
|
28
|
+
}
|
|
9
29
|
/** Decode only correlated native events; diagnostics never become error classifications. */
|
|
10
30
|
export function parseAsrOutput(stdout, requestId, sourceSha256, helperSha256) {
|
|
11
|
-
if (Buffer.byteLength(stdout) > asrLimits.outputBytes ||
|
|
31
|
+
if (Buffer.byteLength(stdout) > asrLimits.outputBytes ||
|
|
32
|
+
!/^[a-f0-9]{64}$/.test(sourceSha256) ||
|
|
33
|
+
!/^[a-f0-9]{64}$/.test(helperSha256))
|
|
12
34
|
fail();
|
|
13
35
|
let ready = false, result;
|
|
14
36
|
for (const line of stdout.split('\n')) {
|
|
@@ -18,43 +40,111 @@ export function parseAsrOutput(stdout, requestId, sourceSha256, helperSha256) {
|
|
|
18
40
|
fail();
|
|
19
41
|
const event = obj(JSON.parse(line));
|
|
20
42
|
if (event.type === 'ready') {
|
|
21
|
-
if (ready ||
|
|
43
|
+
if (ready ||
|
|
44
|
+
event.protocolVersion !== 2 ||
|
|
45
|
+
event.engine !== asrIdentity.engine ||
|
|
46
|
+
event.model !== asrIdentity.model)
|
|
22
47
|
fail();
|
|
23
48
|
ready = true;
|
|
24
49
|
}
|
|
25
50
|
else if (event.type === 'progress') {
|
|
26
|
-
if (!ready ||
|
|
51
|
+
if (!ready ||
|
|
52
|
+
result ||
|
|
53
|
+
event.id !== requestId ||
|
|
54
|
+
!['verifying', 'extracting', 'recognizing'].includes(event.stage))
|
|
27
55
|
fail();
|
|
28
56
|
}
|
|
29
57
|
else if (event.type === 'result') {
|
|
30
|
-
if (!ready ||
|
|
58
|
+
if (!ready ||
|
|
59
|
+
result ||
|
|
60
|
+
event.id !== requestId ||
|
|
61
|
+
event.sourceSha256 !== sourceSha256 ||
|
|
62
|
+
event.engine !== asrIdentity.engine ||
|
|
63
|
+
event.model !== asrIdentity.model)
|
|
31
64
|
fail();
|
|
32
65
|
result = event;
|
|
33
66
|
}
|
|
34
67
|
else if (event.type === 'error' || event.type === 'fatal')
|
|
35
|
-
throw
|
|
68
|
+
throw new SpongeError('unclassified');
|
|
36
69
|
else
|
|
37
70
|
fail();
|
|
38
71
|
}
|
|
39
72
|
if (!ready || !result || result.hasAudio !== true)
|
|
40
73
|
return fail();
|
|
41
74
|
const recognition = obj(result.recognition), map = obj(result.audioMap);
|
|
42
|
-
if (recognition.language !== 'en' ||
|
|
75
|
+
if (recognition.language !== 'en' ||
|
|
76
|
+
typeof recognition.text !== 'string' ||
|
|
77
|
+
!Array.isArray(recognition.words) ||
|
|
78
|
+
recognition.words.length > asrLimits.words ||
|
|
79
|
+
map.sampleRate !== 16000 ||
|
|
80
|
+
!Number.isSafeInteger(map.sampleCount) ||
|
|
81
|
+
map.sampleCount < 0 ||
|
|
82
|
+
map.sampleCount > asrLimits.windowDurationMs * 16 ||
|
|
83
|
+
!Array.isArray(map.spans) ||
|
|
84
|
+
map.spans.length > 30000)
|
|
43
85
|
fail();
|
|
44
86
|
const durationMs = time(map.sourceDuration) * 1000;
|
|
45
87
|
if (durationMs <= 0)
|
|
46
88
|
fail();
|
|
47
89
|
let sampleEnd = 0, sourceEnd = 0;
|
|
48
|
-
const spans = map.spans.map((raw) => {
|
|
49
|
-
|
|
90
|
+
const spans = map.spans.map((raw) => {
|
|
91
|
+
const s = obj(raw), range = obj(s.source), start = time(range.start), end = time(range.end);
|
|
92
|
+
if (!Number.isSafeInteger(s.sampleStart) ||
|
|
93
|
+
!Number.isSafeInteger(s.sampleEnd) ||
|
|
94
|
+
s.sampleStart !== sampleEnd ||
|
|
95
|
+
s.sampleEnd <= s.sampleStart ||
|
|
96
|
+
s.sampleEnd > map.sampleCount ||
|
|
97
|
+
start < sourceEnd ||
|
|
98
|
+
end <= start ||
|
|
99
|
+
end * 1000 > durationMs + 0.1 ||
|
|
100
|
+
!['audio', 'silence'].includes(s.kind))
|
|
101
|
+
fail();
|
|
102
|
+
sampleEnd = s.sampleEnd;
|
|
103
|
+
sourceEnd = end;
|
|
104
|
+
return { sampleStart: s.sampleStart, sampleEnd: s.sampleEnd, start, end };
|
|
105
|
+
});
|
|
50
106
|
if (sampleEnd !== map.sampleCount)
|
|
51
107
|
fail();
|
|
52
|
-
const sourceTime = (seconds, end) => {
|
|
53
|
-
|
|
108
|
+
const sourceTime = (seconds, end) => {
|
|
109
|
+
const sample = seconds * 16000;
|
|
110
|
+
const span = spans.find((s) => sample >= s.sampleStart && (end ? sample <= s.sampleEnd : sample < s.sampleEnd)) ??
|
|
111
|
+
(sample === map.sampleCount ? spans.at(-1) : undefined);
|
|
112
|
+
if (!span)
|
|
113
|
+
return fail();
|
|
114
|
+
return ((span.start + ((sample - span.sampleStart) / (span.sampleEnd - span.sampleStart)) * (span.end - span.start)) *
|
|
115
|
+
1000);
|
|
116
|
+
};
|
|
54
117
|
let prior = 0;
|
|
55
|
-
const segments = recognition.words.map((raw) => {
|
|
56
|
-
|
|
118
|
+
const segments = recognition.words.map((raw) => {
|
|
119
|
+
const w = obj(raw);
|
|
120
|
+
if (typeof w.word !== 'string' || !w.word.trim() || Buffer.byteLength(w.word) > 16384)
|
|
121
|
+
fail();
|
|
122
|
+
const start = finite(w.start, prior, map.sampleCount / 16000), end = finite(w.end, start, map.sampleCount / 16000);
|
|
123
|
+
prior = start;
|
|
124
|
+
return { startMs: sourceTime(start, false), endMs: sourceTime(end, true), text: w.word.trim() };
|
|
125
|
+
});
|
|
57
126
|
if (!segments.length && recognition.text.trim())
|
|
58
127
|
fail();
|
|
59
|
-
return {
|
|
128
|
+
return {
|
|
129
|
+
schemaVersion: 1,
|
|
130
|
+
requestId,
|
|
131
|
+
source: 'local-asr',
|
|
132
|
+
language: 'en',
|
|
133
|
+
modelFingerprint: sha256(JSON.stringify([
|
|
134
|
+
'sponge-asr-v1',
|
|
135
|
+
asrIdentity,
|
|
136
|
+
helperSha256,
|
|
137
|
+
'decoded-audio-map-window-v2',
|
|
138
|
+
{
|
|
139
|
+
windowDurationMs: asrLimits.windowDurationMs,
|
|
140
|
+
overlapMs: asrLimits.overlapMs,
|
|
141
|
+
join: 'source-midpoint-overlap-dedupe-v1',
|
|
142
|
+
},
|
|
143
|
+
])),
|
|
144
|
+
segments,
|
|
145
|
+
cleanup: 'settled',
|
|
146
|
+
sourceSha256,
|
|
147
|
+
timing: 'decoded-audio-map',
|
|
148
|
+
durationMs,
|
|
149
|
+
};
|
|
60
150
|
}
|
|
@@ -8,14 +8,15 @@ import { failure } from '../../acquisition/errors.js';
|
|
|
8
8
|
import { privateRoot } from '../../acquisition/files.js';
|
|
9
9
|
import { asrIdentity, asrLimits, parseAsrOutput } from './protocol.js';
|
|
10
10
|
import { audioWindows, joinWindows } from './windows.js';
|
|
11
|
+
import { SpongeError, codeOf } from '../../errors.js';
|
|
11
12
|
async function verify(path, sha256, maximum, expected) {
|
|
12
|
-
if (!isAbsolute(path) || !/^[a-f0-9]{64}$/.test(sha256) || await realpath(path) !== path)
|
|
13
|
-
throw
|
|
13
|
+
if (!isAbsolute(path) || !/^[a-f0-9]{64}$/.test(sha256) || (await realpath(path)) !== path)
|
|
14
|
+
throw new SpongeError('integrity');
|
|
14
15
|
const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
15
16
|
try {
|
|
16
17
|
const stat = await file.stat();
|
|
17
|
-
if (!stat.isFile() || stat.size > maximum || expected !== undefined && stat.size !== expected)
|
|
18
|
-
throw
|
|
18
|
+
if (!stat.isFile() || stat.size > maximum || (expected !== undefined && stat.size !== expected))
|
|
19
|
+
throw new SpongeError('integrity');
|
|
19
20
|
const hash = createHash('sha256'), buffer = Buffer.alloc(65536);
|
|
20
21
|
let bytes = 0;
|
|
21
22
|
for (;;) {
|
|
@@ -24,27 +25,38 @@ async function verify(path, sha256, maximum, expected) {
|
|
|
24
25
|
break;
|
|
25
26
|
bytes += bytesRead;
|
|
26
27
|
if (bytes > maximum)
|
|
27
|
-
throw
|
|
28
|
+
throw new SpongeError('limit-exceeded');
|
|
28
29
|
hash.update(buffer.subarray(0, bytesRead));
|
|
29
30
|
}
|
|
30
31
|
if (bytes !== stat.size || hash.digest('hex') !== sha256)
|
|
31
|
-
throw
|
|
32
|
+
throw new SpongeError('integrity');
|
|
32
33
|
}
|
|
33
34
|
finally {
|
|
34
35
|
await file.close();
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
/** Portable callers can use captions; this pinned native artifact requires macOS26 arm64. */
|
|
38
|
-
export function supportsNativeAsr(platform = process.platform, arch = process.arch, kernel = release()) {
|
|
39
|
+
export function supportsNativeAsr(platform = process.platform, arch = process.arch, kernel = release()) {
|
|
40
|
+
return platform === 'darwin' && arch === 'arm64' && /^\d+\./.test(kernel) && Number(kernel.split('.')[0]) >= 25;
|
|
41
|
+
}
|
|
39
42
|
export async function transcribeAudio(requestId, audio, language, options) {
|
|
40
|
-
const error = (code, cleanup = 'settled') => ({
|
|
43
|
+
const error = (code, cleanup = 'settled') => ({
|
|
44
|
+
...failure(requestId, code, cleanup === 'pending' ? 'cleanup' : 'inference'),
|
|
45
|
+
cleanup,
|
|
46
|
+
});
|
|
41
47
|
if (options.signal?.aborted)
|
|
42
48
|
return error('cancelled');
|
|
43
49
|
if (!supportsNativeAsr() || language !== 'en')
|
|
44
50
|
return error('unsupported');
|
|
45
51
|
if (!options.runtime)
|
|
46
52
|
return error('runtime-missing');
|
|
47
|
-
if (!/^[A-Za-z0-9_-]{1,128}$/.test(requestId) ||
|
|
53
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(requestId) ||
|
|
54
|
+
!Number.isSafeInteger(audio.bytes) ||
|
|
55
|
+
audio.bytes < 1 ||
|
|
56
|
+
audio.bytes > 512 * 1024 ** 2 ||
|
|
57
|
+
!Number.isFinite(audio.durationMs) ||
|
|
58
|
+
audio.durationMs <= 0 ||
|
|
59
|
+
audio.durationMs > asrLimits.maximumDurationMs)
|
|
48
60
|
return error('limit-exceeded');
|
|
49
61
|
let scratch, cleanup = 'settled', outcome;
|
|
50
62
|
try {
|
|
@@ -60,34 +72,59 @@ export async function transcribeAudio(requestId, audio, language, options) {
|
|
|
60
72
|
const windows = audioWindows(audio.durationMs), transcripts = [];
|
|
61
73
|
let totalWords = 0;
|
|
62
74
|
for (const window of windows) {
|
|
63
|
-
await writeFile(requestFile, JSON.stringify({
|
|
75
|
+
await writeFile(requestFile, JSON.stringify({
|
|
76
|
+
helper: runtime.helper,
|
|
77
|
+
model: runtime.model,
|
|
78
|
+
request: {
|
|
79
|
+
id: requestId,
|
|
80
|
+
sourcePath: audio.path,
|
|
81
|
+
expectedSourceSha256: audio.sha256,
|
|
82
|
+
windowStartMs: window.startMs,
|
|
83
|
+
windowEndMs: window.endMs,
|
|
84
|
+
},
|
|
85
|
+
}), { mode: 0o600, flag: transcripts.length ? 'w' : 'wx' });
|
|
64
86
|
cleanup = 'pending';
|
|
65
|
-
const result = await options.runProcess(process.execPath, source ? ['--import', import.meta.resolve('tsx'), entry, requestFile] : [entry, requestFile], {
|
|
87
|
+
const result = await options.runProcess(process.execPath, source ? ['--import', import.meta.resolve('tsx'), entry, requestFile] : [entry, requestFile], {
|
|
88
|
+
env: { PATH: '/usr/bin:/bin', HOME: scratch, TMPDIR: scratch, LANG: 'en_US.UTF-8' },
|
|
89
|
+
deadlineMs: asrLimits.deadlineMs,
|
|
90
|
+
maxOutputBytes: asrLimits.outputBytes,
|
|
91
|
+
signal: options.signal,
|
|
92
|
+
});
|
|
66
93
|
cleanup = result.cleanup;
|
|
67
94
|
if (cleanup === 'pending')
|
|
68
|
-
throw
|
|
95
|
+
throw new SpongeError('cleanup-required');
|
|
69
96
|
if (options.signal?.aborted || result.stopped === 'cancelled')
|
|
70
|
-
throw
|
|
97
|
+
throw new SpongeError('cancelled');
|
|
71
98
|
if (result.stopped)
|
|
72
|
-
throw
|
|
99
|
+
throw new SpongeError(result.stopped);
|
|
73
100
|
if (result.code !== 0)
|
|
74
|
-
throw
|
|
101
|
+
throw new SpongeError('unclassified');
|
|
75
102
|
const transcript = parseAsrOutput(result.stdout, requestId, audio.sha256, runtime.helperSha256);
|
|
76
103
|
if (Math.abs(transcript.durationMs - audio.durationMs) > 1000)
|
|
77
|
-
throw
|
|
78
|
-
if (transcript.segments.some(s => s.startMs < window.startMs - .1 || s.endMs > window.endMs + .1))
|
|
79
|
-
throw
|
|
104
|
+
throw new SpongeError('integrity');
|
|
105
|
+
if (transcript.segments.some((s) => s.startMs < window.startMs - 0.1 || s.endMs > window.endMs + 0.1))
|
|
106
|
+
throw new SpongeError('integrity');
|
|
80
107
|
totalWords += transcript.segments.length;
|
|
81
108
|
if (totalWords > asrLimits.words)
|
|
82
|
-
throw
|
|
109
|
+
throw new SpongeError('limit-exceeded');
|
|
83
110
|
transcripts.push(transcript);
|
|
84
111
|
}
|
|
85
112
|
await verify(audio.path, audio.sha256, 512 * 1024 ** 2, audio.bytes);
|
|
86
113
|
outcome = { ok: true, transcript: joinWindows(windows, transcripts), cleanup };
|
|
87
114
|
}
|
|
88
115
|
catch (e) {
|
|
89
|
-
const code = e
|
|
90
|
-
outcome = error(cleanup === 'pending'
|
|
116
|
+
const code = codeOf(e);
|
|
117
|
+
outcome = error(cleanup === 'pending'
|
|
118
|
+
? 'cleanup-required'
|
|
119
|
+
: code === 'integrity'
|
|
120
|
+
? 'integrity'
|
|
121
|
+
: code === 'limit-exceeded'
|
|
122
|
+
? 'limit-exceeded'
|
|
123
|
+
: code === 'cancelled'
|
|
124
|
+
? 'cancelled'
|
|
125
|
+
: code === 'unavailable'
|
|
126
|
+
? 'unavailable'
|
|
127
|
+
: 'unclassified', cleanup);
|
|
91
128
|
}
|
|
92
129
|
finally {
|
|
93
130
|
if (scratch && cleanup === 'settled') {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { asrLimits } from './protocol.js';
|
|
2
|
+
import { SpongeError } from '../../errors.js';
|
|
2
3
|
export function audioWindows(durationMs) {
|
|
3
4
|
if (!Number.isFinite(durationMs) || durationMs <= 0 || durationMs > asrLimits.maximumDurationMs)
|
|
4
|
-
throw
|
|
5
|
+
throw new SpongeError('limit-exceeded');
|
|
5
6
|
durationMs = Math.ceil(durationMs);
|
|
6
7
|
const result = [];
|
|
7
8
|
for (let startMs = 0; startMs < durationMs; startMs += asrLimits.windowDurationMs - asrLimits.overlapMs) {
|
|
@@ -16,12 +17,15 @@ export function audioWindows(durationMs) {
|
|
|
16
17
|
* words are removed only when their time ranges overlap, never globally. */
|
|
17
18
|
export function joinWindows(windows, transcripts) {
|
|
18
19
|
if (!transcripts.length || windows.length !== transcripts.length)
|
|
19
|
-
throw
|
|
20
|
+
throw new SpongeError('integrity');
|
|
20
21
|
const first = transcripts[0], segments = [];
|
|
21
22
|
for (let i = 0; i < windows.length; i++) {
|
|
22
23
|
const window = windows[i], transcript = transcripts[i];
|
|
23
|
-
if (transcript.sourceSha256 !== first.sourceSha256 ||
|
|
24
|
-
|
|
24
|
+
if (transcript.sourceSha256 !== first.sourceSha256 ||
|
|
25
|
+
transcript.durationMs !== first.durationMs ||
|
|
26
|
+
transcript.modelFingerprint !== first.modelFingerprint ||
|
|
27
|
+
transcript.requestId !== first.requestId)
|
|
28
|
+
throw new SpongeError('integrity');
|
|
25
29
|
const lower = i ? (windows[i - 1].endMs + window.startMs) / 2 : 0, upper = i + 1 < windows.length ? (window.endMs + windows[i + 1].startMs) / 2 : Infinity;
|
|
26
30
|
for (const segment of transcript.segments) {
|
|
27
31
|
const center = (segment.startMs + segment.endMs) / 2;
|
|
@@ -33,7 +37,7 @@ export function joinWindows(windows, transcripts) {
|
|
|
33
37
|
continue;
|
|
34
38
|
}
|
|
35
39
|
if (prior && segment.startMs < prior.startMs)
|
|
36
|
-
throw
|
|
40
|
+
throw new SpongeError('integrity');
|
|
37
41
|
segments.push({ ...segment });
|
|
38
42
|
}
|
|
39
43
|
}
|