videobook-engine 2.0.1

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.
Files changed (103) hide show
  1. package/README.md +214 -0
  2. package/dist/artifacts.d.ts +20 -0
  3. package/dist/artifacts.d.ts.map +1 -0
  4. package/dist/artifacts.js +312 -0
  5. package/dist/artifacts.js.map +1 -0
  6. package/dist/books.d.ts +7 -0
  7. package/dist/books.d.ts.map +1 -0
  8. package/dist/books.js +27 -0
  9. package/dist/books.js.map +1 -0
  10. package/dist/cas.d.ts +26 -0
  11. package/dist/cas.d.ts.map +1 -0
  12. package/dist/cas.js +139 -0
  13. package/dist/cas.js.map +1 -0
  14. package/dist/communications.d.ts +15 -0
  15. package/dist/communications.d.ts.map +1 -0
  16. package/dist/communications.js +139 -0
  17. package/dist/communications.js.map +1 -0
  18. package/dist/context.d.ts +42 -0
  19. package/dist/context.d.ts.map +1 -0
  20. package/dist/context.js +197 -0
  21. package/dist/context.js.map +1 -0
  22. package/dist/domain.d.ts +32 -0
  23. package/dist/domain.d.ts.map +1 -0
  24. package/dist/domain.js +399 -0
  25. package/dist/domain.js.map +1 -0
  26. package/dist/engine-types.d.ts +548 -0
  27. package/dist/engine-types.d.ts.map +1 -0
  28. package/dist/engine-types.js +10 -0
  29. package/dist/engine-types.js.map +1 -0
  30. package/dist/engine.d.ts +213 -0
  31. package/dist/engine.d.ts.map +1 -0
  32. package/dist/engine.js +130 -0
  33. package/dist/engine.js.map +1 -0
  34. package/dist/files.d.ts +28 -0
  35. package/dist/files.d.ts.map +1 -0
  36. package/dist/files.js +476 -0
  37. package/dist/files.js.map +1 -0
  38. package/dist/history-types.d.ts +74 -0
  39. package/dist/history-types.d.ts.map +1 -0
  40. package/dist/history-types.js +2 -0
  41. package/dist/history-types.js.map +1 -0
  42. package/dist/history.d.ts +20 -0
  43. package/dist/history.d.ts.map +1 -0
  44. package/dist/history.js +921 -0
  45. package/dist/history.js.map +1 -0
  46. package/dist/ids.d.ts +4 -0
  47. package/dist/ids.d.ts.map +1 -0
  48. package/dist/ids.js +14 -0
  49. package/dist/ids.js.map +1 -0
  50. package/dist/index.d.ts +16 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +12 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/job-queue.d.ts +51 -0
  55. package/dist/job-queue.d.ts.map +1 -0
  56. package/dist/job-queue.js +443 -0
  57. package/dist/job-queue.js.map +1 -0
  58. package/dist/media.d.ts +7 -0
  59. package/dist/media.d.ts.map +1 -0
  60. package/dist/media.js +60 -0
  61. package/dist/media.js.map +1 -0
  62. package/dist/metadata.d.ts +21 -0
  63. package/dist/metadata.d.ts.map +1 -0
  64. package/dist/metadata.js +222 -0
  65. package/dist/metadata.js.map +1 -0
  66. package/dist/notebook/types.d.ts +51 -0
  67. package/dist/notebook/types.d.ts.map +1 -0
  68. package/dist/notebook/types.js +2 -0
  69. package/dist/notebook/types.js.map +1 -0
  70. package/dist/resolver.d.ts +15 -0
  71. package/dist/resolver.d.ts.map +1 -0
  72. package/dist/resolver.js +81 -0
  73. package/dist/resolver.js.map +1 -0
  74. package/dist/runtime-services.d.ts +65 -0
  75. package/dist/runtime-services.d.ts.map +1 -0
  76. package/dist/runtime-services.js +584 -0
  77. package/dist/runtime-services.js.map +1 -0
  78. package/dist/schema.d.ts +7 -0
  79. package/dist/schema.d.ts.map +1 -0
  80. package/dist/schema.js +509 -0
  81. package/dist/schema.js.map +1 -0
  82. package/dist/similarity.d.ts +4 -0
  83. package/dist/similarity.d.ts.map +1 -0
  84. package/dist/similarity.js +1551 -0
  85. package/dist/similarity.js.map +1 -0
  86. package/dist/status.d.ts +9 -0
  87. package/dist/status.d.ts.map +1 -0
  88. package/dist/status.js +231 -0
  89. package/dist/status.js.map +1 -0
  90. package/dist/storage.d.ts +7 -0
  91. package/dist/storage.d.ts.map +1 -0
  92. package/dist/storage.js +129 -0
  93. package/dist/storage.js.map +1 -0
  94. package/dist/store.d.ts +55 -0
  95. package/dist/store.d.ts.map +1 -0
  96. package/dist/store.js +378 -0
  97. package/dist/store.js.map +1 -0
  98. package/dist/timeline.d.ts +9 -0
  99. package/dist/timeline.d.ts.map +1 -0
  100. package/dist/timeline.js +236 -0
  101. package/dist/timeline.js.map +1 -0
  102. package/docs/engine-layout.md +489 -0
  103. package/package.json +55 -0
