videobook-engine 4.2.0 → 5.3.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/README.md +84 -39
- package/dist/artifacts.d.ts +5 -10
- package/dist/artifacts.d.ts.map +1 -1
- package/dist/artifacts.js +79 -142
- package/dist/artifacts.js.map +1 -1
- package/dist/books.d.ts +1 -1
- package/dist/books.d.ts.map +1 -1
- package/dist/books.js +16 -9
- package/dist/books.js.map +1 -1
- package/dist/cas.d.ts +2 -1
- package/dist/cas.d.ts.map +1 -1
- package/dist/cas.js +33 -4
- package/dist/cas.js.map +1 -1
- package/dist/catalog-gc-state.d.ts +4 -0
- package/dist/catalog-gc-state.d.ts.map +1 -0
- package/dist/catalog-gc-state.js +49 -0
- package/dist/catalog-gc-state.js.map +1 -0
- package/dist/catalog-metadata.d.ts +2 -2
- package/dist/catalog-metadata.js +3 -3
- package/dist/catalog-metadata.js.map +1 -1
- package/dist/context.d.ts +5 -5
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +17 -49
- package/dist/context.js.map +1 -1
- package/dist/domain.d.ts +35 -2
- package/dist/domain.d.ts.map +1 -1
- package/dist/domain.js +131 -161
- package/dist/domain.js.map +1 -1
- package/dist/edits.js +2 -2
- package/dist/edits.js.map +1 -1
- package/dist/engine-types.d.ts +159 -39
- package/dist/engine-types.d.ts.map +1 -1
- package/dist/engine-types.js +2 -1
- package/dist/engine-types.js.map +1 -1
- package/dist/engine.d.ts +30 -20
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +46 -4
- package/dist/engine.js.map +1 -1
- package/dist/files.d.ts +10 -0
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +49 -2
- package/dist/files.js.map +1 -1
- package/dist/fork.d.ts +14 -30
- package/dist/fork.d.ts.map +1 -1
- package/dist/fork.js +18 -27
- package/dist/fork.js.map +1 -1
- package/dist/generations.d.ts +9 -0
- package/dist/generations.d.ts.map +1 -0
- package/dist/generations.js +175 -0
- package/dist/generations.js.map +1 -0
- package/dist/history.d.ts +2 -1
- package/dist/history.d.ts.map +1 -1
- package/dist/history.js +30 -57
- package/dist/history.js.map +1 -1
- package/dist/index.d.ts +8 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/inline-temporal-models.d.ts +43 -0
- package/dist/inline-temporal-models.d.ts.map +1 -0
- package/dist/inline-temporal-models.js +236 -0
- package/dist/inline-temporal-models.js.map +1 -0
- package/dist/isolated-models.d.ts +8 -0
- package/dist/isolated-models.d.ts.map +1 -0
- package/dist/isolated-models.js +25 -0
- package/dist/isolated-models.js.map +1 -0
- package/dist/job-queue.d.ts +1 -1
- package/dist/job-queue.d.ts.map +1 -1
- package/dist/job-queue.js +20 -6
- package/dist/job-queue.js.map +1 -1
- package/dist/media-image.d.ts +8 -0
- package/dist/media-image.d.ts.map +1 -0
- package/dist/media-image.js +41 -0
- package/dist/media-image.js.map +1 -0
- package/dist/media-process.d.ts +13 -0
- package/dist/media-process.d.ts.map +1 -0
- package/dist/media-process.js +89 -0
- package/dist/media-process.js.map +1 -0
- package/dist/media.d.ts +3 -3
- package/dist/media.d.ts.map +1 -1
- package/dist/media.js +5 -5
- package/dist/media.js.map +1 -1
- package/dist/merge-policy.d.ts +16 -36
- package/dist/merge-policy.d.ts.map +1 -1
- package/dist/merge-policy.js +13 -125
- package/dist/merge-policy.js.map +1 -1
- package/dist/metadata.d.ts +1 -0
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +13 -3
- package/dist/metadata.js.map +1 -1
- package/dist/migrate-grid-text.d.ts +17 -0
- package/dist/migrate-grid-text.d.ts.map +1 -0
- package/dist/migrate-grid-text.js +117 -0
- package/dist/migrate-grid-text.js.map +1 -0
- package/dist/migrate-grid-v22.d.ts +12 -0
- package/dist/migrate-grid-v22.d.ts.map +1 -0
- package/dist/migrate-grid-v22.js +166 -0
- package/dist/migrate-grid-v22.js.map +1 -0
- package/dist/migrate-grid-v23.d.ts +10 -0
- package/dist/migrate-grid-v23.d.ts.map +1 -0
- package/dist/migrate-grid-v23.js +31 -0
- package/dist/migrate-grid-v23.js.map +1 -0
- package/dist/migration-notebooks.d.ts +25 -0
- package/dist/migration-notebooks.d.ts.map +1 -0
- package/dist/migration-notebooks.js +131 -0
- package/dist/migration-notebooks.js.map +1 -0
- package/dist/migration-source.d.ts +6 -0
- package/dist/migration-source.d.ts.map +1 -0
- package/dist/migration-source.js +37 -0
- package/dist/migration-source.js.map +1 -0
- package/dist/migration-timeline.d.ts +36 -0
- package/dist/migration-timeline.d.ts.map +1 -0
- package/dist/migration-timeline.js +187 -0
- package/dist/migration-timeline.js.map +1 -0
- package/dist/migration-validation.d.ts +12 -0
- package/dist/migration-validation.d.ts.map +1 -0
- package/dist/migration-validation.js +68 -0
- package/dist/migration-validation.js.map +1 -0
- package/dist/migration.d.ts.map +1 -1
- package/dist/migration.js +220 -119
- package/dist/migration.js.map +1 -1
- package/dist/model-cache-paths.d.ts +2 -0
- package/dist/model-cache-paths.d.ts.map +1 -0
- package/dist/model-cache-paths.js +5 -0
- package/dist/model-cache-paths.js.map +1 -0
- package/dist/model-checksums.json +338 -0
- package/dist/model-file-resolver.d.ts +30 -0
- package/dist/model-file-resolver.d.ts.map +1 -0
- package/dist/model-file-resolver.js +304 -0
- package/dist/model-file-resolver.js.map +1 -0
- package/dist/model-identity.d.ts +13 -0
- package/dist/model-identity.d.ts.map +1 -0
- package/dist/model-identity.js +44 -0
- package/dist/model-identity.js.map +1 -0
- package/dist/model-worker-errors.d.ts +3 -0
- package/dist/model-worker-errors.d.ts.map +1 -0
- package/dist/model-worker-errors.js +12 -0
- package/dist/model-worker-errors.js.map +1 -0
- package/dist/model-worker-pool.d.ts +32 -0
- package/dist/model-worker-pool.d.ts.map +1 -0
- package/dist/model-worker-pool.js +269 -0
- package/dist/model-worker-pool.js.map +1 -0
- package/dist/model-worker-protocol.d.ts +42 -0
- package/dist/model-worker-protocol.d.ts.map +1 -0
- package/dist/model-worker-protocol.js +2 -0
- package/dist/model-worker-protocol.js.map +1 -0
- package/dist/model-worker.d.ts +2 -0
- package/dist/model-worker.d.ts.map +1 -0
- package/dist/model-worker.js +91 -0
- package/dist/model-worker.js.map +1 -0
- package/dist/mvp-contract-fixtures.js +1 -1
- package/dist/mvp-contract-fixtures.js.map +1 -1
- package/dist/mvp-contracts.d.ts +32 -8
- package/dist/mvp-contracts.d.ts.map +1 -1
- package/dist/mvp-contracts.js +1 -1
- package/dist/mvp-contracts.js.map +1 -1
- package/dist/notebook/types.d.ts +1 -29
- package/dist/notebook/types.d.ts.map +1 -1
- package/dist/notebook-grid.d.ts +46 -0
- package/dist/notebook-grid.d.ts.map +1 -0
- package/dist/notebook-grid.js +187 -0
- package/dist/notebook-grid.js.map +1 -0
- package/dist/notebook-media-artifact.d.ts +23 -0
- package/dist/notebook-media-artifact.d.ts.map +1 -0
- package/dist/notebook-media-artifact.js +67 -0
- package/dist/notebook-media-artifact.js.map +1 -0
- package/dist/notebook-mentions.d.ts +20 -0
- package/dist/notebook-mentions.d.ts.map +1 -0
- package/dist/notebook-mentions.js +54 -0
- package/dist/notebook-mentions.js.map +1 -0
- package/dist/runtime-services.js +4 -9
- package/dist/runtime-services.js.map +1 -1
- package/dist/schema.d.ts +4 -19
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +66 -92
- package/dist/schema.js.map +1 -1
- package/dist/search-provider-access.d.ts +11 -0
- package/dist/search-provider-access.d.ts.map +1 -0
- package/dist/search-provider-access.js +42 -0
- package/dist/search-provider-access.js.map +1 -0
- package/dist/similarity.d.ts +60 -1
- package/dist/similarity.d.ts.map +1 -1
- package/dist/similarity.js +174 -154
- package/dist/similarity.js.map +1 -1
- package/dist/status.js +8 -10
- package/dist/status.js.map +1 -1
- package/dist/store.d.ts +16 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +171 -17
- package/dist/store.js.map +1 -1
- package/dist/temporal-index-cache.d.ts +13 -0
- package/dist/temporal-index-cache.d.ts.map +1 -0
- package/dist/temporal-index-cache.js +59 -0
- package/dist/temporal-index-cache.js.map +1 -0
- package/dist/temporal-model-manifests.d.ts +17 -0
- package/dist/temporal-model-manifests.d.ts.map +1 -0
- package/dist/temporal-model-manifests.js +33 -0
- package/dist/temporal-model-manifests.js.map +1 -0
- package/dist/temporal-models.d.ts +20 -35
- package/dist/temporal-models.d.ts.map +1 -1
- package/dist/temporal-models.js +23 -257
- package/dist/temporal-models.js.map +1 -1
- package/dist/temporal-search.d.ts +6 -3
- package/dist/temporal-search.d.ts.map +1 -1
- package/dist/temporal-search.js +438 -122
- package/dist/temporal-search.js.map +1 -1
- package/dist/temporal-vector-index.d.ts +46 -0
- package/dist/temporal-vector-index.d.ts.map +1 -0
- package/dist/temporal-vector-index.js +220 -0
- package/dist/temporal-vector-index.js.map +1 -0
- package/dist/third-party/@huggingface-jinja.LICENSE +21 -0
- package/dist/third-party/@huggingface-tokenizers.LICENSE +201 -0
- package/dist/third-party/@huggingface-transformers.LICENSE +202 -0
- package/dist/third-party/onnxruntime-common.LICENSE +21 -0
- package/dist/third-party/versions.json +23 -0
- package/dist/transformers-runtime.d.ts +2 -0
- package/dist/transformers-runtime.d.ts.map +1 -0
- package/dist/transformers-runtime.js +33014 -0
- package/dist/transformers-runtime.js.LEGAL.txt +12 -0
- package/dist/transformers-runtime.js.map +7 -0
- package/docs/doltlite-staging.md +69 -0
- package/docs/edit-performance.md +83 -0
- package/docs/engine-layout.md +56 -50
- package/docs/media-limits.md +57 -0
- package/docs/model-download-policy.md +15 -0
- package/docs/model-integrity.md +72 -0
- package/docs/model-isolation.md +72 -0
- package/docs/model-revisions.md +54 -0
- package/docs/mvp-prd.md +2 -2
- package/docs/release-evidence.md +300 -0
- package/docs/search-provider-consent.md +67 -0
- package/docs/semantic-durability.md +55 -0
- package/docs/temporal-search-performance.md +260 -0
- package/docs/v4-migration.md +83 -0
- package/fixtures/v5/contract-fixtures.json +4 -4
- package/package.json +31 -13
- package/dist/artifact-names.d.ts +0 -6
- package/dist/artifact-names.d.ts.map +0 -1
- package/dist/artifact-names.js +0 -56
- package/dist/artifact-names.js.map +0 -1
- package/dist/resolver.d.ts +0 -15
- package/dist/resolver.d.ts.map +0 -1
- package/dist/resolver.js +0 -81
- package/dist/resolver.js.map +0 -1
package/dist/similarity.js
CHANGED
|
@@ -1,28 +1,26 @@
|
|
|
1
|
+
import { modelIdentity } from "./model-identity.js";
|
|
2
|
+
import { guardSearchProvider } from "./search-provider-access.js";
|
|
1
3
|
import { createHash } from "node:crypto";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
2
5
|
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
6
|
import { mkdtemp, mkdir, readdir, rm } from "node:fs/promises";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
7
|
import * as path from "node:path";
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import { Index, MetricKind, ScalarKind, } from "usearch";
|
|
8
|
+
import { decodeModelImage } from "./media-image.js";
|
|
9
|
+
import { runMediaProcess, checkMediaCancellation } from "./media-process.js";
|
|
10
10
|
import { err, } from "./engine-types.js";
|
|
11
11
|
import { resultOf, syncResultOf, } from "./context.js";
|
|
12
12
|
import { EngineFault } from "./store.js";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
import { isolatedModelCall } from "./isolated-models.js";
|
|
14
|
+
function providerOperationOptions(options) {
|
|
15
|
+
return {
|
|
16
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
17
|
+
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
16
20
|
const DEFAULT_DIMENSIONS = 512;
|
|
17
|
-
const DEFAULT_AUDIO_MODEL_ID = "Xenova/clap-htsat-unfused";
|
|
18
|
-
const DEFAULT_AUDIO_MODEL_REVISION = "c28f2883575e590e04d3146ff0713c2448d691ba";
|
|
19
|
-
const DEFAULT_AUDIO_EMBEDDING_SPACE = "clap-htsat-unfused-q8-c28f2883575e590e04d3146ff0713c2448d691ba-audio-v1";
|
|
20
21
|
const DEFAULT_AUDIO_DIMENSIONS = 512;
|
|
21
22
|
const DEFAULT_AUDIO_SAMPLE_RATE = 48_000;
|
|
22
23
|
const DEFAULT_AUDIO_MAX_SAMPLES = 480_000;
|
|
23
|
-
const DEFAULT_TEXT_MODEL_ID = "onnx-community/all-MiniLM-L6-v2-ONNX";
|
|
24
|
-
const DEFAULT_TEXT_MODEL_REVISION = "aff7a1dc4e8a1ea593e6ea21e95c22ef0a25966f";
|
|
25
|
-
const DEFAULT_TEXT_EMBEDDING_SPACE = "all-minilm-l6-v2-q4-aff7a1dc4e8a1ea593e6ea21e95c22ef0a25966f-text-v1";
|
|
26
24
|
const DEFAULT_TEXT_DIMENSIONS = 384;
|
|
27
25
|
const DEFAULT_TEXT_MAX_BYTES = 1024 * 1024;
|
|
28
26
|
const DEFAULT_TEXT_MAX_CHUNKS = 256;
|
|
@@ -37,6 +35,16 @@ const TEXT_ARTIFACT_KINDS = new Set([
|
|
|
37
35
|
"scene",
|
|
38
36
|
"final",
|
|
39
37
|
]);
|
|
38
|
+
let transformersModule;
|
|
39
|
+
let usearchModule;
|
|
40
|
+
function loadTransformers() {
|
|
41
|
+
transformersModule ??= import("./transformers-runtime.js");
|
|
42
|
+
return transformersModule;
|
|
43
|
+
}
|
|
44
|
+
function loadUsearch() {
|
|
45
|
+
usearchModule ??= createRequire(import.meta.url)("usearch");
|
|
46
|
+
return usearchModule;
|
|
47
|
+
}
|
|
40
48
|
export function createSimilarityApi(context) {
|
|
41
49
|
if (!context.config.similarity)
|
|
42
50
|
return disabledSimilarityApi();
|
|
@@ -50,17 +58,26 @@ class LocalSimilarityApi {
|
|
|
50
58
|
indexes = new Map();
|
|
51
59
|
constructor(context, config) {
|
|
52
60
|
this.context = context;
|
|
53
|
-
this.provider = config.provider
|
|
61
|
+
this.provider = config.provider
|
|
62
|
+
? guardSearchProvider(config.provider, config.providerConsent).provider
|
|
63
|
+
: isolatedCompatibilityProvider("compat-clip", context, config);
|
|
54
64
|
this.audioProvider = config.audio
|
|
55
|
-
? config.audio.provider
|
|
56
|
-
|
|
65
|
+
? config.audio.provider
|
|
66
|
+
? guardSearchProvider(config.audio.provider, config.audio.providerConsent).provider
|
|
67
|
+
: isolatedCompatibilityProvider("compat-clap", context, config)
|
|
57
68
|
: null;
|
|
58
69
|
this.textProvider = config.text
|
|
59
|
-
? config.text.provider
|
|
70
|
+
? config.text.provider
|
|
71
|
+
? guardSearchProvider(config.text.provider, config.text.providerConsent).provider
|
|
72
|
+
: isolatedCompatibilityProvider("compat-text", context, config)
|
|
60
73
|
: null;
|
|
61
74
|
}
|
|
75
|
+
localResult(operation) {
|
|
76
|
+
return this.context.withLocalMedia(() => resultOf(operation));
|
|
77
|
+
}
|
|
62
78
|
async prepare(options = {}) {
|
|
63
|
-
return
|
|
79
|
+
return this.localResult(async () => {
|
|
80
|
+
checkMediaCancellation(options);
|
|
64
81
|
const kinds = options.kind
|
|
65
82
|
? [options.kind]
|
|
66
83
|
: [
|
|
@@ -74,13 +91,13 @@ class LocalSimilarityApi {
|
|
|
74
91
|
for (const kind of kinds) {
|
|
75
92
|
if (kind === "text") {
|
|
76
93
|
const provider = this.requireTextProvider();
|
|
77
|
-
await provider.prepare();
|
|
94
|
+
await provider.prepare(providerOperationOptions(options));
|
|
78
95
|
spaces.text = provider.embeddingSpace;
|
|
79
96
|
}
|
|
80
97
|
else {
|
|
81
98
|
const provider = this.providerFor(kind);
|
|
82
99
|
if (!preparedMedia.has(provider)) {
|
|
83
|
-
await provider.prepare();
|
|
100
|
+
await provider.prepare(providerOperationOptions(options));
|
|
84
101
|
preparedMedia.add(provider);
|
|
85
102
|
}
|
|
86
103
|
spaces[kind] = provider.embeddingSpace;
|
|
@@ -93,7 +110,8 @@ class LocalSimilarityApi {
|
|
|
93
110
|
});
|
|
94
111
|
}
|
|
95
112
|
async index(artifactReference, options = {}) {
|
|
96
|
-
return
|
|
113
|
+
return this.localResult(async () => {
|
|
114
|
+
checkMediaCancellation(options);
|
|
97
115
|
const artifact = this.context.artifactRow(artifactReference);
|
|
98
116
|
const kind = similarityKind(artifact);
|
|
99
117
|
if (kind === "text") {
|
|
@@ -103,7 +121,8 @@ class LocalSimilarityApi {
|
|
|
103
121
|
});
|
|
104
122
|
}
|
|
105
123
|
async rebuild(options = {}) {
|
|
106
|
-
return
|
|
124
|
+
return this.localResult(async () => {
|
|
125
|
+
checkMediaCancellation(options);
|
|
107
126
|
const allowedKinds = options.kind
|
|
108
127
|
? [options.kind]
|
|
109
128
|
: [
|
|
@@ -114,7 +133,7 @@ class LocalSimilarityApi {
|
|
|
114
133
|
];
|
|
115
134
|
const artifactKinds = allowedKinds.flatMap((kind) => kind === "text" ? [...TEXT_ARTIFACT_KINDS] : [kind]);
|
|
116
135
|
const artifactRows = this.context.store.db
|
|
117
|
-
.prepare(`SELECT artifact_id,
|
|
136
|
+
.prepare(`SELECT artifact_id, label, kind, created_at
|
|
118
137
|
FROM artifacts
|
|
119
138
|
WHERE kind IN (${artifactKinds.map(() => "?").join(", ")})
|
|
120
139
|
ORDER BY created_at, artifact_id`)
|
|
@@ -126,9 +145,7 @@ class LocalSimilarityApi {
|
|
|
126
145
|
continue;
|
|
127
146
|
if (kind === "text" && !this.hasTextSource(artifact.artifact_id))
|
|
128
147
|
continue;
|
|
129
|
-
const result = await this.index(artifact.artifact_id,
|
|
130
|
-
force: options.force,
|
|
131
|
-
});
|
|
148
|
+
const result = await this.index(artifact.artifact_id, options);
|
|
132
149
|
if (!result.ok)
|
|
133
150
|
throw new EngineFault(result.error);
|
|
134
151
|
indexed.push(result.value);
|
|
@@ -227,7 +244,8 @@ class LocalSimilarityApi {
|
|
|
227
244
|
});
|
|
228
245
|
}
|
|
229
246
|
async findSimilar(artifactReference, options = {}) {
|
|
230
|
-
return
|
|
247
|
+
return this.localResult(async () => {
|
|
248
|
+
checkMediaCancellation(options);
|
|
231
249
|
const artifact = this.context.artifactRow(artifactReference);
|
|
232
250
|
const kind = similarityKind(artifact);
|
|
233
251
|
if (kind === "text") {
|
|
@@ -237,8 +255,9 @@ class LocalSimilarityApi {
|
|
|
237
255
|
});
|
|
238
256
|
}
|
|
239
257
|
async findSimilarText(query, options = {}) {
|
|
240
|
-
return
|
|
258
|
+
return this.localResult(async () => {
|
|
241
259
|
const provider = this.requireTextProvider();
|
|
260
|
+
checkMediaCancellation(options);
|
|
242
261
|
const normalizedQuery = normalizePlainText(query);
|
|
243
262
|
if (normalizedQuery.text.length === 0) {
|
|
244
263
|
throw new EngineFault({
|
|
@@ -253,7 +272,7 @@ class LocalSimilarityApi {
|
|
|
253
272
|
message: `Text similarity queries cannot exceed ${maxBytes} bytes`,
|
|
254
273
|
});
|
|
255
274
|
}
|
|
256
|
-
const chunks = await this.embedText(normalizedQuery.text);
|
|
275
|
+
const chunks = await this.embedText(normalizedQuery.text, options);
|
|
257
276
|
return this.searchText(null, null, normalizedQuery.contentHash, chunks, options, provider);
|
|
258
277
|
});
|
|
259
278
|
}
|
|
@@ -277,7 +296,8 @@ class LocalSimilarityApi {
|
|
|
277
296
|
frameCount: reusable.frame_count,
|
|
278
297
|
reused: true,
|
|
279
298
|
}
|
|
280
|
-
: await this.embedMediaSource(source);
|
|
299
|
+
: await this.embedMediaSource(source, options);
|
|
300
|
+
checkMediaCancellation(options);
|
|
281
301
|
const row = this.context.store.runtime((now) => {
|
|
282
302
|
if (existing) {
|
|
283
303
|
this.context.store.db
|
|
@@ -320,7 +340,7 @@ class LocalSimilarityApi {
|
|
|
320
340
|
endOffset: chunk.end_offset,
|
|
321
341
|
vector: vectorFromBlob(chunk),
|
|
322
342
|
}))
|
|
323
|
-
: await this.embedText(normalizedSource.text);
|
|
343
|
+
: await this.embedText(normalizedSource.text, options);
|
|
324
344
|
const maxChunks = checkedTextMaxChunks(this.context.config.similarity?.text);
|
|
325
345
|
if (embeddedChunks.length > maxChunks) {
|
|
326
346
|
throw new EngineFault({
|
|
@@ -330,6 +350,7 @@ class LocalSimilarityApi {
|
|
|
330
350
|
}
|
|
331
351
|
this.validateTextChunks(normalizedSource.text, embeddedChunks, provider.dimensions);
|
|
332
352
|
this.indexes.delete(indexKey("text", provider.embeddingSpace));
|
|
353
|
+
checkMediaCancellation(options);
|
|
333
354
|
const row = this.context.store.runtime((now) => {
|
|
334
355
|
let documentId;
|
|
335
356
|
if (existing) {
|
|
@@ -370,7 +391,7 @@ class LocalSimilarityApi {
|
|
|
370
391
|
if (!query) {
|
|
371
392
|
throw new EngineFault({
|
|
372
393
|
code: "NOT_READY",
|
|
373
|
-
message: `Artifact is not indexed for similarity: ${artifact.
|
|
394
|
+
message: `Artifact is not indexed for similarity: ${artifact.artifact_id}`,
|
|
374
395
|
details: { artifactId: artifact.artifact_id },
|
|
375
396
|
});
|
|
376
397
|
}
|
|
@@ -389,7 +410,7 @@ class LocalSimilarityApi {
|
|
|
389
410
|
if (!query) {
|
|
390
411
|
throw new EngineFault({
|
|
391
412
|
code: "NOT_READY",
|
|
392
|
-
message: `Artifact is not indexed for similarity: ${artifact.
|
|
413
|
+
message: `Artifact is not indexed for similarity: ${artifact.artifact_id}`,
|
|
393
414
|
details: { artifactId: artifact.artifact_id },
|
|
394
415
|
});
|
|
395
416
|
}
|
|
@@ -416,7 +437,7 @@ class LocalSimilarityApi {
|
|
|
416
437
|
: cosine(queryVector, vectorFromBlob(candidate));
|
|
417
438
|
return {
|
|
418
439
|
artifactId: candidate.artifact_id,
|
|
419
|
-
|
|
440
|
+
...(candidate.label ? { label: candidate.label } : {}),
|
|
420
441
|
kind: candidate.kind,
|
|
421
442
|
score: global,
|
|
422
443
|
exactBytes,
|
|
@@ -513,7 +534,7 @@ class LocalSimilarityApi {
|
|
|
513
534
|
const queryChunk = queryChunks[match.queryChunkIndex];
|
|
514
535
|
return {
|
|
515
536
|
artifactId: match.chunk.artifact_id,
|
|
516
|
-
|
|
537
|
+
...(match.chunk.label ? { label: match.chunk.label } : {}),
|
|
517
538
|
kind: "text",
|
|
518
539
|
score: match.score,
|
|
519
540
|
exactBytes: match.exactBytes,
|
|
@@ -540,36 +561,37 @@ class LocalSimilarityApi {
|
|
|
540
561
|
left.artifactId.localeCompare(right.artifactId))
|
|
541
562
|
.slice(0, limit);
|
|
542
563
|
}
|
|
543
|
-
async embedMediaSource(source) {
|
|
564
|
+
async embedMediaSource(source, options) {
|
|
544
565
|
const localPath = await this.context.objects.ensureLocalPath(source.object_hash);
|
|
545
566
|
if (source.kind === "audio") {
|
|
546
567
|
const provider = this.requireAudioProvider();
|
|
547
|
-
await provider.prepare();
|
|
568
|
+
await provider.prepare(providerOperationOptions(options));
|
|
548
569
|
return {
|
|
549
|
-
vector: normalized(await provider.embedAudio(localPath)),
|
|
570
|
+
vector: normalized(await provider.embedAudio(localPath, providerOperationOptions(options))),
|
|
550
571
|
frameCount: null,
|
|
551
572
|
reused: false,
|
|
552
573
|
};
|
|
553
574
|
}
|
|
554
|
-
await this.provider.prepare();
|
|
575
|
+
await this.provider.prepare(providerOperationOptions(options));
|
|
555
576
|
if (source.kind === "image") {
|
|
556
577
|
return {
|
|
557
|
-
vector: normalized(await this.provider.embedImage(localPath)),
|
|
578
|
+
vector: normalized(await this.provider.embedImage(localPath, providerOperationOptions(options))),
|
|
558
579
|
frameCount: null,
|
|
559
580
|
reused: false,
|
|
560
581
|
};
|
|
561
582
|
}
|
|
562
|
-
const video = await this.provider.embedVideo(localPath);
|
|
583
|
+
const video = await this.provider.embedVideo(localPath, providerOperationOptions(options));
|
|
563
584
|
return {
|
|
564
585
|
vector: normalized(video.vector),
|
|
565
586
|
frameCount: video.frameCount,
|
|
566
587
|
reused: false,
|
|
567
588
|
};
|
|
568
589
|
}
|
|
569
|
-
async embedText(text) {
|
|
590
|
+
async embedText(text, options) {
|
|
570
591
|
const provider = this.requireTextProvider();
|
|
571
|
-
await provider.prepare();
|
|
572
|
-
const chunks = await provider.embedText(text);
|
|
592
|
+
await provider.prepare(providerOperationOptions(options));
|
|
593
|
+
const chunks = await provider.embedText(text, providerOperationOptions(options));
|
|
594
|
+
checkMediaCancellation(options);
|
|
573
595
|
this.validateTextChunks(text, chunks, provider.dimensions);
|
|
574
596
|
const maxChunks = checkedTextMaxChunks(this.context.config.similarity?.text);
|
|
575
597
|
if (chunks.length > maxChunks) {
|
|
@@ -665,13 +687,13 @@ class LocalSimilarityApi {
|
|
|
665
687
|
if (sources.length === 0) {
|
|
666
688
|
throw new EngineFault({
|
|
667
689
|
code: "INVALID_INPUT",
|
|
668
|
-
message: `No supported original ${kind} file for ${artifact.
|
|
690
|
+
message: `No supported original ${kind} file for ${artifact.artifact_id}`,
|
|
669
691
|
});
|
|
670
692
|
}
|
|
671
693
|
if (kind === "text" && sources.length > 1) {
|
|
672
694
|
throw new EngineFault({
|
|
673
695
|
code: "INVALID_INPUT",
|
|
674
|
-
message: `Multiple supported original ${kind} files for ${artifact.
|
|
696
|
+
message: `Multiple supported original ${kind} files for ${artifact.artifact_id}`,
|
|
675
697
|
details: { paths: sources.map((source) => source.path) },
|
|
676
698
|
});
|
|
677
699
|
}
|
|
@@ -727,7 +749,7 @@ class LocalSimilarityApi {
|
|
|
727
749
|
return this.context.store.db
|
|
728
750
|
.prepare(`SELECT e.id, e.artifact_id, e.kind, e.source_path,
|
|
729
751
|
e.object_hash, e.embedding_space, e.dimensions, e.vector_blob,
|
|
730
|
-
e.frame_count, e.updated_at, a.
|
|
752
|
+
e.frame_count, e.updated_at, a.label AS label
|
|
731
753
|
FROM runtime_similarity_embeddings e
|
|
732
754
|
JOIN artifacts a ON a.artifact_id=e.artifact_id
|
|
733
755
|
WHERE e.kind=? AND e.embedding_space=?
|
|
@@ -749,6 +771,7 @@ class LocalSimilarityApi {
|
|
|
749
771
|
const current = this.indexes.get(key);
|
|
750
772
|
if (current)
|
|
751
773
|
return current;
|
|
774
|
+
const { Index, MetricKind, ScalarKind } = loadUsearch();
|
|
752
775
|
const index = new Index({
|
|
753
776
|
dimensions,
|
|
754
777
|
metric: MetricKind.Cos,
|
|
@@ -781,6 +804,7 @@ class LocalSimilarityApi {
|
|
|
781
804
|
const current = this.indexes.get(key);
|
|
782
805
|
if (current)
|
|
783
806
|
return current;
|
|
807
|
+
const { Index, MetricKind, ScalarKind } = loadUsearch();
|
|
784
808
|
const index = new Index({
|
|
785
809
|
dimensions,
|
|
786
810
|
metric: MetricKind.Cos,
|
|
@@ -872,7 +896,7 @@ class LocalSimilarityApi {
|
|
|
872
896
|
c.embedding_space, c.chunk_index, c.start_offset,
|
|
873
897
|
c.end_offset, c.chunk_text, c.dimensions, c.vector_blob,
|
|
874
898
|
c.updated_at, d.source_path, d.object_hash, d.content_hash,
|
|
875
|
-
a.
|
|
899
|
+
a.label AS label
|
|
876
900
|
FROM runtime_text_similarity_chunks c
|
|
877
901
|
JOIN runtime_text_similarity_documents d ON d.id=c.document_id
|
|
878
902
|
JOIN artifacts a ON a.artifact_id=c.artifact_id
|
|
@@ -921,36 +945,68 @@ class LocalSimilarityApi {
|
|
|
921
945
|
return this.textProvider;
|
|
922
946
|
}
|
|
923
947
|
}
|
|
948
|
+
function isolatedCompatibilityProvider(kind, context, config) {
|
|
949
|
+
const local = kind === "compat-clap" ? config.audio : kind === "compat-text" ? config.text : config;
|
|
950
|
+
const modelId = local.modelId?.startsWith(".") ? path.resolve(local.modelId) : local.modelId;
|
|
951
|
+
const modelCacheDir = local.modelCacheDir ?? config.modelCacheDir ?? path.join(context.config.dataDir, "similarity-models");
|
|
952
|
+
const configuration = { kind, modelCacheDir, modelId, modelRevision: local.modelRevision, allowModelDownload: local.allowModelDownload ?? config.allowModelDownload,
|
|
953
|
+
ffmpegPath: kind === "compat-clap" ? config.audio.ffmpegPath ?? config.ffmpegPath : config.ffmpegPath,
|
|
954
|
+
ffprobePath: config.ffprobePath };
|
|
955
|
+
return {
|
|
956
|
+
networkAccess: Object.freeze({ modelDownloads: configuration.allowModelDownload === true, inference: false }),
|
|
957
|
+
get embeddingSpace() { return modelIdentity(kind, configuration).embeddingSpace; },
|
|
958
|
+
dimensions: kind === "compat-text" ? DEFAULT_TEXT_DIMENSIONS : DEFAULT_DIMENSIONS,
|
|
959
|
+
async prepare(options = {}) { await isolatedModelCall(configuration, { method: "prepare" }, options); },
|
|
960
|
+
embedImage(sourcePath, options = {}) { return isolatedModelCall(configuration, { method: "embedImage", sourcePath }, options); },
|
|
961
|
+
embedAudio(sourcePath, options = {}) { return isolatedModelCall(configuration, { method: "embedAudio", sourcePath }, options); },
|
|
962
|
+
embedVideo(sourcePath, options = {}) { return isolatedModelCall(configuration, { method: "embedVideo", sourcePath }, options); },
|
|
963
|
+
embedText(text, options = {}) { return isolatedModelCall(configuration, { method: "embedText", text }, options); },
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
export function createInlineSimilarityProvider(configuration) {
|
|
967
|
+
const common = { modelCacheDir: configuration.modelCacheDir, allowModelDownload: configuration.allowModelDownload,
|
|
968
|
+
ffmpegPath: configuration.ffmpegPath, ffprobePath: configuration.ffprobePath };
|
|
969
|
+
const model = { ...common, modelId: configuration.modelId, modelRevision: configuration.modelRevision };
|
|
970
|
+
if (configuration.kind === "compat-clip")
|
|
971
|
+
return new LocalClipProvider(process.cwd(), model);
|
|
972
|
+
if (configuration.kind === "compat-clap")
|
|
973
|
+
return new LocalClapAudioProvider(process.cwd(), common, model);
|
|
974
|
+
return new LocalTextProvider(process.cwd(), common, model);
|
|
975
|
+
}
|
|
924
976
|
class LocalClipProvider {
|
|
925
|
-
|
|
977
|
+
dataDir;
|
|
926
978
|
config;
|
|
927
|
-
|
|
979
|
+
get networkAccess() { return { modelDownloads: this.config.allowModelDownload === true, inference: false }; }
|
|
980
|
+
get embeddingSpace() { return modelIdentity("compat-clip", this.config).embeddingSpace; }
|
|
928
981
|
dimensions = DEFAULT_DIMENSIONS;
|
|
929
982
|
embedder = null;
|
|
930
|
-
constructor(
|
|
931
|
-
this.
|
|
983
|
+
constructor(dataDir, config) {
|
|
984
|
+
this.dataDir = dataDir;
|
|
932
985
|
this.config = config;
|
|
933
986
|
}
|
|
934
|
-
async prepare() {
|
|
987
|
+
async prepare(options = {}) {
|
|
988
|
+
checkMediaCancellation(options);
|
|
935
989
|
await this.loadEmbedder();
|
|
990
|
+
checkMediaCancellation(options);
|
|
936
991
|
}
|
|
937
|
-
async embedImage(sourcePath) {
|
|
938
|
-
const vectors = await this.embedImages([sourcePath]);
|
|
992
|
+
async embedImage(sourcePath, options = {}) {
|
|
993
|
+
const vectors = await this.embedImages([sourcePath], options);
|
|
939
994
|
const vector = vectors[0];
|
|
940
995
|
if (!vector)
|
|
941
996
|
throw new Error("Image embedder returned no vector");
|
|
942
997
|
return vector;
|
|
943
998
|
}
|
|
944
|
-
async embedVideo(sourcePath) {
|
|
945
|
-
const duration = await probeDuration(this.config.ffprobePath ?? "ffprobe", sourcePath);
|
|
999
|
+
async embedVideo(sourcePath, options = {}) {
|
|
1000
|
+
const duration = await probeDuration(this.config.ffprobePath ?? "ffprobe", sourcePath, options);
|
|
946
1001
|
const frameCount = Math.min(120, Math.max(1, Math.ceil(duration / 2)));
|
|
947
|
-
const framesDir = await mkdtemp(path.join(
|
|
1002
|
+
const framesDir = await mkdtemp(path.join(process.cwd(), "videobook-sim-"));
|
|
948
1003
|
try {
|
|
949
1004
|
const fps = `${frameCount}/${Math.max(duration, 0.001)}`;
|
|
950
1005
|
await runCommand(this.config.ffmpegPath ?? "ffmpeg", [
|
|
951
1006
|
"-hide_banner",
|
|
952
1007
|
"-loglevel",
|
|
953
1008
|
"error",
|
|
1009
|
+
"-nostdin", "-protocol_whitelist", "file,pipe",
|
|
954
1010
|
"-i",
|
|
955
1011
|
sourcePath,
|
|
956
1012
|
"-an",
|
|
@@ -959,7 +1015,7 @@ class LocalClipProvider {
|
|
|
959
1015
|
"-frames:v",
|
|
960
1016
|
String(frameCount),
|
|
961
1017
|
path.join(framesDir, "frame-%04d.png"),
|
|
962
|
-
]);
|
|
1018
|
+
], options);
|
|
963
1019
|
const frames = (await readdir(framesDir))
|
|
964
1020
|
.filter((name) => name.endsWith(".png"))
|
|
965
1021
|
.sort()
|
|
@@ -972,7 +1028,7 @@ class LocalClipProvider {
|
|
|
972
1028
|
}
|
|
973
1029
|
const vectors = [];
|
|
974
1030
|
for (let offset = 0; offset < frames.length; offset += 8) {
|
|
975
|
-
vectors.push(...(await this.embedImages(frames.slice(offset, offset + 8))));
|
|
1031
|
+
vectors.push(...(await this.embedImages(frames.slice(offset, offset + 8), options)));
|
|
976
1032
|
}
|
|
977
1033
|
return { vector: normalizedCentroid(vectors), frameCount: vectors.length };
|
|
978
1034
|
}
|
|
@@ -983,34 +1039,35 @@ class LocalClipProvider {
|
|
|
983
1039
|
async loadEmbedder() {
|
|
984
1040
|
if (this.embedder)
|
|
985
1041
|
return this.embedder;
|
|
986
|
-
const dataDir = this.
|
|
1042
|
+
const dataDir = this.dataDir;
|
|
987
1043
|
if (!dataDir)
|
|
988
1044
|
throw new Error("Similarity requires a configured dataDir");
|
|
989
1045
|
const cacheDir = this.config.modelCacheDir ?? path.join(dataDir, "similarity-models");
|
|
990
1046
|
await mkdir(cacheDir, { recursive: true });
|
|
991
|
-
const modelId = this.config
|
|
1047
|
+
const { modelId, modelRevision } = modelIdentity("compat-clip", this.config);
|
|
992
1048
|
try {
|
|
1049
|
+
const { pipeline } = await loadTransformers();
|
|
993
1050
|
this.embedder = await pipeline("image-feature-extraction", modelId, {
|
|
994
1051
|
dtype: "q8",
|
|
995
1052
|
cache_dir: cacheDir,
|
|
996
|
-
local_files_only: this.config.allowModelDownload
|
|
997
|
-
...(
|
|
998
|
-
? { revision: DEFAULT_MODEL_REVISION }
|
|
999
|
-
: {}),
|
|
1053
|
+
local_files_only: this.config.allowModelDownload !== true,
|
|
1054
|
+
...(modelRevision ? { revision: modelRevision } : {}),
|
|
1000
1055
|
});
|
|
1001
1056
|
return this.embedder;
|
|
1002
1057
|
}
|
|
1003
1058
|
catch (error) {
|
|
1004
1059
|
throw new EngineFault({
|
|
1005
|
-
code: this.config.allowModelDownload
|
|
1060
|
+
code: this.config.allowModelDownload !== true ? "OFFLINE" : "FEATURE_UNAVAILABLE",
|
|
1006
1061
|
message: `Unable to load local similarity model: ${errorMessage(error)}`,
|
|
1007
1062
|
});
|
|
1008
1063
|
}
|
|
1009
1064
|
}
|
|
1010
|
-
async embedImages(sourcePaths) {
|
|
1065
|
+
async embedImages(sourcePaths, options) {
|
|
1011
1066
|
const embedder = await this.loadEmbedder();
|
|
1012
|
-
|
|
1067
|
+
checkMediaCancellation(options);
|
|
1068
|
+
const images = await Promise.all(sourcePaths.map((source) => readNormalizedImage(source, options)));
|
|
1013
1069
|
const output = await embedder(images);
|
|
1070
|
+
checkMediaCancellation(options);
|
|
1014
1071
|
const batch = output.dims[0];
|
|
1015
1072
|
const dimensions = output.dims[1];
|
|
1016
1073
|
if (batch !== sourcePaths.length || dimensions !== this.dimensions) {
|
|
@@ -1025,32 +1082,33 @@ class LocalClipProvider {
|
|
|
1025
1082
|
}
|
|
1026
1083
|
}
|
|
1027
1084
|
class LocalClapAudioProvider {
|
|
1028
|
-
|
|
1085
|
+
dataDir;
|
|
1029
1086
|
sharedConfig;
|
|
1030
1087
|
config;
|
|
1031
|
-
|
|
1088
|
+
get networkAccess() { return { modelDownloads: (this.config.allowModelDownload ?? this.sharedConfig.allowModelDownload) === true, inference: false }; }
|
|
1089
|
+
get embeddingSpace() { return modelIdentity("compat-clap", this.config).embeddingSpace; }
|
|
1032
1090
|
dimensions = DEFAULT_AUDIO_DIMENSIONS;
|
|
1033
1091
|
processor = null;
|
|
1034
1092
|
model = null;
|
|
1035
1093
|
sampleRate = DEFAULT_AUDIO_SAMPLE_RATE;
|
|
1036
1094
|
maxSamples = DEFAULT_AUDIO_MAX_SAMPLES;
|
|
1037
|
-
constructor(
|
|
1038
|
-
this.
|
|
1095
|
+
constructor(dataDir, sharedConfig, config) {
|
|
1096
|
+
this.dataDir = dataDir;
|
|
1039
1097
|
this.sharedConfig = sharedConfig;
|
|
1040
1098
|
this.config = config;
|
|
1041
|
-
this.embeddingSpace =
|
|
1042
|
-
config.modelId && config.modelId !== DEFAULT_AUDIO_MODEL_ID
|
|
1043
|
-
? `audio-${config.modelId.replace(/[^a-zA-Z0-9]+/g, "-")}-v1`
|
|
1044
|
-
: DEFAULT_AUDIO_EMBEDDING_SPACE;
|
|
1045
1099
|
}
|
|
1046
|
-
async prepare() {
|
|
1100
|
+
async prepare(options = {}) {
|
|
1101
|
+
checkMediaCancellation(options);
|
|
1047
1102
|
await this.loadModel();
|
|
1103
|
+
checkMediaCancellation(options);
|
|
1048
1104
|
}
|
|
1049
|
-
async embedAudio(sourcePath) {
|
|
1105
|
+
async embedAudio(sourcePath, options = {}) {
|
|
1106
|
+
checkMediaCancellation(options);
|
|
1050
1107
|
const { processor, model } = await this.loadModel();
|
|
1051
|
-
const audio = await decodeAudioToMono(this.config.ffmpegPath ?? this.sharedConfig.ffmpegPath ?? "ffmpeg", sourcePath, this.sampleRate, this.maxSamples);
|
|
1108
|
+
const audio = await decodeAudioToMono(this.config.ffmpegPath ?? this.sharedConfig.ffmpegPath ?? "ffmpeg", sourcePath, this.sampleRate, this.maxSamples, options);
|
|
1052
1109
|
const inputs = await processor(audio);
|
|
1053
1110
|
const output = await model(inputs);
|
|
1111
|
+
checkMediaCancellation(options);
|
|
1054
1112
|
const dimensions = output.audio_embeds.dims;
|
|
1055
1113
|
if (dimensions.length !== 2 ||
|
|
1056
1114
|
dimensions[0] !== 1 ||
|
|
@@ -1064,29 +1122,28 @@ class LocalClapAudioProvider {
|
|
|
1064
1122
|
if (this.processor && this.model) {
|
|
1065
1123
|
return { processor: this.processor, model: this.model };
|
|
1066
1124
|
}
|
|
1067
|
-
const dataDir = this.
|
|
1125
|
+
const dataDir = this.dataDir;
|
|
1068
1126
|
if (!dataDir)
|
|
1069
1127
|
throw new Error("Audio similarity requires a configured dataDir");
|
|
1070
1128
|
const cacheDir = this.config.modelCacheDir ??
|
|
1071
1129
|
this.sharedConfig.modelCacheDir ??
|
|
1072
1130
|
path.join(dataDir, "similarity-models");
|
|
1073
1131
|
await mkdir(cacheDir, { recursive: true });
|
|
1074
|
-
const modelId = this.config
|
|
1132
|
+
const { modelId, modelRevision } = modelIdentity("compat-clap", this.config);
|
|
1075
1133
|
const allowDownload = this.config.allowModelDownload ??
|
|
1076
1134
|
this.sharedConfig.allowModelDownload;
|
|
1077
|
-
const pinned =
|
|
1078
|
-
? { revision: DEFAULT_AUDIO_MODEL_REVISION }
|
|
1079
|
-
: {};
|
|
1135
|
+
const pinned = modelRevision ? { revision: modelRevision } : {};
|
|
1080
1136
|
try {
|
|
1137
|
+
const { AutoProcessor, ClapAudioModelWithProjection } = await loadTransformers();
|
|
1081
1138
|
const processor = await AutoProcessor.from_pretrained(modelId, {
|
|
1082
1139
|
cache_dir: cacheDir,
|
|
1083
|
-
local_files_only: allowDownload
|
|
1140
|
+
local_files_only: allowDownload !== true,
|
|
1084
1141
|
...pinned,
|
|
1085
1142
|
});
|
|
1086
1143
|
const model = await ClapAudioModelWithProjection.from_pretrained(modelId, {
|
|
1087
1144
|
dtype: "q8",
|
|
1088
1145
|
cache_dir: cacheDir,
|
|
1089
|
-
local_files_only: allowDownload
|
|
1146
|
+
local_files_only: allowDownload !== true,
|
|
1090
1147
|
...pinned,
|
|
1091
1148
|
});
|
|
1092
1149
|
const featureConfig = processor.feature_extractor?.config;
|
|
@@ -1098,35 +1155,37 @@ class LocalClapAudioProvider {
|
|
|
1098
1155
|
}
|
|
1099
1156
|
catch (error) {
|
|
1100
1157
|
throw new EngineFault({
|
|
1101
|
-
code: allowDownload
|
|
1158
|
+
code: allowDownload !== true ? "OFFLINE" : "FEATURE_UNAVAILABLE",
|
|
1102
1159
|
message: `Unable to load local audio similarity model: ${errorMessage(error)}`,
|
|
1103
1160
|
});
|
|
1104
1161
|
}
|
|
1105
1162
|
}
|
|
1106
1163
|
}
|
|
1107
1164
|
class LocalTextProvider {
|
|
1108
|
-
|
|
1165
|
+
dataDir;
|
|
1109
1166
|
sharedConfig;
|
|
1110
1167
|
config;
|
|
1111
|
-
|
|
1168
|
+
get networkAccess() { return { modelDownloads: (this.config.allowModelDownload ?? this.sharedConfig.allowModelDownload) === true, inference: false }; }
|
|
1169
|
+
get embeddingSpace() { return modelIdentity("compat-text", this.config).embeddingSpace; }
|
|
1112
1170
|
dimensions = DEFAULT_TEXT_DIMENSIONS;
|
|
1113
1171
|
embedder = null;
|
|
1114
|
-
constructor(
|
|
1115
|
-
this.
|
|
1172
|
+
constructor(dataDir, sharedConfig, config) {
|
|
1173
|
+
this.dataDir = dataDir;
|
|
1116
1174
|
this.sharedConfig = sharedConfig;
|
|
1117
1175
|
this.config = config;
|
|
1118
|
-
this.embeddingSpace = config.modelId && config.modelId !== DEFAULT_TEXT_MODEL_ID
|
|
1119
|
-
? `text-${config.modelId.replace(/[^a-zA-Z0-9]+/g, "-")}-v1`
|
|
1120
|
-
: DEFAULT_TEXT_EMBEDDING_SPACE;
|
|
1121
1176
|
}
|
|
1122
|
-
async prepare() {
|
|
1177
|
+
async prepare(options = {}) {
|
|
1178
|
+
checkMediaCancellation(options);
|
|
1123
1179
|
await this.loadEmbedder();
|
|
1180
|
+
checkMediaCancellation(options);
|
|
1124
1181
|
}
|
|
1125
|
-
async embedText(text) {
|
|
1182
|
+
async embedText(text, options = {}) {
|
|
1183
|
+
checkMediaCancellation(options);
|
|
1126
1184
|
const embedder = await this.loadEmbedder();
|
|
1127
1185
|
const ranges = makeTextRanges(text, embedder.tokenizer);
|
|
1128
1186
|
const chunks = [];
|
|
1129
1187
|
for (let offset = 0; offset < ranges.length; offset += 32) {
|
|
1188
|
+
checkMediaCancellation(options);
|
|
1130
1189
|
const batchRanges = ranges.slice(offset, offset + 32);
|
|
1131
1190
|
const output = await embedder(batchRanges.map((range) => text.slice(range.startOffset, range.endOffset)), { pooling: "mean", normalize: true });
|
|
1132
1191
|
const batch = output.dims[0];
|
|
@@ -1148,30 +1207,29 @@ class LocalTextProvider {
|
|
|
1148
1207
|
async loadEmbedder() {
|
|
1149
1208
|
if (this.embedder)
|
|
1150
1209
|
return this.embedder;
|
|
1151
|
-
const dataDir = this.
|
|
1210
|
+
const dataDir = this.dataDir;
|
|
1152
1211
|
if (!dataDir)
|
|
1153
1212
|
throw new Error("Text similarity requires a configured dataDir");
|
|
1154
1213
|
const cacheDir = this.config.modelCacheDir ??
|
|
1155
1214
|
this.sharedConfig.modelCacheDir ??
|
|
1156
1215
|
path.join(dataDir, "similarity-models");
|
|
1157
1216
|
await mkdir(cacheDir, { recursive: true });
|
|
1158
|
-
const modelId = this.config
|
|
1217
|
+
const { modelId, modelRevision } = modelIdentity("compat-text", this.config);
|
|
1159
1218
|
const allowDownload = this.config.allowModelDownload ??
|
|
1160
1219
|
this.sharedConfig.allowModelDownload;
|
|
1161
1220
|
try {
|
|
1221
|
+
const { pipeline } = await loadTransformers();
|
|
1162
1222
|
this.embedder = await pipeline("feature-extraction", modelId, {
|
|
1163
1223
|
dtype: "q4",
|
|
1164
1224
|
cache_dir: cacheDir,
|
|
1165
|
-
local_files_only: allowDownload
|
|
1166
|
-
...(
|
|
1167
|
-
? { revision: DEFAULT_TEXT_MODEL_REVISION }
|
|
1168
|
-
: {}),
|
|
1225
|
+
local_files_only: allowDownload !== true,
|
|
1226
|
+
...(modelRevision ? { revision: modelRevision } : {}),
|
|
1169
1227
|
});
|
|
1170
1228
|
return this.embedder;
|
|
1171
1229
|
}
|
|
1172
1230
|
catch (error) {
|
|
1173
1231
|
throw new EngineFault({
|
|
1174
|
-
code: allowDownload
|
|
1232
|
+
code: allowDownload !== true ? "OFFLINE" : "FEATURE_UNAVAILABLE",
|
|
1175
1233
|
message: `Unable to load local text similarity model: ${errorMessage(error)}`,
|
|
1176
1234
|
});
|
|
1177
1235
|
}
|
|
@@ -1205,22 +1263,11 @@ function similarityKind(artifact) {
|
|
|
1205
1263
|
message: `Similarity supports image, video, audio, and text artifacts, not ${artifact.kind}`,
|
|
1206
1264
|
});
|
|
1207
1265
|
}
|
|
1208
|
-
async function readNormalizedImage(sourcePath) {
|
|
1209
|
-
const decoded = await
|
|
1210
|
-
.rotate()
|
|
1211
|
-
.toColourspace("srgb")
|
|
1212
|
-
.removeAlpha()
|
|
1213
|
-
.raw()
|
|
1214
|
-
.toBuffer({ resolveWithObject: true });
|
|
1215
|
-
if (decoded.info.width <= 0 || decoded.info.height <= 0) {
|
|
1216
|
-
throw new Error(`Invalid image dimensions: ${sourcePath}`);
|
|
1217
|
-
}
|
|
1218
|
-
if (decoded.info.channels !== 3) {
|
|
1219
|
-
throw new Error(`Expected RGB image after conversion: ${sourcePath}`);
|
|
1220
|
-
}
|
|
1266
|
+
async function readNormalizedImage(sourcePath, options = {}) {
|
|
1267
|
+
const [{ RawImage }, decoded] = await Promise.all([loadTransformers(), decodeModelImage(sourcePath, options)]);
|
|
1221
1268
|
return new RawImage(decoded.data, decoded.info.width, decoded.info.height, 3);
|
|
1222
1269
|
}
|
|
1223
|
-
async function probeDuration(ffprobePath, sourcePath) {
|
|
1270
|
+
async function probeDuration(ffprobePath, sourcePath, options) {
|
|
1224
1271
|
const result = await runCommand(ffprobePath, [
|
|
1225
1272
|
"-v",
|
|
1226
1273
|
"error",
|
|
@@ -1228,8 +1275,9 @@ async function probeDuration(ffprobePath, sourcePath) {
|
|
|
1228
1275
|
"format=duration",
|
|
1229
1276
|
"-of",
|
|
1230
1277
|
"default=noprint_wrappers=1:nokey=1",
|
|
1278
|
+
"-protocol_whitelist", "file,pipe",
|
|
1231
1279
|
sourcePath,
|
|
1232
|
-
]);
|
|
1280
|
+
], { ...options, maxStdoutBytes: 64 * 1024 });
|
|
1233
1281
|
const duration = Number.parseFloat(result.stdout.trim());
|
|
1234
1282
|
if (!Number.isFinite(duration) || duration <= 0) {
|
|
1235
1283
|
throw new EngineFault({
|
|
@@ -1239,12 +1287,13 @@ async function probeDuration(ffprobePath, sourcePath) {
|
|
|
1239
1287
|
}
|
|
1240
1288
|
return duration;
|
|
1241
1289
|
}
|
|
1242
|
-
async function decodeAudioToMono(ffmpegPath, sourcePath, sampleRate, maxSamples) {
|
|
1243
|
-
const output = await
|
|
1290
|
+
async function decodeAudioToMono(ffmpegPath, sourcePath, sampleRate, maxSamples, options) {
|
|
1291
|
+
const output = await runMediaProcess(ffmpegPath, [
|
|
1244
1292
|
"-hide_banner",
|
|
1245
1293
|
"-loglevel",
|
|
1246
1294
|
"error",
|
|
1247
1295
|
"-nostdin",
|
|
1296
|
+
"-protocol_whitelist", "file,pipe",
|
|
1248
1297
|
"-i",
|
|
1249
1298
|
sourcePath,
|
|
1250
1299
|
"-map",
|
|
@@ -1261,7 +1310,7 @@ async function decodeAudioToMono(ffmpegPath, sourcePath, sampleRate, maxSamples)
|
|
|
1261
1310
|
"-acodec",
|
|
1262
1311
|
"pcm_f32le",
|
|
1263
1312
|
"pipe:1",
|
|
1264
|
-
]);
|
|
1313
|
+
], { ...options, maxStdoutBytes: maxSamples * Float32Array.BYTES_PER_ELEMENT });
|
|
1265
1314
|
if (output.stdout.byteLength === 0) {
|
|
1266
1315
|
throw new EngineFault({
|
|
1267
1316
|
code: "INVALID_INPUT",
|
|
@@ -1278,42 +1327,13 @@ async function decodeAudioToMono(ffmpegPath, sourcePath, sampleRate, maxSamples)
|
|
|
1278
1327
|
? decoded.slice(0, maxSamples)
|
|
1279
1328
|
: decoded;
|
|
1280
1329
|
}
|
|
1281
|
-
async function runCommand(command, args) {
|
|
1282
|
-
const output = await
|
|
1330
|
+
async function runCommand(command, args, options = {}) {
|
|
1331
|
+
const output = await runMediaProcess(command, args, options);
|
|
1283
1332
|
return {
|
|
1284
1333
|
stdout: output.stdout.toString("utf8"),
|
|
1285
1334
|
stderr: output.stderr,
|
|
1286
1335
|
};
|
|
1287
1336
|
}
|
|
1288
|
-
function runBinaryCommand(command, args) {
|
|
1289
|
-
return new Promise((resolve, reject) => {
|
|
1290
|
-
const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1291
|
-
const stdout = [];
|
|
1292
|
-
const stderr = [];
|
|
1293
|
-
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
1294
|
-
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
1295
|
-
child.once("error", (error) => {
|
|
1296
|
-
reject(new EngineFault({
|
|
1297
|
-
code: "FEATURE_UNAVAILABLE",
|
|
1298
|
-
message: `Unable to start ${command}: ${errorMessage(error)}`,
|
|
1299
|
-
}));
|
|
1300
|
-
});
|
|
1301
|
-
child.once("close", (code) => {
|
|
1302
|
-
const output = {
|
|
1303
|
-
stdout: Buffer.concat(stdout),
|
|
1304
|
-
stderr: Buffer.concat(stderr).toString("utf8"),
|
|
1305
|
-
};
|
|
1306
|
-
if (code === 0) {
|
|
1307
|
-
resolve(output);
|
|
1308
|
-
return;
|
|
1309
|
-
}
|
|
1310
|
-
reject(new EngineFault({
|
|
1311
|
-
code: "INVALID_INPUT",
|
|
1312
|
-
message: `${command} exited with code ${code}: ${output.stderr.trim()}`,
|
|
1313
|
-
}));
|
|
1314
|
-
});
|
|
1315
|
-
});
|
|
1316
|
-
}
|
|
1317
1337
|
function normalizePlainText(raw) {
|
|
1318
1338
|
const text = raw
|
|
1319
1339
|
.replace(/^\uFEFF/, "")
|