@@ -0,0 +1,1551 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import { spawn } from "node:child_process";
4
+ import { mkdtemp, mkdir, readdir, rm } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+ import * as path from "node:path";
7
+ import { AutoProcessor, ClapAudioModelWithProjection, pipeline, RawImage, } from "@huggingface/transformers";
8
+ import sharp from "sharp";
9
+ import { Index, MetricKind, ScalarKind, } from "usearch";
10
+ import { err, } from "./engine-types.js";
11
+ import { resultOf, syncResultOf, } from "./context.js";
12
+ import { EngineFault } from "./store.js";
13
+ const DEFAULT_MODEL_ID = "Xenova/clip-vit-base-patch32";
14
+ const DEFAULT_MODEL_REVISION = "d15189d7028b43f1d3e65039190477f6af591c2a";
15
+ const DEFAULT_EMBEDDING_SPACE = "clip-vit-b32-q8-d15189d7028b43f1d3e65039190477f6af591c2a-v1";
16
+ 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
+ const DEFAULT_AUDIO_DIMENSIONS = 512;
21
+ const DEFAULT_AUDIO_SAMPLE_RATE = 48_000;
22
+ 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
+ const DEFAULT_TEXT_DIMENSIONS = 384;
27
+ const DEFAULT_TEXT_MAX_BYTES = 1024 * 1024;
28
+ const DEFAULT_TEXT_MAX_CHUNKS = 256;
29
+ const TEXT_CHUNK_TOKENS = 224;
30
+ const TEXT_OVERLAP_TOKENS = 32;
31
+ const TEXT_CHUNK_CHAR_WINDOW = 1600;
32
+ const TEXT_EXCERPT_LIMIT = 480;
33
+ const TEXT_ARTIFACT_KINDS = new Set([
34
+ "script",
35
+ "character",
36
+ "prompt",
37
+ "scene",
38
+ "final",
39
+ ]);
40
+ export function createSimilarityApi(context) {
41
+ if (!context.config.similarity)
42
+ return disabledSimilarityApi();
43
+ return new LocalSimilarityApi(context, context.config.similarity);
44
+ }
45
+ class LocalSimilarityApi {
46
+ context;
47
+ provider;
48
+ audioProvider;
49
+ textProvider;
50
+ indexes = new Map();
51
+ constructor(context, config) {
52
+ this.context = context;
53
+ this.provider = config.provider ?? new LocalClipProvider(context, config);
54
+ this.audioProvider = config.audio
55
+ ? config.audio.provider ??
56
+ new LocalClapAudioProvider(context, config, config.audio)
57
+ : null;
58
+ this.textProvider = config.text
59
+ ? config.text.provider ?? new LocalTextProvider(context, config, config.text)
60
+ : null;
61
+ }
62
+ async prepare(options = {}) {
63
+ return resultOf(async () => {
64
+ const kinds = options.kind
65
+ ? [options.kind]
66
+ : [
67
+ "image",
68
+ "video",
69
+ ...(this.audioProvider ? ["audio"] : []),
70
+ ...(this.textProvider ? ["text"] : []),
71
+ ];
72
+ const spaces = {};
73
+ const preparedMedia = new Set();
74
+ for (const kind of kinds) {
75
+ if (kind === "text") {
76
+ const provider = this.requireTextProvider();
77
+ await provider.prepare();
78
+ spaces.text = provider.embeddingSpace;
79
+ }
80
+ else {
81
+ const provider = this.providerFor(kind);
82
+ if (!preparedMedia.has(provider)) {
83
+ await provider.prepare();
84
+ preparedMedia.add(provider);
85
+ }
86
+ spaces[kind] = provider.embeddingSpace;
87
+ }
88
+ }
89
+ return {
90
+ embeddingSpace: this.provider.embeddingSpace,
91
+ embeddingSpaces: spaces,
92
+ };
93
+ });
94
+ }
95
+ async index(artifactReference, options = {}) {
96
+ return resultOf(async () => {
97
+ const artifact = this.context.artifactRow(artifactReference);
98
+ const kind = similarityKind(artifact);
99
+ if (kind === "text") {
100
+ return this.indexText(artifact, options);
101
+ }
102
+ return this.indexMedia(artifact, kind, options);
103
+ });
104
+ }
105
+ async rebuild(options = {}) {
106
+ return resultOf(async () => {
107
+ const allowedKinds = options.kind
108
+ ? [options.kind]
109
+ : [
110
+ "image",
111
+ "video",
112
+ ...(this.audioProvider ? ["audio"] : []),
113
+ ...(this.textProvider ? ["text"] : []),
114
+ ];
115
+ const artifactKinds = allowedKinds.flatMap((kind) => kind === "text" ? [...TEXT_ARTIFACT_KINDS] : [kind]);
116
+ const artifactRows = this.context.store.db
117
+ .prepare(`SELECT artifact_id, slug, kind, created_at
118
+ FROM artifacts
119
+ WHERE kind IN (${artifactKinds.map(() => "?").join(", ")})
120
+ ORDER BY created_at, artifact_id`)
121
+ .all(...artifactKinds);
122
+ const indexed = [];
123
+ for (const artifact of artifactRows) {
124
+ const kind = similarityKind(artifact);
125
+ if (!allowedKinds.includes(kind))
126
+ continue;
127
+ if (kind === "text" && !this.hasTextSource(artifact.artifact_id))
128
+ continue;
129
+ const result = await this.index(artifact.artifact_id, {
130
+ force: options.force,
131
+ });
132
+ if (!result.ok)
133
+ throw new EngineFault(result.error);
134
+ indexed.push(result.value);
135
+ }
136
+ return indexed;
137
+ });
138
+ }
139
+ status(artifactReference) {
140
+ return syncResultOf(() => {
141
+ const artifact = this.context.artifactRow(artifactReference);
142
+ const kind = similarityKind(artifact);
143
+ if (kind === "text") {
144
+ const provider = this.requireTextProvider();
145
+ const row = this.textDocumentForArtifact(artifact.artifact_id, provider.embeddingSpace);
146
+ if (!row) {
147
+ return {
148
+ artifactId: artifact.artifact_id,
149
+ kind,
150
+ state: "not_indexed",
151
+ embeddingSpace: provider.embeddingSpace,
152
+ };
153
+ }
154
+ return {
155
+ artifactId: artifact.artifact_id,
156
+ kind,
157
+ state: "ready",
158
+ embeddingSpace: row.embedding_space,
159
+ objectHash: row.object_hash,
160
+ contentHash: row.content_hash,
161
+ chunkCount: row.chunk_count,
162
+ updatedAt: row.updated_at,
163
+ };
164
+ }
165
+ const provider = this.providerFor(kind);
166
+ const row = this.embeddingForArtifact(artifact.artifact_id, provider.embeddingSpace);
167
+ if (!row) {
168
+ return {
169
+ artifactId: artifact.artifact_id,
170
+ kind,
171
+ state: "not_indexed",
172
+ embeddingSpace: provider.embeddingSpace,
173
+ };
174
+ }
175
+ return {
176
+ artifactId: artifact.artifact_id,
177
+ kind,
178
+ state: "ready",
179
+ embeddingSpace: row.embedding_space,
180
+ objectHash: row.object_hash,
181
+ frameCount: row.frame_count,
182
+ updatedAt: row.updated_at,
183
+ };
184
+ });
185
+ }
186
+ stats() {
187
+ return syncResultOf(() => {
188
+ const rows = this.context.store.db
189
+ .prepare(`SELECT kind, COUNT(*) AS count
190
+ FROM runtime_similarity_embeddings
191
+ WHERE embedding_space=?
192
+ GROUP BY kind`)
193
+ .all(this.provider.embeddingSpace);
194
+ const spaces = {
195
+ image: this.provider.embeddingSpace,
196
+ video: this.provider.embeddingSpace,
197
+ };
198
+ let audioCount = 0;
199
+ if (this.audioProvider) {
200
+ spaces.audio = this.audioProvider.embeddingSpace;
201
+ const count = this.context.store.db
202
+ .prepare(`SELECT COUNT(*) AS count
203
+ FROM runtime_similarity_embeddings
204
+ WHERE kind='audio' AND embedding_space=?`)
205
+ .get(this.audioProvider.embeddingSpace);
206
+ audioCount = count.count;
207
+ }
208
+ let textCount = 0;
209
+ if (this.textProvider) {
210
+ spaces.text = this.textProvider.embeddingSpace;
211
+ const count = this.context.store.db
212
+ .prepare(`SELECT COUNT(*) AS count
213
+ FROM runtime_text_similarity_documents d
214
+ JOIN artifacts a ON a.artifact_id=d.artifact_id
215
+ WHERE d.embedding_space=?`)
216
+ .get(this.textProvider.embeddingSpace);
217
+ textCount = count.count;
218
+ }
219
+ return {
220
+ embeddingSpace: this.provider.embeddingSpace,
221
+ embeddingSpaces: spaces,
222
+ imageCount: rows.find((row) => row.kind === "image")?.count ?? 0,
223
+ videoCount: rows.find((row) => row.kind === "video")?.count ?? 0,
224
+ audioCount,
225
+ textCount,
226
+ };
227
+ });
228
+ }
229
+ async findSimilar(artifactReference, options = {}) {
230
+ return resultOf(async () => {
231
+ const artifact = this.context.artifactRow(artifactReference);
232
+ const kind = similarityKind(artifact);
233
+ if (kind === "text") {
234
+ return this.findSimilarTextArtifact(artifact, options);
235
+ }
236
+ return this.findSimilarMedia(artifact, kind, options);
237
+ });
238
+ }
239
+ async findSimilarText(query, options = {}) {
240
+ return resultOf(async () => {
241
+ const provider = this.requireTextProvider();
242
+ const normalizedQuery = normalizePlainText(query);
243
+ if (normalizedQuery.text.length === 0) {
244
+ throw new EngineFault({
245
+ code: "INVALID_INPUT",
246
+ message: "Text similarity queries must not be empty",
247
+ });
248
+ }
249
+ const maxBytes = checkedTextMaxBytes(this.context.config.similarity?.text);
250
+ if (Buffer.byteLength(normalizedQuery.text, "utf8") > maxBytes) {
251
+ throw new EngineFault({
252
+ code: "INVALID_INPUT",
253
+ message: `Text similarity queries cannot exceed ${maxBytes} bytes`,
254
+ });
255
+ }
256
+ const chunks = await this.embedText(normalizedQuery.text);
257
+ return this.searchText(null, null, normalizedQuery.contentHash, chunks, options, provider);
258
+ });
259
+ }
260
+ async indexMedia(artifact, kind, options) {
261
+ const provider = this.providerFor(kind);
262
+ const source = this.sourceFor(artifact, kind);
263
+ const existing = this.embeddingForArtifact(artifact.artifact_id, provider.embeddingSpace);
264
+ if (existing &&
265
+ existing.object_hash === source.object_hash &&
266
+ existing.dimensions === provider.dimensions &&
267
+ !options.force) {
268
+ this.addToCachedIndex(existing, vectorFromBlob(existing));
269
+ return this.indexResult(existing, true);
270
+ }
271
+ const reusable = !options.force
272
+ ? this.embeddingForObject(source.object_hash, kind, provider.embeddingSpace, provider.dimensions)
273
+ : null;
274
+ const embedded = reusable
275
+ ? {
276
+ vector: vectorFromBlob(reusable),
277
+ frameCount: reusable.frame_count,
278
+ reused: true,
279
+ }
280
+ : await this.embedMediaSource(source);
281
+ const row = this.context.store.runtime((now) => {
282
+ if (existing) {
283
+ this.context.store.db
284
+ .prepare(`UPDATE runtime_similarity_embeddings
285
+ SET kind=?, source_path=?, object_hash=?,
286
+ dimensions=?, vector_blob=?, frame_count=?, updated_at=?
287
+ WHERE id=?`)
288
+ .run(kind, source.path, source.object_hash, provider.dimensions, vectorToBlob(embedded.vector), embedded.frameCount, now, existing.id);
289
+ return this.embeddingById(existing.id);
290
+ }
291
+ const result = this.context.store.db
292
+ .prepare(`INSERT INTO runtime_similarity_embeddings(
293
+ artifact_id, kind, source_path, object_hash,
294
+ embedding_space, dimensions, vector_blob, frame_count, updated_at
295
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
296
+ .run(artifact.artifact_id, kind, source.path, source.object_hash, provider.embeddingSpace, provider.dimensions, vectorToBlob(embedded.vector), embedded.frameCount, now);
297
+ return this.embeddingById(Number(result.lastInsertRowid));
298
+ });
299
+ this.addToCachedIndex(row, embedded.vector);
300
+ return this.indexResult(row, embedded.reused);
301
+ }
302
+ async indexText(artifact, options) {
303
+ const provider = this.requireTextProvider();
304
+ const source = this.sourceFor(artifact, "text");
305
+ const normalizedSource = await this.readNormalizedText(source);
306
+ const existing = this.textDocumentForArtifact(artifact.artifact_id, provider.embeddingSpace);
307
+ if (existing &&
308
+ existing.object_hash === source.object_hash &&
309
+ existing.content_hash === normalizedSource.contentHash &&
310
+ existing.dimensions === provider.dimensions &&
311
+ !options.force) {
312
+ return this.textIndexResult(existing, true);
313
+ }
314
+ const reusable = !options.force
315
+ ? this.textDocumentForObjectOrContent(source.object_hash, normalizedSource.contentHash, provider.embeddingSpace, provider.dimensions)
316
+ : null;
317
+ const embeddedChunks = reusable
318
+ ? this.textChunksForDocument(reusable.id).map((chunk) => ({
319
+ startOffset: chunk.start_offset,
320
+ endOffset: chunk.end_offset,
321
+ vector: vectorFromBlob(chunk),
322
+ }))
323
+ : await this.embedText(normalizedSource.text);
324
+ const maxChunks = checkedTextMaxChunks(this.context.config.similarity?.text);
325
+ if (embeddedChunks.length > maxChunks) {
326
+ throw new EngineFault({
327
+ code: "INVALID_INPUT",
328
+ message: `Text source produces more than ${maxChunks} chunks`,
329
+ });
330
+ }
331
+ this.validateTextChunks(normalizedSource.text, embeddedChunks, provider.dimensions);
332
+ this.indexes.delete(indexKey("text", provider.embeddingSpace));
333
+ const row = this.context.store.runtime((now) => {
334
+ let documentId;
335
+ if (existing) {
336
+ documentId = existing.id;
337
+ this.context.store.db
338
+ .prepare(`UPDATE runtime_text_similarity_documents
339
+ SET source_path=?, object_hash=?, content_hash=?,
340
+ dimensions=?, chunk_count=?, updated_at=?
341
+ WHERE id=?`)
342
+ .run(source.path, source.object_hash, normalizedSource.contentHash, provider.dimensions, embeddedChunks.length, now, documentId);
343
+ this.context.store.db
344
+ .prepare("DELETE FROM runtime_text_similarity_chunks WHERE document_id=?")
345
+ .run(documentId);
346
+ }
347
+ else {
348
+ const result = this.context.store.db
349
+ .prepare(`INSERT INTO runtime_text_similarity_documents(
350
+ artifact_id, source_path, object_hash, content_hash,
351
+ embedding_space, dimensions, chunk_count, updated_at
352
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
353
+ .run(artifact.artifact_id, source.path, source.object_hash, normalizedSource.contentHash, provider.embeddingSpace, provider.dimensions, embeddedChunks.length, now);
354
+ documentId = Number(result.lastInsertRowid);
355
+ }
356
+ const insertChunk = this.context.store.db.prepare(`INSERT INTO runtime_text_similarity_chunks(
357
+ document_id, artifact_id, embedding_space, chunk_index,
358
+ start_offset, end_offset, chunk_text, dimensions, vector_blob, updated_at
359
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
360
+ embeddedChunks.forEach((chunk, index) => {
361
+ insertChunk.run(documentId, artifact.artifact_id, provider.embeddingSpace, index, chunk.startOffset, chunk.endOffset, normalizedSource.text.slice(chunk.startOffset, chunk.endOffset), provider.dimensions, vectorToBlob(normalized(chunk.vector)), now);
362
+ });
363
+ return this.textDocumentById(documentId);
364
+ });
365
+ return this.textIndexResult(row, Boolean(reusable));
366
+ }
367
+ async findSimilarTextArtifact(artifact, options) {
368
+ const provider = this.requireTextProvider();
369
+ const query = this.textDocumentForArtifact(artifact.artifact_id, provider.embeddingSpace);
370
+ if (!query) {
371
+ throw new EngineFault({
372
+ code: "NOT_READY",
373
+ message: `Artifact is not indexed for similarity: ${artifact.slug}`,
374
+ details: { artifactId: artifact.artifact_id },
375
+ });
376
+ }
377
+ const queryChunks = this.textChunksForDocument(query.id).map((chunk) => ({
378
+ startOffset: chunk.start_offset,
379
+ endOffset: chunk.end_offset,
380
+ vector: vectorFromBlob(chunk),
381
+ }));
382
+ return this.searchText(artifact.artifact_id, query.object_hash, query.content_hash, queryChunks, options, provider);
383
+ }
384
+ async findSimilarMedia(artifact, kind, options) {
385
+ const limit = checkedLimit(options.limit);
386
+ const minScore = checkedMinScore(options.minScore);
387
+ const provider = this.providerFor(kind);
388
+ const query = this.embeddingForArtifact(artifact.artifact_id, provider.embeddingSpace);
389
+ if (!query) {
390
+ throw new EngineFault({
391
+ code: "NOT_READY",
392
+ message: `Artifact is not indexed for similarity: ${artifact.slug}`,
393
+ details: { artifactId: artifact.artifact_id },
394
+ });
395
+ }
396
+ const index = this.indexFor(kind, query.embedding_space, query.dimensions);
397
+ if (index.index.size() === 0)
398
+ return [];
399
+ const candidateCount = Math.min(index.index.size(), Math.max(20, limit * 5));
400
+ const nearest = index.index.search(vectorFromBlob(query), candidateCount, 0);
401
+ const ids = new Set();
402
+ for (const key of nearest.keys)
403
+ ids.add(Number(key));
404
+ for (const exact of this.exactObjectEmbeddings(kind, query.object_hash, query.embedding_space)) {
405
+ ids.add(exact.id);
406
+ }
407
+ const candidates = this.activeEmbeddingsByIds(kind, query.embedding_space, [...ids]);
408
+ const queryVector = vectorFromBlob(query);
409
+ return candidates
410
+ .filter((candidate) => options.includeSelf === true ||
411
+ candidate.artifact_id !== artifact.artifact_id)
412
+ .map((candidate) => {
413
+ const exactBytes = candidate.object_hash === query.object_hash;
414
+ const global = exactBytes
415
+ ? 1
416
+ : cosine(queryVector, vectorFromBlob(candidate));
417
+ return {
418
+ artifactId: candidate.artifact_id,
419
+ slug: candidate.slug ?? candidate.artifact_id,
420
+ kind: candidate.kind,
421
+ score: global,
422
+ exactBytes,
423
+ embeddingSpace: candidate.embedding_space,
424
+ signals: { global },
425
+ };
426
+ })
427
+ .filter((match) => minScore === undefined || match.score >= minScore)
428
+ .sort((left, right) => right.score - left.score ||
429
+ left.artifactId.localeCompare(right.artifactId))
430
+ .slice(0, limit);
431
+ }
432
+ searchText(queryArtifactId, queryObjectHash, queryContentHash, queryChunks, options, provider) {
433
+ const limit = checkedLimit(options.limit);
434
+ const minScore = checkedMinScore(options.minScore);
435
+ if (queryChunks.length === 0)
436
+ return [];
437
+ const index = this.textIndexFor(provider.embeddingSpace, provider.dimensions);
438
+ if (index.index.size() === 0)
439
+ return [];
440
+ const candidateCount = Math.min(index.index.size(), Math.max(50, limit * 10));
441
+ const nearest = index.index.search(queryChunks.map((chunk) => normalized(chunk.vector)), candidateCount, 0);
442
+ const pairs = new Map();
443
+ for (let queryIndex = 0; queryIndex < queryChunks.length; queryIndex += 1) {
444
+ const matches = queryChunks.length === 1
445
+ ? nearest
446
+ : nearest.get(queryIndex);
447
+ for (const key of matches.keys) {
448
+ const chunkId = Number(key);
449
+ const current = pairs.get(chunkId) ?? [];
450
+ current.push({ chunkId, queryChunkIndex: queryIndex });
451
+ pairs.set(chunkId, current);
452
+ }
453
+ }
454
+ const exactDocuments = new Map();
455
+ if (queryObjectHash) {
456
+ for (const document of this.textDocumentsForHash("object_hash", queryObjectHash, provider)) {
457
+ exactDocuments.set(document.id, {
458
+ exactBytes: true,
459
+ exactContent: document.content_hash === queryContentHash,
460
+ });
461
+ }
462
+ }
463
+ for (const document of this.textDocumentsForHash("content_hash", queryContentHash, provider)) {
464
+ const current = exactDocuments.get(document.id);
465
+ exactDocuments.set(document.id, {
466
+ exactBytes: current?.exactBytes ?? false,
467
+ exactContent: true,
468
+ });
469
+ }
470
+ for (const document of exactDocuments.keys()) {
471
+ const first = this.textChunksForDocument(document)[0];
472
+ if (first) {
473
+ const current = pairs.get(first.id) ?? [];
474
+ current.push({
475
+ chunkId: first.id,
476
+ queryChunkIndex: 0,
477
+ exactBytes: exactDocuments.get(document)?.exactBytes,
478
+ exactContent: exactDocuments.get(document)?.exactContent,
479
+ });
480
+ pairs.set(first.id, current);
481
+ }
482
+ }
483
+ const chunks = this.activeTextChunksByIds(provider.embeddingSpace, provider.dimensions, [...pairs.keys()]);
484
+ const best = new Map();
485
+ for (const chunk of chunks) {
486
+ for (const pair of pairs.get(chunk.id) ?? []) {
487
+ const queryChunk = queryChunks[pair.queryChunkIndex];
488
+ if (!queryChunk)
489
+ continue;
490
+ const exactBytes = pair.exactBytes === true ||
491
+ chunk.object_hash === queryObjectHash;
492
+ const exactContent = pair.exactContent === true ||
493
+ chunk.content_hash === queryContentHash;
494
+ const score = exactBytes || exactContent
495
+ ? 1
496
+ : cosine(normalized(queryChunk.vector), vectorFromBlob(chunk));
497
+ const current = best.get(chunk.document_id);
498
+ if (!current || score > current.score) {
499
+ best.set(chunk.document_id, {
500
+ chunk,
501
+ queryChunkIndex: pair.queryChunkIndex,
502
+ score,
503
+ exactBytes,
504
+ exactContent,
505
+ });
506
+ }
507
+ }
508
+ }
509
+ return [...best.entries()]
510
+ .filter(([, match]) => options.includeSelf === true ||
511
+ queryArtifactId === null || match.chunk.artifact_id !== queryArtifactId)
512
+ .map(([, match]) => {
513
+ const queryChunk = queryChunks[match.queryChunkIndex];
514
+ return {
515
+ artifactId: match.chunk.artifact_id,
516
+ slug: match.chunk.slug ?? match.chunk.artifact_id,
517
+ kind: "text",
518
+ score: match.score,
519
+ exactBytes: match.exactBytes,
520
+ exactContent: match.exactContent,
521
+ embeddingSpace: match.chunk.embedding_space,
522
+ text: {
523
+ sourcePath: match.chunk.source_path ?? "original.txt",
524
+ chunkIndex: match.chunk.chunk_index,
525
+ startOffset: match.chunk.start_offset,
526
+ endOffset: match.chunk.end_offset,
527
+ excerpt: excerpt(match.chunk.chunk_text),
528
+ ...(queryChunk
529
+ ? {
530
+ queryStartOffset: queryChunk.startOffset,
531
+ queryEndOffset: queryChunk.endOffset,
532
+ }
533
+ : {}),
534
+ },
535
+ signals: { global: match.score },
536
+ };
537
+ })
538
+ .filter((match) => minScore === undefined || match.score >= minScore)
539
+ .sort((left, right) => right.score - left.score ||
540
+ left.artifactId.localeCompare(right.artifactId))
541
+ .slice(0, limit);
542
+ }
543
+ async embedMediaSource(source) {
544
+ const localPath = await this.context.objects.ensureLocalPath(source.object_hash);
545
+ if (source.kind === "audio") {
546
+ const provider = this.requireAudioProvider();
547
+ await provider.prepare();
548
+ return {
549
+ vector: normalized(await provider.embedAudio(localPath)),
550
+ frameCount: null,
551
+ reused: false,
552
+ };
553
+ }
554
+ await this.provider.prepare();
555
+ if (source.kind === "image") {
556
+ return {
557
+ vector: normalized(await this.provider.embedImage(localPath)),
558
+ frameCount: null,
559
+ reused: false,
560
+ };
561
+ }
562
+ const video = await this.provider.embedVideo(localPath);
563
+ return {
564
+ vector: normalized(video.vector),
565
+ frameCount: video.frameCount,
566
+ reused: false,
567
+ };
568
+ }
569
+ async embedText(text) {
570
+ const provider = this.requireTextProvider();
571
+ await provider.prepare();
572
+ const chunks = await provider.embedText(text);
573
+ this.validateTextChunks(text, chunks, provider.dimensions);
574
+ const maxChunks = checkedTextMaxChunks(this.context.config.similarity?.text);
575
+ if (chunks.length > maxChunks) {
576
+ throw new EngineFault({
577
+ code: "INVALID_INPUT",
578
+ message: `Text source produces more than ${maxChunks} chunks`,
579
+ });
580
+ }
581
+ return chunks.map((chunk) => ({
582
+ ...chunk,
583
+ vector: normalized(chunk.vector),
584
+ }));
585
+ }
586
+ async readNormalizedText(source) {
587
+ const maxBytes = checkedTextMaxBytes(this.context.config.similarity?.text);
588
+ const localPath = await this.context.objects.ensureLocalPath(source.object_hash);
589
+ const bytes = await readFile(localPath);
590
+ if (bytes.byteLength > maxBytes) {
591
+ throw new EngineFault({
592
+ code: "INVALID_INPUT",
593
+ message: `Text source exceeds the ${maxBytes}-byte similarity limit`,
594
+ });
595
+ }
596
+ let raw;
597
+ try {
598
+ raw = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
599
+ }
600
+ catch {
601
+ throw new EngineFault({
602
+ code: "INVALID_INPUT",
603
+ message: `Text source is not valid UTF-8: ${source.path}`,
604
+ });
605
+ }
606
+ const extension = path.extname(source.path).toLowerCase();
607
+ const normalizedText = extension === ".json"
608
+ ? normalizeJsonText(raw, source.path)
609
+ : normalizePlainText(raw);
610
+ if (normalizedText.text.length === 0) {
611
+ throw new EngineFault({
612
+ code: "INVALID_INPUT",
613
+ message: `Text source is empty: ${source.path}`,
614
+ });
615
+ }
616
+ return normalizedText;
617
+ }
618
+ validateTextChunks(text, chunks, dimensions) {
619
+ if (chunks.length === 0) {
620
+ throw new EngineFault({
621
+ code: "INVALID_INPUT",
622
+ message: "Text provider returned no chunks",
623
+ });
624
+ }
625
+ let previousStart = -1;
626
+ for (const chunk of chunks) {
627
+ if (!Number.isInteger(chunk.startOffset) ||
628
+ !Number.isInteger(chunk.endOffset) ||
629
+ chunk.startOffset < 0 ||
630
+ chunk.endOffset <= chunk.startOffset ||
631
+ chunk.endOffset > text.length ||
632
+ chunk.startOffset < previousStart) {
633
+ throw new EngineFault({
634
+ code: "INVALID_INPUT",
635
+ message: "Text provider returned invalid chunk offsets",
636
+ });
637
+ }
638
+ if (chunk.vector.length !== dimensions) {
639
+ throw new EngineFault({
640
+ code: "INVALID_INPUT",
641
+ message: `Text provider returned ${chunk.vector.length} dimensions; expected ${dimensions}`,
642
+ });
643
+ }
644
+ normalized(chunk.vector);
645
+ previousStart = chunk.startOffset;
646
+ }
647
+ }
648
+ sourceFor(artifact, kind) {
649
+ const rows = this.context.store.db
650
+ .prepare(`SELECT f.artifact_id, f.path, f.object_hash, o.size_bytes,
651
+ f.mtime_ms, f.created_at
652
+ FROM artifact_files f
653
+ JOIN objects o ON o.object_hash=f.object_hash
654
+ WHERE f.artifact_id=?
655
+ ORDER BY f.path`)
656
+ .all(artifact.artifact_id);
657
+ const extension = kind === "image"
658
+ ? "(?:png|jpe?g|webp)"
659
+ : kind === "video"
660
+ ? "(?:mp4|mov|webm|mkv|avi)"
661
+ : kind === "audio"
662
+ ? "(?:mp3|wav|ogg|flac|aac|m4a)"
663
+ : "(?:json|md|txt)";
664
+ const sources = rows.filter((row) => new RegExp(`(?:^|/)original\\.${extension}$`, "i").test(row.path));
665
+ if (sources.length === 0) {
666
+ throw new EngineFault({
667
+ code: "INVALID_INPUT",
668
+ message: `No supported original ${kind} file for ${artifact.slug}`,
669
+ });
670
+ }
671
+ if (kind === "text" && sources.length > 1) {
672
+ throw new EngineFault({
673
+ code: "INVALID_INPUT",
674
+ message: `Multiple supported original ${kind} files for ${artifact.slug}`,
675
+ details: { paths: sources.map((source) => source.path) },
676
+ });
677
+ }
678
+ return { ...sources[0], kind };
679
+ }
680
+ hasTextSource(artifactId) {
681
+ const rows = this.context.store.db
682
+ .prepare("SELECT path FROM artifact_files WHERE artifact_id=?")
683
+ .all(artifactId);
684
+ return rows.some((row) => /(?:^|\/)original\.(?:json|md|txt)$/i.test(row.path));
685
+ }
686
+ embeddingForArtifact(artifactId, embeddingSpace) {
687
+ const row = this.context.store.db
688
+ .prepare(`SELECT id, artifact_id, kind, source_path, object_hash,
689
+ embedding_space, dimensions, vector_blob, frame_count, updated_at
690
+ FROM runtime_similarity_embeddings
691
+ WHERE artifact_id=? AND embedding_space=?`)
692
+ .get(artifactId, embeddingSpace);
693
+ return row ?? null;
694
+ }
695
+ embeddingForObject(objectHash, kind, embeddingSpace, dimensions) {
696
+ const row = this.context.store.db
697
+ .prepare(`SELECT id, artifact_id, kind, source_path, object_hash,
698
+ embedding_space, dimensions, vector_blob, frame_count, updated_at
699
+ FROM runtime_similarity_embeddings
700
+ WHERE object_hash=? AND kind=? AND embedding_space=? AND dimensions=?
701
+ ORDER BY updated_at DESC, id DESC LIMIT 1`)
702
+ .get(objectHash, kind, embeddingSpace, dimensions);
703
+ return row ?? null;
704
+ }
705
+ embeddingById(id) {
706
+ const row = this.context.store.db
707
+ .prepare(`SELECT id, artifact_id, kind, source_path, object_hash,
708
+ embedding_space, dimensions, vector_blob, frame_count, updated_at
709
+ FROM runtime_similarity_embeddings WHERE id=?`)
710
+ .get(id);
711
+ if (!row)
712
+ throw new Error(`Similarity embedding not found: ${id}`);
713
+ return row;
714
+ }
715
+ exactObjectEmbeddings(kind, objectHash, embeddingSpace) {
716
+ return this.context.store.db
717
+ .prepare(`SELECT id, artifact_id, kind, source_path, object_hash,
718
+ embedding_space, dimensions, vector_blob, frame_count, updated_at
719
+ FROM runtime_similarity_embeddings
720
+ WHERE kind=? AND object_hash=? AND embedding_space=?`)
721
+ .all(kind, objectHash, embeddingSpace);
722
+ }
723
+ activeEmbeddingsByIds(kind, embeddingSpace, ids) {
724
+ if (ids.length === 0)
725
+ return [];
726
+ const placeholders = ids.map(() => "?").join(", ");
727
+ return this.context.store.db
728
+ .prepare(`SELECT e.id, e.artifact_id, e.kind, e.source_path,
729
+ e.object_hash, e.embedding_space, e.dimensions, e.vector_blob,
730
+ e.frame_count, e.updated_at, a.slug
731
+ FROM runtime_similarity_embeddings e
732
+ JOIN artifacts a ON a.artifact_id=e.artifact_id
733
+ WHERE e.kind=? AND e.embedding_space=?
734
+ AND e.id IN (${placeholders})`)
735
+ .all(kind, embeddingSpace, ...ids);
736
+ }
737
+ indexFor(kind, embeddingSpace, dimensions) {
738
+ return this.cachedIndex(kind, embeddingSpace, dimensions, `SELECT e.id, e.artifact_id, e.kind, e.source_path,
739
+ e.object_hash, e.embedding_space, e.dimensions, e.vector_blob,
740
+ e.frame_count, e.updated_at
741
+ FROM runtime_similarity_embeddings e
742
+ JOIN artifacts a ON a.artifact_id=e.artifact_id
743
+ WHERE e.kind=? AND e.embedding_space=?
744
+ AND e.dimensions=?
745
+ ORDER BY e.id`);
746
+ }
747
+ textIndexFor(embeddingSpace, dimensions) {
748
+ const key = indexKey("text", embeddingSpace);
749
+ const current = this.indexes.get(key);
750
+ if (current)
751
+ return current;
752
+ const index = new Index({
753
+ dimensions,
754
+ metric: MetricKind.Cos,
755
+ quantization: ScalarKind.F16,
756
+ connectivity: 16,
757
+ expansion_add: 128,
758
+ expansion_search: 128,
759
+ multi: false,
760
+ });
761
+ const rows = this.context.store.db
762
+ .prepare(`SELECT c.id, c.document_id, c.artifact_id,
763
+ c.embedding_space, c.chunk_index, c.start_offset,
764
+ c.end_offset, c.chunk_text, c.dimensions, c.vector_blob,
765
+ c.updated_at
766
+ FROM runtime_text_similarity_chunks c
767
+ JOIN artifacts a ON a.artifact_id=c.artifact_id
768
+ WHERE c.embedding_space=?
769
+ AND c.dimensions=?
770
+ ORDER BY c.id`)
771
+ .all(embeddingSpace, dimensions);
772
+ for (const row of rows) {
773
+ index.add(BigInt(row.id), vectorFromBlob(row), 0);
774
+ }
775
+ const cached = { index, dimensions };
776
+ this.indexes.set(key, cached);
777
+ return cached;
778
+ }
779
+ cachedIndex(kind, embeddingSpace, dimensions, sql) {
780
+ const key = indexKey(kind, embeddingSpace);
781
+ const current = this.indexes.get(key);
782
+ if (current)
783
+ return current;
784
+ const index = new Index({
785
+ dimensions,
786
+ metric: MetricKind.Cos,
787
+ quantization: ScalarKind.F16,
788
+ connectivity: 16,
789
+ expansion_add: 128,
790
+ expansion_search: 128,
791
+ multi: false,
792
+ });
793
+ const rows = this.context.store.db
794
+ .prepare(sql)
795
+ .all(kind, embeddingSpace, dimensions);
796
+ for (const row of rows) {
797
+ index.add(BigInt(row.id), vectorFromBlob(row), 0);
798
+ }
799
+ const cached = { index, dimensions };
800
+ this.indexes.set(key, cached);
801
+ return cached;
802
+ }
803
+ addToCachedIndex(row, vector) {
804
+ const cached = this.indexes.get(indexKey(row.kind, row.embedding_space));
805
+ if (!cached)
806
+ return;
807
+ if (cached.dimensions !== vector.length) {
808
+ throw new Error("Similarity index dimension mismatch");
809
+ }
810
+ try {
811
+ cached.index.remove(BigInt(row.id));
812
+ }
813
+ catch {
814
+ // The row may not have been part of an index built before it existed.
815
+ }
816
+ cached.index.add(BigInt(row.id), vector, 0);
817
+ }
818
+ textDocumentForArtifact(artifactId, embeddingSpace) {
819
+ const row = this.context.store.db
820
+ .prepare(`SELECT id, artifact_id, source_path, object_hash,
821
+ content_hash, embedding_space, dimensions, chunk_count, updated_at
822
+ FROM runtime_text_similarity_documents
823
+ WHERE artifact_id=? AND embedding_space=?`)
824
+ .get(artifactId, embeddingSpace);
825
+ return row ?? null;
826
+ }
827
+ textDocumentById(id) {
828
+ const row = this.context.store.db
829
+ .prepare(`SELECT id, artifact_id, source_path, object_hash,
830
+ content_hash, embedding_space, dimensions, chunk_count, updated_at
831
+ FROM runtime_text_similarity_documents WHERE id=?`)
832
+ .get(id);
833
+ if (!row)
834
+ throw new Error(`Text similarity document not found: ${id}`);
835
+ return row;
836
+ }
837
+ textDocumentForObjectOrContent(objectHash, contentHash, embeddingSpace, dimensions) {
838
+ const row = this.context.store.db
839
+ .prepare(`SELECT id, artifact_id, source_path, object_hash,
840
+ content_hash, embedding_space, dimensions, chunk_count, updated_at
841
+ FROM runtime_text_similarity_documents
842
+ WHERE (object_hash=? OR content_hash=?)
843
+ AND embedding_space=? AND dimensions=?
844
+ ORDER BY updated_at DESC, id DESC LIMIT 1`)
845
+ .get(objectHash, contentHash, embeddingSpace, dimensions);
846
+ return row ?? null;
847
+ }
848
+ textDocumentsForHash(column, hash, provider) {
849
+ return this.context.store.db
850
+ .prepare(`SELECT id, artifact_id, source_path, object_hash,
851
+ content_hash, embedding_space, dimensions, chunk_count, updated_at
852
+ FROM runtime_text_similarity_documents
853
+ WHERE ${column}=? AND embedding_space=?
854
+ AND dimensions=?`)
855
+ .all(hash, provider.embeddingSpace, provider.dimensions);
856
+ }
857
+ textChunksForDocument(documentId) {
858
+ return this.context.store.db
859
+ .prepare(`SELECT id, document_id, artifact_id, embedding_space,
860
+ chunk_index, start_offset, end_offset, chunk_text, dimensions,
861
+ vector_blob, updated_at
862
+ FROM runtime_text_similarity_chunks
863
+ WHERE document_id=? ORDER BY chunk_index`)
864
+ .all(documentId);
865
+ }
866
+ activeTextChunksByIds(embeddingSpace, dimensions, ids) {
867
+ if (ids.length === 0)
868
+ return [];
869
+ const placeholders = ids.map(() => "?").join(", ");
870
+ return this.context.store.db
871
+ .prepare(`SELECT c.id, c.document_id, c.artifact_id,
872
+ c.embedding_space, c.chunk_index, c.start_offset,
873
+ c.end_offset, c.chunk_text, c.dimensions, c.vector_blob,
874
+ c.updated_at, d.source_path, d.object_hash, d.content_hash,
875
+ a.slug
876
+ FROM runtime_text_similarity_chunks c
877
+ JOIN runtime_text_similarity_documents d ON d.id=c.document_id
878
+ JOIN artifacts a ON a.artifact_id=c.artifact_id
879
+ WHERE c.embedding_space=? AND c.dimensions=?
880
+ AND c.id IN (${placeholders})`)
881
+ .all(embeddingSpace, dimensions, ...ids);
882
+ }
883
+ indexResult(row, reused) {
884
+ return {
885
+ artifactId: row.artifact_id,
886
+ kind: row.kind,
887
+ embeddingSpace: row.embedding_space,
888
+ frameCount: row.frame_count,
889
+ reused,
890
+ };
891
+ }
892
+ textIndexResult(row, reused) {
893
+ return {
894
+ artifactId: row.artifact_id,
895
+ kind: "text",
896
+ embeddingSpace: row.embedding_space,
897
+ frameCount: null,
898
+ chunkCount: row.chunk_count,
899
+ reused,
900
+ };
901
+ }
902
+ providerFor(kind) {
903
+ return kind === "audio" ? this.requireAudioProvider() : this.provider;
904
+ }
905
+ requireAudioProvider() {
906
+ if (!this.audioProvider) {
907
+ throw new EngineFault({
908
+ code: "FEATURE_UNAVAILABLE",
909
+ message: "Audio similarity is disabled; configure EngineConfig.similarity.audio",
910
+ });
911
+ }
912
+ return this.audioProvider;
913
+ }
914
+ requireTextProvider() {
915
+ if (!this.textProvider) {
916
+ throw new EngineFault({
917
+ code: "FEATURE_UNAVAILABLE",
918
+ message: "Text similarity is disabled; configure EngineConfig.similarity.text",
919
+ });
920
+ }
921
+ return this.textProvider;
922
+ }
923
+ }
924
+ class LocalClipProvider {
925
+ context;
926
+ config;
927
+ embeddingSpace = DEFAULT_EMBEDDING_SPACE;
928
+ dimensions = DEFAULT_DIMENSIONS;
929
+ embedder = null;
930
+ constructor(context, config) {
931
+ this.context = context;
932
+ this.config = config;
933
+ }
934
+ async prepare() {
935
+ await this.loadEmbedder();
936
+ }
937
+ async embedImage(sourcePath) {
938
+ const vectors = await this.embedImages([sourcePath]);
939
+ const vector = vectors[0];
940
+ if (!vector)
941
+ throw new Error("Image embedder returned no vector");
942
+ return vector;
943
+ }
944
+ async embedVideo(sourcePath) {
945
+ const duration = await probeDuration(this.config.ffprobePath ?? "ffprobe", sourcePath);
946
+ const frameCount = Math.min(120, Math.max(1, Math.ceil(duration / 2)));
947
+ const framesDir = await mkdtemp(path.join(tmpdir(), "videobook-sim-"));
948
+ try {
949
+ const fps = `${frameCount}/${Math.max(duration, 0.001)}`;
950
+ await runCommand(this.config.ffmpegPath ?? "ffmpeg", [
951
+ "-hide_banner",
952
+ "-loglevel",
953
+ "error",
954
+ "-i",
955
+ sourcePath,
956
+ "-an",
957
+ "-vf",
958
+ `fps=${fps},scale=224:224:force_original_aspect_ratio=increase,crop=224:224`,
959
+ "-frames:v",
960
+ String(frameCount),
961
+ path.join(framesDir, "frame-%04d.png"),
962
+ ]);
963
+ const frames = (await readdir(framesDir))
964
+ .filter((name) => name.endsWith(".png"))
965
+ .sort()
966
+ .map((name) => path.join(framesDir, name));
967
+ if (frames.length === 0) {
968
+ throw new EngineFault({
969
+ code: "INVALID_INPUT",
970
+ message: `No video frames could be decoded from ${sourcePath}`,
971
+ });
972
+ }
973
+ const vectors = [];
974
+ for (let offset = 0; offset < frames.length; offset += 8) {
975
+ vectors.push(...(await this.embedImages(frames.slice(offset, offset + 8))));
976
+ }
977
+ return { vector: normalizedCentroid(vectors), frameCount: vectors.length };
978
+ }
979
+ finally {
980
+ await rm(framesDir, { recursive: true, force: true }).catch(() => undefined);
981
+ }
982
+ }
983
+ async loadEmbedder() {
984
+ if (this.embedder)
985
+ return this.embedder;
986
+ const dataDir = this.context.config.dataDir;
987
+ if (!dataDir)
988
+ throw new Error("Similarity requires a configured dataDir");
989
+ const cacheDir = this.config.modelCacheDir ?? path.join(dataDir, "similarity-models");
990
+ await mkdir(cacheDir, { recursive: true });
991
+ const modelId = this.config.modelId ?? DEFAULT_MODEL_ID;
992
+ try {
993
+ this.embedder = await pipeline("image-feature-extraction", modelId, {
994
+ dtype: "q8",
995
+ cache_dir: cacheDir,
996
+ local_files_only: this.config.allowModelDownload === false,
997
+ ...(modelId === DEFAULT_MODEL_ID
998
+ ? { revision: DEFAULT_MODEL_REVISION }
999
+ : {}),
1000
+ });
1001
+ return this.embedder;
1002
+ }
1003
+ catch (error) {
1004
+ throw new EngineFault({
1005
+ code: this.config.allowModelDownload === false ? "OFFLINE" : "FEATURE_UNAVAILABLE",
1006
+ message: `Unable to load local similarity model: ${errorMessage(error)}`,
1007
+ });
1008
+ }
1009
+ }
1010
+ async embedImages(sourcePaths) {
1011
+ const embedder = await this.loadEmbedder();
1012
+ const images = await Promise.all(sourcePaths.map(readNormalizedImage));
1013
+ const output = await embedder(images);
1014
+ const batch = output.dims[0];
1015
+ const dimensions = output.dims[1];
1016
+ if (batch !== sourcePaths.length || dimensions !== this.dimensions) {
1017
+ throw new Error(`Unexpected embedding shape: ${output.dims.join("x")}`);
1018
+ }
1019
+ const vectors = [];
1020
+ for (let index = 0; index < batch; index += 1) {
1021
+ const start = index * this.dimensions;
1022
+ vectors.push(normalized(output.data.slice(start, start + this.dimensions)));
1023
+ }
1024
+ return vectors;
1025
+ }
1026
+ }
1027
+ class LocalClapAudioProvider {
1028
+ context;
1029
+ sharedConfig;
1030
+ config;
1031
+ embeddingSpace;
1032
+ dimensions = DEFAULT_AUDIO_DIMENSIONS;
1033
+ processor = null;
1034
+ model = null;
1035
+ sampleRate = DEFAULT_AUDIO_SAMPLE_RATE;
1036
+ maxSamples = DEFAULT_AUDIO_MAX_SAMPLES;
1037
+ constructor(context, sharedConfig, config) {
1038
+ this.context = context;
1039
+ this.sharedConfig = sharedConfig;
1040
+ 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
+ }
1046
+ async prepare() {
1047
+ await this.loadModel();
1048
+ }
1049
+ async embedAudio(sourcePath) {
1050
+ const { processor, model } = await this.loadModel();
1051
+ const audio = await decodeAudioToMono(this.config.ffmpegPath ?? this.sharedConfig.ffmpegPath ?? "ffmpeg", sourcePath, this.sampleRate, this.maxSamples);
1052
+ const inputs = await processor(audio);
1053
+ const output = await model(inputs);
1054
+ const dimensions = output.audio_embeds.dims;
1055
+ if (dimensions.length !== 2 ||
1056
+ dimensions[0] !== 1 ||
1057
+ dimensions[1] !== this.dimensions ||
1058
+ output.audio_embeds.data.length !== this.dimensions) {
1059
+ throw new Error(`Unexpected audio embedding shape: ${dimensions.join("x")}`);
1060
+ }
1061
+ return output.audio_embeds.data.slice();
1062
+ }
1063
+ async loadModel() {
1064
+ if (this.processor && this.model) {
1065
+ return { processor: this.processor, model: this.model };
1066
+ }
1067
+ const dataDir = this.context.config.dataDir;
1068
+ if (!dataDir)
1069
+ throw new Error("Audio similarity requires a configured dataDir");
1070
+ const cacheDir = this.config.modelCacheDir ??
1071
+ this.sharedConfig.modelCacheDir ??
1072
+ path.join(dataDir, "similarity-models");
1073
+ await mkdir(cacheDir, { recursive: true });
1074
+ const modelId = this.config.modelId ?? DEFAULT_AUDIO_MODEL_ID;
1075
+ const allowDownload = this.config.allowModelDownload ??
1076
+ this.sharedConfig.allowModelDownload;
1077
+ const pinned = modelId === DEFAULT_AUDIO_MODEL_ID
1078
+ ? { revision: DEFAULT_AUDIO_MODEL_REVISION }
1079
+ : {};
1080
+ try {
1081
+ const processor = await AutoProcessor.from_pretrained(modelId, {
1082
+ cache_dir: cacheDir,
1083
+ local_files_only: allowDownload === false,
1084
+ ...pinned,
1085
+ });
1086
+ const model = await ClapAudioModelWithProjection.from_pretrained(modelId, {
1087
+ dtype: "q8",
1088
+ cache_dir: cacheDir,
1089
+ local_files_only: allowDownload === false,
1090
+ ...pinned,
1091
+ });
1092
+ const featureConfig = processor.feature_extractor?.config;
1093
+ this.sampleRate = checkedAudioSampleRate(featureConfig?.sampling_rate);
1094
+ this.maxSamples = checkedAudioMaxSamples(featureConfig?.nb_max_samples, this.sampleRate);
1095
+ this.processor = processor;
1096
+ this.model = model;
1097
+ return { processor, model };
1098
+ }
1099
+ catch (error) {
1100
+ throw new EngineFault({
1101
+ code: allowDownload === false ? "OFFLINE" : "FEATURE_UNAVAILABLE",
1102
+ message: `Unable to load local audio similarity model: ${errorMessage(error)}`,
1103
+ });
1104
+ }
1105
+ }
1106
+ }
1107
+ class LocalTextProvider {
1108
+ context;
1109
+ sharedConfig;
1110
+ config;
1111
+ embeddingSpace;
1112
+ dimensions = DEFAULT_TEXT_DIMENSIONS;
1113
+ embedder = null;
1114
+ constructor(context, sharedConfig, config) {
1115
+ this.context = context;
1116
+ this.sharedConfig = sharedConfig;
1117
+ 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
+ }
1122
+ async prepare() {
1123
+ await this.loadEmbedder();
1124
+ }
1125
+ async embedText(text) {
1126
+ const embedder = await this.loadEmbedder();
1127
+ const ranges = makeTextRanges(text, embedder.tokenizer);
1128
+ const chunks = [];
1129
+ for (let offset = 0; offset < ranges.length; offset += 32) {
1130
+ const batchRanges = ranges.slice(offset, offset + 32);
1131
+ const output = await embedder(batchRanges.map((range) => text.slice(range.startOffset, range.endOffset)), { pooling: "mean", normalize: true });
1132
+ const batch = output.dims[0];
1133
+ const dimensions = output.dims[1];
1134
+ if (batch !== batchRanges.length || dimensions !== this.dimensions) {
1135
+ throw new Error(`Unexpected text embedding shape: ${output.dims.join("x")}`);
1136
+ }
1137
+ for (let index = 0; index < batch; index += 1) {
1138
+ const start = index * this.dimensions;
1139
+ chunks.push({
1140
+ startOffset: batchRanges[index].startOffset,
1141
+ endOffset: batchRanges[index].endOffset,
1142
+ vector: normalized(output.data.slice(start, start + this.dimensions)),
1143
+ });
1144
+ }
1145
+ }
1146
+ return chunks;
1147
+ }
1148
+ async loadEmbedder() {
1149
+ if (this.embedder)
1150
+ return this.embedder;
1151
+ const dataDir = this.context.config.dataDir;
1152
+ if (!dataDir)
1153
+ throw new Error("Text similarity requires a configured dataDir");
1154
+ const cacheDir = this.config.modelCacheDir ??
1155
+ this.sharedConfig.modelCacheDir ??
1156
+ path.join(dataDir, "similarity-models");
1157
+ await mkdir(cacheDir, { recursive: true });
1158
+ const modelId = this.config.modelId ?? DEFAULT_TEXT_MODEL_ID;
1159
+ const allowDownload = this.config.allowModelDownload ??
1160
+ this.sharedConfig.allowModelDownload;
1161
+ try {
1162
+ this.embedder = await pipeline("feature-extraction", modelId, {
1163
+ dtype: "q4",
1164
+ cache_dir: cacheDir,
1165
+ local_files_only: allowDownload === false,
1166
+ ...(modelId === DEFAULT_TEXT_MODEL_ID
1167
+ ? { revision: DEFAULT_TEXT_MODEL_REVISION }
1168
+ : {}),
1169
+ });
1170
+ return this.embedder;
1171
+ }
1172
+ catch (error) {
1173
+ throw new EngineFault({
1174
+ code: allowDownload === false ? "OFFLINE" : "FEATURE_UNAVAILABLE",
1175
+ message: `Unable to load local text similarity model: ${errorMessage(error)}`,
1176
+ });
1177
+ }
1178
+ }
1179
+ }
1180
+ function disabledSimilarityApi() {
1181
+ const unavailable = () => err({
1182
+ code: "FEATURE_UNAVAILABLE",
1183
+ message: "Similarity is disabled; configure EngineConfig.similarity to enable it",
1184
+ });
1185
+ return {
1186
+ prepare: async () => unavailable(),
1187
+ index: async () => unavailable(),
1188
+ rebuild: async () => unavailable(),
1189
+ status: () => unavailable(),
1190
+ stats: () => unavailable(),
1191
+ findSimilar: async () => unavailable(),
1192
+ findSimilarText: async () => unavailable(),
1193
+ };
1194
+ }
1195
+ function similarityKind(artifact) {
1196
+ if (artifact.kind === "image" ||
1197
+ artifact.kind === "video" ||
1198
+ artifact.kind === "audio") {
1199
+ return artifact.kind;
1200
+ }
1201
+ if (TEXT_ARTIFACT_KINDS.has(artifact.kind))
1202
+ return "text";
1203
+ throw new EngineFault({
1204
+ code: "INVALID_INPUT",
1205
+ message: `Similarity supports image, video, audio, and text artifacts, not ${artifact.kind}`,
1206
+ });
1207
+ }
1208
+ async function readNormalizedImage(sourcePath) {
1209
+ const decoded = await sharp(sourcePath, { animated: false })
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
+ }
1221
+ return new RawImage(decoded.data, decoded.info.width, decoded.info.height, 3);
1222
+ }
1223
+ async function probeDuration(ffprobePath, sourcePath) {
1224
+ const result = await runCommand(ffprobePath, [
1225
+ "-v",
1226
+ "error",
1227
+ "-show_entries",
1228
+ "format=duration",
1229
+ "-of",
1230
+ "default=noprint_wrappers=1:nokey=1",
1231
+ sourcePath,
1232
+ ]);
1233
+ const duration = Number.parseFloat(result.stdout.trim());
1234
+ if (!Number.isFinite(duration) || duration <= 0) {
1235
+ throw new EngineFault({
1236
+ code: "INVALID_INPUT",
1237
+ message: `Unable to determine a positive duration for ${sourcePath}`,
1238
+ });
1239
+ }
1240
+ return duration;
1241
+ }
1242
+ async function decodeAudioToMono(ffmpegPath, sourcePath, sampleRate, maxSamples) {
1243
+ const output = await runBinaryCommand(ffmpegPath, [
1244
+ "-hide_banner",
1245
+ "-loglevel",
1246
+ "error",
1247
+ "-nostdin",
1248
+ "-i",
1249
+ sourcePath,
1250
+ "-map",
1251
+ "0:a:0",
1252
+ "-vn",
1253
+ "-ac",
1254
+ "1",
1255
+ "-ar",
1256
+ String(sampleRate),
1257
+ "-t",
1258
+ String(maxSamples / sampleRate),
1259
+ "-f",
1260
+ "f32le",
1261
+ "-acodec",
1262
+ "pcm_f32le",
1263
+ "pipe:1",
1264
+ ]);
1265
+ if (output.stdout.byteLength === 0) {
1266
+ throw new EngineFault({
1267
+ code: "INVALID_INPUT",
1268
+ message: `No audio samples could be decoded from ${sourcePath}`,
1269
+ });
1270
+ }
1271
+ if (output.stdout.byteLength % Float32Array.BYTES_PER_ELEMENT !== 0) {
1272
+ throw new Error("FFmpeg returned an invalid PCM byte length");
1273
+ }
1274
+ const bytes = new Uint8Array(output.stdout.byteLength);
1275
+ bytes.set(output.stdout);
1276
+ const decoded = new Float32Array(bytes.buffer);
1277
+ return decoded.length > maxSamples
1278
+ ? decoded.slice(0, maxSamples)
1279
+ : decoded;
1280
+ }
1281
+ async function runCommand(command, args) {
1282
+ const output = await runBinaryCommand(command, args);
1283
+ return {
1284
+ stdout: output.stdout.toString("utf8"),
1285
+ stderr: output.stderr,
1286
+ };
1287
+ }
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
+ function normalizePlainText(raw) {
1318
+ const text = raw
1319
+ .replace(/^\uFEFF/, "")
1320
+ .normalize("NFC")
1321
+ .replace(/\r\n?/g, "\n")
1322
+ .replace(/[ \t]+$/gm, "")
1323
+ .replace(/\n{3,}/g, "\n\n")
1324
+ .trim();
1325
+ return { text, contentHash: hashText(text) };
1326
+ }
1327
+ function normalizeJsonText(raw, sourcePath) {
1328
+ let value;
1329
+ try {
1330
+ value = JSON.parse(raw.replace(/^\uFEFF/, ""));
1331
+ }
1332
+ catch (error) {
1333
+ throw new EngineFault({
1334
+ code: "INVALID_INPUT",
1335
+ message: `Invalid JSON source ${sourcePath}: ${errorMessage(error)}`,
1336
+ });
1337
+ }
1338
+ const lines = [];
1339
+ appendJsonLines(lines, "$", value, 0);
1340
+ return normalizePlainText(lines.join("\n"));
1341
+ }
1342
+ function appendJsonLines(lines, jsonPath, value, depth) {
1343
+ if (depth > 100) {
1344
+ throw new EngineFault({
1345
+ code: "INVALID_INPUT",
1346
+ message: "JSON source exceeds the maximum nesting depth",
1347
+ });
1348
+ }
1349
+ if (Array.isArray(value)) {
1350
+ if (value.length === 0) {
1351
+ lines.push(`${jsonPath}: []`);
1352
+ return;
1353
+ }
1354
+ value.forEach((item, index) => appendJsonLines(lines, `${jsonPath}[${index}]`, item, depth + 1));
1355
+ return;
1356
+ }
1357
+ if (value !== null && typeof value === "object") {
1358
+ const entries = Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
1359
+ if (entries.length === 0) {
1360
+ lines.push(`${jsonPath}: {}`);
1361
+ return;
1362
+ }
1363
+ for (const [key, item] of entries) {
1364
+ appendJsonLines(lines, `${jsonPath}.${key}`, item, depth + 1);
1365
+ }
1366
+ return;
1367
+ }
1368
+ lines.push(`${jsonPath}: ${JSON.stringify(value)}`);
1369
+ }
1370
+ function makeTextRanges(text, tokenizer) {
1371
+ const ranges = [];
1372
+ let startOffset = 0;
1373
+ while (startOffset < text.length) {
1374
+ let upper = Math.min(text.length, startOffset + TEXT_CHUNK_CHAR_WINDOW);
1375
+ let low = startOffset + 1;
1376
+ let high = upper;
1377
+ while (low < high) {
1378
+ const middle = Math.ceil((low + high) / 2);
1379
+ if (tokenCount(tokenizer, text.slice(startOffset, middle)) <= TEXT_CHUNK_TOKENS) {
1380
+ low = middle;
1381
+ }
1382
+ else {
1383
+ high = middle - 1;
1384
+ }
1385
+ }
1386
+ upper = low;
1387
+ const boundary = preferredBoundary(text, startOffset, upper);
1388
+ if (boundary > startOffset &&
1389
+ tokenCount(tokenizer, text.slice(startOffset, boundary)) <= TEXT_CHUNK_TOKENS) {
1390
+ upper = boundary;
1391
+ }
1392
+ ranges.push({ startOffset, endOffset: upper });
1393
+ if (upper >= text.length)
1394
+ break;
1395
+ const overlapStart = findOverlapStart(tokenizer, text, startOffset, upper);
1396
+ startOffset = overlapStart <= startOffset
1397
+ ? upper
1398
+ : Math.max(startOffset + 1, Math.min(overlapStart, upper - 1));
1399
+ }
1400
+ return ranges;
1401
+ }
1402
+ function tokenCount(tokenizer, text) {
1403
+ return tokenizer.encode(text, { add_special_tokens: false }).length;
1404
+ }
1405
+ function preferredBoundary(text, start, end) {
1406
+ const floor = Math.max(start + 1, end - 180);
1407
+ for (let index = end; index >= floor; index -= 1) {
1408
+ const character = text[index - 1];
1409
+ if (character === "\n" || character === " " || character === "\t") {
1410
+ return index;
1411
+ }
1412
+ }
1413
+ return end;
1414
+ }
1415
+ function findOverlapStart(tokenizer, text, start, end) {
1416
+ let low = start;
1417
+ let high = end - 1;
1418
+ while (low < high) {
1419
+ const middle = Math.floor((low + high) / 2);
1420
+ if (tokenCount(tokenizer, text.slice(middle, end)) > TEXT_OVERLAP_TOKENS) {
1421
+ low = middle + 1;
1422
+ }
1423
+ else {
1424
+ high = middle;
1425
+ }
1426
+ }
1427
+ const boundary = preferredBoundary(text, low, end);
1428
+ return boundary > start && boundary < end ? boundary : low;
1429
+ }
1430
+ function hashText(text) {
1431
+ return createHash("sha256").update(text, "utf8").digest("hex");
1432
+ }
1433
+ function normalized(vector) {
1434
+ const norm = Math.hypot(...vector);
1435
+ if (!Number.isFinite(norm) || norm === 0) {
1436
+ throw new Error("Embedding vector must have a finite non-zero norm");
1437
+ }
1438
+ const result = new Float32Array(vector.length);
1439
+ for (let index = 0; index < vector.length; index += 1) {
1440
+ result[index] = vector[index] / norm;
1441
+ }
1442
+ return result;
1443
+ }
1444
+ function normalizedCentroid(vectors) {
1445
+ const first = vectors[0];
1446
+ if (!first)
1447
+ throw new Error("Cannot pool an empty vector collection");
1448
+ const centroid = new Float32Array(first.length);
1449
+ for (const vector of vectors) {
1450
+ if (vector.length !== centroid.length) {
1451
+ throw new Error("Video frame embedding dimensions do not match");
1452
+ }
1453
+ for (let index = 0; index < vector.length; index += 1) {
1454
+ centroid[index] = (centroid[index] ?? 0) + vector[index];
1455
+ }
1456
+ }
1457
+ return normalized(centroid);
1458
+ }
1459
+ function vectorToBlob(vector) {
1460
+ return Buffer.from(new Uint8Array(vector.buffer, vector.byteOffset, vector.byteLength));
1461
+ }
1462
+ function vectorFromBlob(row) {
1463
+ const bytes = row.vector_blob;
1464
+ if (bytes.byteLength !== row.dimensions * Float32Array.BYTES_PER_ELEMENT) {
1465
+ throw new Error("Stored similarity vector has an invalid byte length");
1466
+ }
1467
+ const copied = new Uint8Array(bytes.byteLength);
1468
+ copied.set(bytes);
1469
+ return new Float32Array(copied.buffer);
1470
+ }
1471
+ function cosine(left, right) {
1472
+ if (left.length !== right.length) {
1473
+ throw new Error("Cannot compare vectors with different dimensions");
1474
+ }
1475
+ let score = 0;
1476
+ for (let index = 0; index < left.length; index += 1) {
1477
+ score += left[index] * right[index];
1478
+ }
1479
+ return Math.max(-1, Math.min(1, score));
1480
+ }
1481
+ function excerpt(text) {
1482
+ return text.length <= TEXT_EXCERPT_LIMIT
1483
+ ? text
1484
+ : `${text.slice(0, TEXT_EXCERPT_LIMIT - 1)}…`;
1485
+ }
1486
+ function checkedLimit(value) {
1487
+ const limit = value ?? 20;
1488
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
1489
+ throw new EngineFault({
1490
+ code: "INVALID_INPUT",
1491
+ message: "Similarity limit must be an integer between 1 and 100",
1492
+ });
1493
+ }
1494
+ return limit;
1495
+ }
1496
+ function checkedMinScore(value) {
1497
+ if (value === undefined)
1498
+ return undefined;
1499
+ if (!Number.isFinite(value) || value < -1 || value > 1) {
1500
+ throw new EngineFault({
1501
+ code: "INVALID_INPUT",
1502
+ message: "Similarity minScore must be between -1 and 1",
1503
+ });
1504
+ }
1505
+ return value;
1506
+ }
1507
+ function checkedAudioSampleRate(value) {
1508
+ const sampleRate = value ?? DEFAULT_AUDIO_SAMPLE_RATE;
1509
+ if (!Number.isInteger(sampleRate) ||
1510
+ sampleRate < 8_000 ||
1511
+ sampleRate > 192_000) {
1512
+ throw new Error("Audio model sampling rate must be an integer between 8000 and 192000");
1513
+ }
1514
+ return sampleRate;
1515
+ }
1516
+ function checkedAudioMaxSamples(value, sampleRate) {
1517
+ const maxSamples = value ?? DEFAULT_AUDIO_MAX_SAMPLES;
1518
+ if (!Number.isInteger(maxSamples) ||
1519
+ maxSamples < sampleRate ||
1520
+ maxSamples > sampleRate * 60) {
1521
+ throw new Error("Audio model window must be between 1 and 60 seconds");
1522
+ }
1523
+ return maxSamples;
1524
+ }
1525
+ function checkedTextMaxBytes(config) {
1526
+ const value = config?.maxSourceBytes ?? DEFAULT_TEXT_MAX_BYTES;
1527
+ if (!Number.isInteger(value) || value < 1 || value > 64 * 1024 * 1024) {
1528
+ throw new EngineFault({
1529
+ code: "INVALID_INPUT",
1530
+ message: "Text maxSourceBytes must be an integer between 1 and 67108864",
1531
+ });
1532
+ }
1533
+ return value;
1534
+ }
1535
+ function checkedTextMaxChunks(config) {
1536
+ const value = config?.maxChunks ?? DEFAULT_TEXT_MAX_CHUNKS;
1537
+ if (!Number.isInteger(value) || value < 1 || value > 2048) {
1538
+ throw new EngineFault({
1539
+ code: "INVALID_INPUT",
1540
+ message: "Text maxChunks must be an integer between 1 and 2048",
1541
+ });
1542
+ }
1543
+ return value;
1544
+ }
1545
+ function indexKey(kind, embeddingSpace) {
1546
+ return `${kind}\u0000${embeddingSpace}`;
1547
+ }
1548
+ function errorMessage(error) {
1549
+ return error instanceof Error ? error.message : String(error);
1550
+ }
1551
+ //# sourceMappingURL=similarity.js.map