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.
Files changed (244) hide show
  1. package/README.md +84 -39
  2. package/dist/artifacts.d.ts +5 -10
  3. package/dist/artifacts.d.ts.map +1 -1
  4. package/dist/artifacts.js +79 -142
  5. package/dist/artifacts.js.map +1 -1
  6. package/dist/books.d.ts +1 -1
  7. package/dist/books.d.ts.map +1 -1
  8. package/dist/books.js +16 -9
  9. package/dist/books.js.map +1 -1
  10. package/dist/cas.d.ts +2 -1
  11. package/dist/cas.d.ts.map +1 -1
  12. package/dist/cas.js +33 -4
  13. package/dist/cas.js.map +1 -1
  14. package/dist/catalog-gc-state.d.ts +4 -0
  15. package/dist/catalog-gc-state.d.ts.map +1 -0
  16. package/dist/catalog-gc-state.js +49 -0
  17. package/dist/catalog-gc-state.js.map +1 -0
  18. package/dist/catalog-metadata.d.ts +2 -2
  19. package/dist/catalog-metadata.js +3 -3
  20. package/dist/catalog-metadata.js.map +1 -1
  21. package/dist/context.d.ts +5 -5
  22. package/dist/context.d.ts.map +1 -1
  23. package/dist/context.js +17 -49
  24. package/dist/context.js.map +1 -1
  25. package/dist/domain.d.ts +35 -2
  26. package/dist/domain.d.ts.map +1 -1
  27. package/dist/domain.js +131 -161
  28. package/dist/domain.js.map +1 -1
  29. package/dist/edits.js +2 -2
  30. package/dist/edits.js.map +1 -1
  31. package/dist/engine-types.d.ts +159 -39
  32. package/dist/engine-types.d.ts.map +1 -1
  33. package/dist/engine-types.js +2 -1
  34. package/dist/engine-types.js.map +1 -1
  35. package/dist/engine.d.ts +30 -20
  36. package/dist/engine.d.ts.map +1 -1
  37. package/dist/engine.js +46 -4
  38. package/dist/engine.js.map +1 -1
  39. package/dist/files.d.ts +10 -0
  40. package/dist/files.d.ts.map +1 -1
  41. package/dist/files.js +49 -2
  42. package/dist/files.js.map +1 -1
  43. package/dist/fork.d.ts +14 -30
  44. package/dist/fork.d.ts.map +1 -1
  45. package/dist/fork.js +18 -27
  46. package/dist/fork.js.map +1 -1
  47. package/dist/generations.d.ts +9 -0
  48. package/dist/generations.d.ts.map +1 -0
  49. package/dist/generations.js +175 -0
  50. package/dist/generations.js.map +1 -0
  51. package/dist/history.d.ts +2 -1
  52. package/dist/history.d.ts.map +1 -1
  53. package/dist/history.js +30 -57
  54. package/dist/history.js.map +1 -1
  55. package/dist/index.d.ts +8 -6
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +5 -5
  58. package/dist/index.js.map +1 -1
  59. package/dist/inline-temporal-models.d.ts +43 -0
  60. package/dist/inline-temporal-models.d.ts.map +1 -0
  61. package/dist/inline-temporal-models.js +236 -0
  62. package/dist/inline-temporal-models.js.map +1 -0
  63. package/dist/isolated-models.d.ts +8 -0
  64. package/dist/isolated-models.d.ts.map +1 -0
  65. package/dist/isolated-models.js +25 -0
  66. package/dist/isolated-models.js.map +1 -0
  67. package/dist/job-queue.d.ts +1 -1
  68. package/dist/job-queue.d.ts.map +1 -1
  69. package/dist/job-queue.js +20 -6
  70. package/dist/job-queue.js.map +1 -1
  71. package/dist/media-image.d.ts +8 -0
  72. package/dist/media-image.d.ts.map +1 -0
  73. package/dist/media-image.js +41 -0
  74. package/dist/media-image.js.map +1 -0
  75. package/dist/media-process.d.ts +13 -0
  76. package/dist/media-process.d.ts.map +1 -0
  77. package/dist/media-process.js +89 -0
  78. package/dist/media-process.js.map +1 -0
  79. package/dist/media.d.ts +3 -3
  80. package/dist/media.d.ts.map +1 -1
  81. package/dist/media.js +5 -5
  82. package/dist/media.js.map +1 -1
  83. package/dist/merge-policy.d.ts +16 -36
  84. package/dist/merge-policy.d.ts.map +1 -1
  85. package/dist/merge-policy.js +13 -125
  86. package/dist/merge-policy.js.map +1 -1
  87. package/dist/metadata.d.ts +1 -0
  88. package/dist/metadata.d.ts.map +1 -1
  89. package/dist/metadata.js +13 -3
  90. package/dist/metadata.js.map +1 -1
  91. package/dist/migrate-grid-text.d.ts +17 -0
  92. package/dist/migrate-grid-text.d.ts.map +1 -0
  93. package/dist/migrate-grid-text.js +117 -0
  94. package/dist/migrate-grid-text.js.map +1 -0
  95. package/dist/migrate-grid-v22.d.ts +12 -0
  96. package/dist/migrate-grid-v22.d.ts.map +1 -0
  97. package/dist/migrate-grid-v22.js +166 -0
  98. package/dist/migrate-grid-v22.js.map +1 -0
  99. package/dist/migrate-grid-v23.d.ts +10 -0
  100. package/dist/migrate-grid-v23.d.ts.map +1 -0
  101. package/dist/migrate-grid-v23.js +31 -0
  102. package/dist/migrate-grid-v23.js.map +1 -0
  103. package/dist/migration-notebooks.d.ts +25 -0
  104. package/dist/migration-notebooks.d.ts.map +1 -0
  105. package/dist/migration-notebooks.js +131 -0
  106. package/dist/migration-notebooks.js.map +1 -0
  107. package/dist/migration-source.d.ts +6 -0
  108. package/dist/migration-source.d.ts.map +1 -0
  109. package/dist/migration-source.js +37 -0
  110. package/dist/migration-source.js.map +1 -0
  111. package/dist/migration-timeline.d.ts +36 -0
  112. package/dist/migration-timeline.d.ts.map +1 -0
  113. package/dist/migration-timeline.js +187 -0
  114. package/dist/migration-timeline.js.map +1 -0
  115. package/dist/migration-validation.d.ts +12 -0
  116. package/dist/migration-validation.d.ts.map +1 -0
  117. package/dist/migration-validation.js +68 -0
  118. package/dist/migration-validation.js.map +1 -0
  119. package/dist/migration.d.ts.map +1 -1
  120. package/dist/migration.js +220 -119
  121. package/dist/migration.js.map +1 -1
  122. package/dist/model-cache-paths.d.ts +2 -0
  123. package/dist/model-cache-paths.d.ts.map +1 -0
  124. package/dist/model-cache-paths.js +5 -0
  125. package/dist/model-cache-paths.js.map +1 -0
  126. package/dist/model-checksums.json +338 -0
  127. package/dist/model-file-resolver.d.ts +30 -0
  128. package/dist/model-file-resolver.d.ts.map +1 -0
  129. package/dist/model-file-resolver.js +304 -0
  130. package/dist/model-file-resolver.js.map +1 -0
  131. package/dist/model-identity.d.ts +13 -0
  132. package/dist/model-identity.d.ts.map +1 -0
  133. package/dist/model-identity.js +44 -0
  134. package/dist/model-identity.js.map +1 -0
  135. package/dist/model-worker-errors.d.ts +3 -0
  136. package/dist/model-worker-errors.d.ts.map +1 -0
  137. package/dist/model-worker-errors.js +12 -0
  138. package/dist/model-worker-errors.js.map +1 -0
  139. package/dist/model-worker-pool.d.ts +32 -0
  140. package/dist/model-worker-pool.d.ts.map +1 -0
  141. package/dist/model-worker-pool.js +269 -0
  142. package/dist/model-worker-pool.js.map +1 -0
  143. package/dist/model-worker-protocol.d.ts +42 -0
  144. package/dist/model-worker-protocol.d.ts.map +1 -0
  145. package/dist/model-worker-protocol.js +2 -0
  146. package/dist/model-worker-protocol.js.map +1 -0
  147. package/dist/model-worker.d.ts +2 -0
  148. package/dist/model-worker.d.ts.map +1 -0
  149. package/dist/model-worker.js +91 -0
  150. package/dist/model-worker.js.map +1 -0
  151. package/dist/mvp-contract-fixtures.js +1 -1
  152. package/dist/mvp-contract-fixtures.js.map +1 -1
  153. package/dist/mvp-contracts.d.ts +32 -8
  154. package/dist/mvp-contracts.d.ts.map +1 -1
  155. package/dist/mvp-contracts.js +1 -1
  156. package/dist/mvp-contracts.js.map +1 -1
  157. package/dist/notebook/types.d.ts +1 -29
  158. package/dist/notebook/types.d.ts.map +1 -1
  159. package/dist/notebook-grid.d.ts +46 -0
  160. package/dist/notebook-grid.d.ts.map +1 -0
  161. package/dist/notebook-grid.js +187 -0
  162. package/dist/notebook-grid.js.map +1 -0
  163. package/dist/notebook-media-artifact.d.ts +23 -0
  164. package/dist/notebook-media-artifact.d.ts.map +1 -0
  165. package/dist/notebook-media-artifact.js +67 -0
  166. package/dist/notebook-media-artifact.js.map +1 -0
  167. package/dist/notebook-mentions.d.ts +20 -0
  168. package/dist/notebook-mentions.d.ts.map +1 -0
  169. package/dist/notebook-mentions.js +54 -0
  170. package/dist/notebook-mentions.js.map +1 -0
  171. package/dist/runtime-services.js +4 -9
  172. package/dist/runtime-services.js.map +1 -1
  173. package/dist/schema.d.ts +4 -19
  174. package/dist/schema.d.ts.map +1 -1
  175. package/dist/schema.js +66 -92
  176. package/dist/schema.js.map +1 -1
  177. package/dist/search-provider-access.d.ts +11 -0
  178. package/dist/search-provider-access.d.ts.map +1 -0
  179. package/dist/search-provider-access.js +42 -0
  180. package/dist/search-provider-access.js.map +1 -0
  181. package/dist/similarity.d.ts +60 -1
  182. package/dist/similarity.d.ts.map +1 -1
  183. package/dist/similarity.js +174 -154
  184. package/dist/similarity.js.map +1 -1
  185. package/dist/status.js +8 -10
  186. package/dist/status.js.map +1 -1
  187. package/dist/store.d.ts +16 -2
  188. package/dist/store.d.ts.map +1 -1
  189. package/dist/store.js +171 -17
  190. package/dist/store.js.map +1 -1
  191. package/dist/temporal-index-cache.d.ts +13 -0
  192. package/dist/temporal-index-cache.d.ts.map +1 -0
  193. package/dist/temporal-index-cache.js +59 -0
  194. package/dist/temporal-index-cache.js.map +1 -0
  195. package/dist/temporal-model-manifests.d.ts +17 -0
  196. package/dist/temporal-model-manifests.d.ts.map +1 -0
  197. package/dist/temporal-model-manifests.js +33 -0
  198. package/dist/temporal-model-manifests.js.map +1 -0
  199. package/dist/temporal-models.d.ts +20 -35
  200. package/dist/temporal-models.d.ts.map +1 -1
  201. package/dist/temporal-models.js +23 -257
  202. package/dist/temporal-models.js.map +1 -1
  203. package/dist/temporal-search.d.ts +6 -3
  204. package/dist/temporal-search.d.ts.map +1 -1
  205. package/dist/temporal-search.js +438 -122
  206. package/dist/temporal-search.js.map +1 -1
  207. package/dist/temporal-vector-index.d.ts +46 -0
  208. package/dist/temporal-vector-index.d.ts.map +1 -0
  209. package/dist/temporal-vector-index.js +220 -0
  210. package/dist/temporal-vector-index.js.map +1 -0
  211. package/dist/third-party/@huggingface-jinja.LICENSE +21 -0
  212. package/dist/third-party/@huggingface-tokenizers.LICENSE +201 -0
  213. package/dist/third-party/@huggingface-transformers.LICENSE +202 -0
  214. package/dist/third-party/onnxruntime-common.LICENSE +21 -0
  215. package/dist/third-party/versions.json +23 -0
  216. package/dist/transformers-runtime.d.ts +2 -0
  217. package/dist/transformers-runtime.d.ts.map +1 -0
  218. package/dist/transformers-runtime.js +33014 -0
  219. package/dist/transformers-runtime.js.LEGAL.txt +12 -0
  220. package/dist/transformers-runtime.js.map +7 -0
  221. package/docs/doltlite-staging.md +69 -0
  222. package/docs/edit-performance.md +83 -0
  223. package/docs/engine-layout.md +56 -50
  224. package/docs/media-limits.md +57 -0
  225. package/docs/model-download-policy.md +15 -0
  226. package/docs/model-integrity.md +72 -0
  227. package/docs/model-isolation.md +72 -0
  228. package/docs/model-revisions.md +54 -0
  229. package/docs/mvp-prd.md +2 -2
  230. package/docs/release-evidence.md +300 -0
  231. package/docs/search-provider-consent.md +67 -0
  232. package/docs/semantic-durability.md +55 -0
  233. package/docs/temporal-search-performance.md +260 -0
  234. package/docs/v4-migration.md +83 -0
  235. package/fixtures/v5/contract-fixtures.json +4 -4
  236. package/package.json +31 -13
  237. package/dist/artifact-names.d.ts +0 -6
  238. package/dist/artifact-names.d.ts.map +0 -1
  239. package/dist/artifact-names.js +0 -56
  240. package/dist/artifact-names.js.map +0 -1
  241. package/dist/resolver.d.ts +0 -15
  242. package/dist/resolver.d.ts.map +0 -1
  243. package/dist/resolver.js +0 -81
  244. package/dist/resolver.js.map +0 -1
@@ -1,7 +1,28 @@
1
+ import { guardSearchProvider } from "./search-provider-access.js";
1
2
  import { createHash } from "node:crypto";
3
+ import { join } from "node:path";
2
4
  import { normalizeSourcePoint, normalizeSourceRange, rationalEquals, sourceRangeEndTick, } from "./mvp-time.js";
3
5
  import { resultOf, syncResultOf } from "./context.js";
4
6
  import { canonicalJson, EngineFault, parseJson } from "./store.js";
7
+ import { TemporalVectorIndex } from "./temporal-vector-index.js";
8
+ const retrievalStates = new WeakMap();
9
+ const VECTOR_CANDIDATES = 1_000;
10
+ const continuityPrefixes = new WeakMap();
11
+ const SEGMENT_SELECT = `SELECT s.segment_id, s.artifact_id, a.label AS artifact_label,
12
+ a.kind AS artifact_kind, a.created_at AS artifact_created_at,
13
+ s.stream_id, s.object_hash, s.source_range_json, s.source_path, s.segment_kind,
14
+ s.representative_tick, s.generation, s.manifest_id
15
+ FROM runtime_media_segments s JOIN artifacts a ON a.artifact_id=s.artifact_id`;
16
+ export function clearTemporalSearchCache(context) {
17
+ const state = retrievalStates.get(context);
18
+ retrievalStates.delete(context);
19
+ for (const index of state?.vectors.values() ?? []) {
20
+ try {
21
+ index.dispose();
22
+ }
23
+ catch { /* Cache writes cannot prevent closing the catalog. */ }
24
+ }
25
+ }
5
26
  const INDEX_PHASES = [
6
27
  "probe",
7
28
  "segment",
@@ -14,13 +35,28 @@ const INDEX_PHASES = [
14
35
  ];
15
36
  export function createTemporalSearchApi(context) {
16
37
  const providers = new Map();
38
+ const registrations = new Map();
17
39
  return {
18
40
  providers: {
19
- register: (provider) => {
20
- requiredText(provider.manifestId, "Provider manifest ID");
21
- providers.set(provider.manifestId, provider);
41
+ register: (provider, consent = {}) => {
42
+ const id = requiredText(provider.manifestId, "Provider manifest ID");
43
+ const existing = registrations.get(id);
44
+ if (existing?.matches(provider, consent)) {
45
+ existing.validate();
46
+ return;
47
+ }
48
+ let registered = false;
49
+ const guarded = guardSearchProvider(provider, consent, () => !registered || providers.get(id) === guarded.provider);
50
+ guarded.validate();
51
+ providers.set(id, guarded.provider);
52
+ registrations.set(id, guarded);
53
+ registered = true;
22
54
  },
23
55
  list: () => [...providers.keys()].sort(),
56
+ unregister: (manifestId) => {
57
+ registrations.delete(manifestId);
58
+ return providers.delete(manifestId);
59
+ },
24
60
  },
25
61
  manifests: {
26
62
  register: (manifest) => syncResultOf(() => registerManifest(context, manifest)),
@@ -31,8 +67,9 @@ export function createTemporalSearchApi(context) {
31
67
  commitBatch: (input) => syncResultOf(() => commitIndexBatch(context, input)),
32
68
  activate: (manifestId, generation) => syncResultOf(() => activateGeneration(context, manifestId, generation)),
33
69
  coverage: (artifact) => syncResultOf(() => searchCoverage(context, artifact)),
34
- query: (query) => queryTemporalIndex(context, providers, query),
35
- queryPrepared: (query, reference, options = {}) => queryTemporalIndex(context, providers, query, { reference, options }),
70
+ prepare: (options = {}) => context.withLocalMedia(() => resultOf(() => prepareTemporalIndexes(context, options))),
71
+ query: (query) => context.withLocalMedia(() => queryTemporalIndex(context, providers, query)),
72
+ queryPrepared: (query, reference, options = {}) => context.withLocalMedia(() => queryTemporalIndex(context, providers, query, { reference, options })),
36
73
  invalidate: (artifact, objectHash) => syncResultOf(() => invalidateCoverage(context, artifact, objectHash)),
37
74
  cleanup: () => syncResultOf(() => cleanupRetired(context)),
38
75
  stats: () => temporalStats(context),
@@ -226,6 +263,11 @@ function commitIndexBatch(context, input) {
226
263
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
227
264
  .run(batchKey, input.artifactId, input.objectHash, input.manifestId, input.generation, input.phase, input.cursor ?? null, canonicalJson(result), now);
228
265
  });
266
+ const cache = retrievalStates.get(context);
267
+ if (cache?.generations.has(input.generation)) {
268
+ for (const observation of observations)
269
+ cache.pending.add(observation.segmentId);
270
+ }
229
271
  return result;
230
272
  }
231
273
  function normalizeObservation(context, manifest, batch, input, index, streamCache) {
@@ -445,7 +487,8 @@ async function queryTemporalIndex(context, providers, query, prepared) {
445
487
  const active = activeGenerations(context);
446
488
  const generationKey = hashJson(active.map((item) => [item.embedding_space, item.generation]));
447
489
  const offset = decodeCursor(query.cursor, generationKey);
448
- const segments = querySegments(context, active, query);
490
+ const orderedVideo = prepared?.reference.kind === "video" || query.reference?.kind === "video";
491
+ const segments = querySegments(context, active, orderedVideo ? { ...query, durationMs: undefined } : query);
449
492
  const candidates = new Map(segments.map((segment) => [
450
493
  segment.segment_id,
451
494
  { segment, signals: [], score: 0 },
@@ -468,7 +511,8 @@ async function queryTemporalIndex(context, providers, query, prepared) {
468
511
  .filter((candidate) => (!query.sourceArtifactIds
469
512
  || query.sourceArtifactIds.includes(candidate.segment.artifact_id))
470
513
  && (!query.artifactKinds
471
- || query.artifactKinds.includes(candidate.segment.artifact_kind)))
514
+ || query.artifactKinds.includes(candidate.segment.artifact_kind))
515
+ && segmentMatchesDuration(candidate.segment, query.durationMs))
472
516
  .map((candidate) => ({
473
517
  ...candidate,
474
518
  signals: candidate.signals.filter((signal) => allowsSignal(query.modalities, signal.kind)),
@@ -519,22 +563,200 @@ function activeGenerations(context) {
519
563
  ORDER BY embedding_space, generation`)
520
564
  .all();
521
565
  }
566
+ async function prepareTemporalIndexes(context, options) {
567
+ const active = activeGenerations(context);
568
+ const selected = active.filter((generation) => (!options.manifestId || generation.manifest_id === options.manifestId)
569
+ && (!options.generation || generation.generation === options.generation));
570
+ if ((options.manifestId || options.generation) && selected.length === 0) {
571
+ throw new EngineFault({ code: "NOT_READY", message: "Activate a generation before preparing its search index" });
572
+ }
573
+ const state = retrievalState(context, active, false);
574
+ const result = { indexes: 0, vectors: 0, updatedVectors: 0, loadedIndexes: 0, persistedIndexes: 0 };
575
+ for (const generation of selected) {
576
+ for (const modality of requiredManifest(context, generation.manifest_id).modalities) {
577
+ if (modality === "metadata")
578
+ continue;
579
+ const prepared = await vectorIndex(context, generation, modality, state).prepare(options.signal, options.checkpoint === "periodic");
580
+ result.indexes++;
581
+ result.vectors += prepared.vectors;
582
+ result.updatedVectors += prepared.updatedVectors;
583
+ result.loadedIndexes += Number(prepared.loaded);
584
+ result.persistedIndexes += Number(prepared.persisted);
585
+ }
586
+ }
587
+ return result;
588
+ }
589
+ function retrievalState(context, active = activeGenerations(context), refreshMetadata = true) {
590
+ const generationKey = canonicalJson(active.map((item) => [item.generation, item.embedding_space, item.manifest_id]));
591
+ let state = retrievalStates.get(context);
592
+ if (!state || state.generationKey !== generationKey) {
593
+ const generations = [...new Set(active.map((item) => item.generation))];
594
+ const rows = generations.length === 0 ? [] : context.store.db.prepare(`${SEGMENT_SELECT} WHERE s.generation IN (${generations.map(() => "?").join(",")}) ORDER BY s.segment_id`).all(...generations);
595
+ const segments = new Map(rows.map((row) => [row.segment_id, row]));
596
+ const vectors = state?.vectors ?? new Map();
597
+ for (const [key, index] of vectors) {
598
+ const [generation, space] = JSON.parse(key);
599
+ if (!active.some((item) => item.generation === generation && item.embedding_space === space)) {
600
+ try {
601
+ index.dispose();
602
+ }
603
+ catch { /* Retired cache persistence is optional. */ }
604
+ vectors.delete(key);
605
+ continue;
606
+ }
607
+ for (const [segmentId, entry] of index.entries) {
608
+ const segment = segments.get(segmentId);
609
+ if (segment)
610
+ entry.value.segment = segment;
611
+ else
612
+ index.delete(segmentId);
613
+ }
614
+ }
615
+ state = {
616
+ semanticVersion: context.store.semanticVersion, generationKey, generations: new Set(generations),
617
+ segments, vectors, ordered: new Map(), pending: state?.pending ?? new Set(),
618
+ };
619
+ retrievalStates.set(context, state);
620
+ }
621
+ if (refreshMetadata && state.semanticVersion !== context.store.semanticVersion) {
622
+ const generations = [...state.generations];
623
+ const rows = generations.length === 0 ? [] : context.store.db.prepare(`${SEGMENT_SELECT} WHERE s.generation IN (${generations.map(() => "?").join(",")}) ORDER BY s.segment_id`).all(...generations);
624
+ state.segments = new Map(rows.map((row) => [row.segment_id, row]));
625
+ for (const [key, index] of state.vectors) {
626
+ for (const [segmentId, entry] of index.entries) {
627
+ const segment = state.segments.get(segmentId);
628
+ if (segment)
629
+ entry.value.segment = segment;
630
+ else
631
+ index.delete(segmentId);
632
+ }
633
+ if (index.entries.size === 0) {
634
+ try {
635
+ index.dispose();
636
+ }
637
+ catch { /* Empty cache persistence is optional. */ }
638
+ state.vectors.delete(key);
639
+ }
640
+ }
641
+ state.texts = undefined;
642
+ state.ordered.clear();
643
+ state.semanticVersion = context.store.semanticVersion;
644
+ }
645
+ if (state.pending.size > 0)
646
+ refreshRetrievalState(context, state);
647
+ return state;
648
+ }
649
+ function refreshRetrievalState(context, state) {
650
+ const pending = [...state.pending];
651
+ for (let start = 0; start < pending.length; start += 400) {
652
+ const keys = pending.slice(start, start + 400);
653
+ const placeholders = keys.map(() => "?").join(",");
654
+ for (const key of keys) {
655
+ state.segments.delete(key);
656
+ state.texts?.delete(key);
657
+ for (const index of state.vectors.values())
658
+ index.delete(key);
659
+ }
660
+ const rows = context.store.db.prepare(`${SEGMENT_SELECT} WHERE s.segment_id IN (${placeholders}) ORDER BY s.segment_id`).all(...keys);
661
+ for (const row of rows) {
662
+ if (state.generations.has(row.generation))
663
+ state.segments.set(row.segment_id, row);
664
+ }
665
+ if (state.texts) {
666
+ const texts = context.store.db.prepare(`SELECT segment_id, kind, text FROM runtime_segment_text
667
+ WHERE segment_id IN (${placeholders}) ORDER BY segment_id, kind, text`).all(...keys);
668
+ for (const row of texts)
669
+ addCachedText(state.texts, row);
670
+ }
671
+ for (const [key, index] of state.vectors) {
672
+ const [generation, embeddingSpace, modality] = JSON.parse(key);
673
+ const vectors = context.store.db.prepare(`SELECT segment_id, dimensions, vector_blob FROM runtime_segment_embeddings
674
+ WHERE generation=? AND embedding_space=? AND modality=?
675
+ AND segment_id IN (${placeholders}) ORDER BY segment_id`).all(generation, embeddingSpace, modality, ...keys);
676
+ for (const row of vectors)
677
+ addCachedVector(state, index, row);
678
+ }
679
+ }
680
+ state.ordered.clear();
681
+ state.pending.clear();
682
+ }
683
+ function cachedTextRows(context, active) {
684
+ const state = retrievalState(context, active);
685
+ if (!state.texts) {
686
+ state.texts = new Map();
687
+ const generations = [...state.generations];
688
+ const rows = generations.length === 0 ? [] : context.store.db.prepare(`SELECT segment_id, kind, text FROM runtime_segment_text
689
+ WHERE generation IN (${generations.map(() => "?").join(",")}) ORDER BY segment_id, kind, text`).all(...generations);
690
+ for (const row of rows)
691
+ addCachedText(state.texts, row);
692
+ }
693
+ return [...state.texts.values()].flat();
694
+ }
695
+ function addCachedText(texts, row) {
696
+ const rows = texts.get(row.segment_id) ?? [];
697
+ rows.push(row);
698
+ texts.set(row.segment_id, rows);
699
+ }
700
+ function addCachedVector(state, index, row) {
701
+ const segment = state.segments.get(row.segment_id);
702
+ if (!segment)
703
+ return;
704
+ if (row.dimensions !== index.dimensions) {
705
+ throw new EngineFault({ code: "MANIFEST_INCOMPATIBLE", message: "Stored vector dimensions do not match the index manifest" });
706
+ }
707
+ const vector = blobToVector(row.vector_blob, row.dimensions);
708
+ index.set(row.segment_id, vector, { segment, vector, range: segmentRange(segment) });
709
+ }
710
+ function vectorIndex(context, generation, modality, state = retrievalState(context)) {
711
+ const key = canonicalJson([generation.generation, generation.embedding_space, modality]);
712
+ const cached = state.vectors.get(key);
713
+ if (cached)
714
+ return cached;
715
+ const manifest = requiredManifest(context, generation.manifest_id);
716
+ const identity = canonicalJson([key, manifest]);
717
+ const index = new TemporalVectorIndex(manifest.dimensions, {
718
+ identity, basePath: join(context.store.dataDir, "runtime-search", hashJson(identity)),
719
+ });
720
+ let after = "";
721
+ for (;;) {
722
+ const rows = context.store.db.prepare(`SELECT segment_id, dimensions, vector_blob FROM runtime_segment_embeddings
723
+ WHERE generation=? AND embedding_space=? AND modality=? AND segment_id>?
724
+ ORDER BY segment_id LIMIT 512`).all(generation.generation, generation.embedding_space, modality, after);
725
+ for (const row of rows)
726
+ addCachedVector(state, index, row);
727
+ if (rows.length < 512)
728
+ break;
729
+ after = rows.at(-1).segment_id;
730
+ }
731
+ state.vectors.set(key, index);
732
+ return index;
733
+ }
734
+ function orderedStreams(context, generation) {
735
+ const state = retrievalState(context);
736
+ const key = canonicalJson([generation.generation, generation.embedding_space]);
737
+ const cached = state.ordered.get(key);
738
+ if (cached)
739
+ return cached;
740
+ const streams = new Map();
741
+ for (const { value } of vectorIndex(context, generation, "visual").entries.values()) {
742
+ if (!value.range)
743
+ continue;
744
+ const stream = streams.get(value.range.streamId) ?? [];
745
+ stream.push({ segment: value.segment, vector: value.vector, range: value.range });
746
+ streams.set(value.range.streamId, stream);
747
+ }
748
+ for (const stream of streams.values())
749
+ stream.sort((left, right) => left.range.startTick - right.range.startTick || left.segment.segment_id.localeCompare(right.segment.segment_id));
750
+ state.ordered.set(key, streams);
751
+ return streams;
752
+ }
522
753
  function querySegments(context, active, query) {
523
754
  if (active.length === 0)
524
755
  return [];
525
- const generations = active.map((item) => item.generation);
526
- const placeholders = generations.map(() => "?").join(",");
527
- const rows = context.store.db
528
- .prepare(`SELECT s.segment_id, s.artifact_id, a.slug AS artifact_slug,
529
- a.kind AS artifact_kind, a.created_at AS artifact_created_at,
530
- s.stream_id, s.object_hash,
531
- s.source_range_json, s.source_path, s.segment_kind,
532
- s.representative_tick, s.generation, s.manifest_id
533
- FROM runtime_media_segments s
534
- JOIN artifacts a ON a.artifact_id=s.artifact_id
535
- WHERE s.generation IN (${placeholders})
536
- ORDER BY s.artifact_id, s.stream_id, s.representative_tick, s.segment_id`)
537
- .all(...generations);
756
+ const rows = [...retrievalState(context, active).segments.values()];
757
+ const labelMatches = new Map();
758
+ const orientations = new Map();
759
+ const indexingStates = new Map();
538
760
  return rows.filter((row) => {
539
761
  if (query.artifactKinds && !query.artifactKinds.includes(row.artifact_kind)) {
540
762
  return false;
@@ -551,33 +773,38 @@ function querySegments(context, active, query) {
551
773
  && row.artifact_created_at >= query.createdBefore) {
552
774
  return false;
553
775
  }
554
- if (query.labels && !artifactHasLabels(context, row.artifact_id, query.labels)) {
555
- return false;
556
- }
557
- if (query.orientations
558
- && !query.orientations.includes(artifactOrientation(context, row.artifact_id))) {
559
- return false;
776
+ if (query.labels) {
777
+ if (!labelMatches.has(row.artifact_id))
778
+ labelMatches.set(row.artifact_id, artifactHasLabels(context, row.artifact_id, query.labels));
779
+ if (!labelMatches.get(row.artifact_id))
780
+ return false;
560
781
  }
561
- if (query.indexingStates
562
- && !query.indexingStates.includes(artifactGenerationState(context, row.artifact_id, row.generation))) {
563
- return false;
782
+ if (query.orientations) {
783
+ if (!orientations.has(row.artifact_id))
784
+ orientations.set(row.artifact_id, artifactOrientation(context, row.artifact_id));
785
+ if (!query.orientations.includes(orientations.get(row.artifact_id)))
786
+ return false;
564
787
  }
565
- if (query.durationMs) {
566
- const range = segmentRange(row);
567
- if (range) {
568
- const durationMs = (range.durationTicks * range.timeBase.numerator * 1_000)
569
- / range.timeBase.denominator;
570
- if ((query.durationMs.min !== undefined
571
- && durationMs < query.durationMs.min)
572
- || (query.durationMs.max !== undefined
573
- && durationMs > query.durationMs.max)) {
574
- return false;
575
- }
576
- }
788
+ if (query.indexingStates) {
789
+ const key = `${row.artifact_id}:${row.generation}`;
790
+ if (!indexingStates.has(key))
791
+ indexingStates.set(key, artifactGenerationState(context, row.artifact_id, row.generation));
792
+ if (!query.indexingStates.includes(indexingStates.get(key)))
793
+ return false;
577
794
  }
578
- return true;
795
+ return segmentMatchesDuration(row, query.durationMs);
579
796
  });
580
797
  }
798
+ function segmentMatchesDuration(segment, requested) {
799
+ if (!requested)
800
+ return true;
801
+ const range = segmentRange(segment);
802
+ if (!range)
803
+ return true;
804
+ const durationMs = sourceTickToMs(range.durationTicks, range);
805
+ return (requested.min === undefined || durationMs >= requested.min)
806
+ && (requested.max === undefined || durationMs <= requested.max);
807
+ }
581
808
  function artifactHasLabels(context, artifactId, requested) {
582
809
  const row = context.store.db
583
810
  .prepare(`SELECT value_json FROM artifact_metadata
@@ -612,12 +839,7 @@ function addLexicalSignals(context, candidates, text, active) {
612
839
  const generations = active.map((item) => item.generation);
613
840
  if (generations.length === 0)
614
841
  return;
615
- const rows = context.store.db
616
- .prepare(`SELECT segment_id, kind, text
617
- FROM runtime_segment_text
618
- WHERE generation IN (${generations.map(() => "?").join(",")})
619
- ORDER BY segment_id, kind, text`)
620
- .all(...generations);
842
+ const rows = cachedTextRows(context, active);
621
843
  const normalized = text.trim().toLocaleLowerCase();
622
844
  const quoted = [...text.matchAll(/"([^"]+)"/g)]
623
845
  .map((match) => match[1]?.trim().toLocaleLowerCase())
@@ -705,8 +927,11 @@ function addOrderedVideoSignals(context, candidates, rangeInput, generation) {
705
927
  && rangesOverlap(segmentRangeValue.startTick, sourceRangeEndTick(segmentRangeValue), range.startTick, sourceRangeEndTick(range)));
706
928
  if (query.length === 0)
707
929
  return;
708
- const sample = evenlySample(query, Math.min(query.length, 16));
709
- addOrderedVideoVectorSignals(context, candidates, sample, generation);
930
+ const sample = evenlySample(query, Math.min(query.length, 16)).map((item) => ({
931
+ ...item,
932
+ offsetMs: sourceTickToMs(item.range.startTick - range.startTick, range),
933
+ }));
934
+ addOrderedVideoVectorSignals(context, candidates, sample, generation, sourceTickToMs(range.durationTicks, range));
710
935
  }
711
936
  function addPreparedReferenceSignals(context, candidates, reference, options, active) {
712
937
  for (const generation of active) {
@@ -720,38 +945,62 @@ function addPreparedReferenceSignals(context, candidates, reference, options, ac
720
945
  continue;
721
946
  }
722
947
  const samples = preparedVideoSamples(reference, options);
948
+ const startMs = options.range?.startMs ?? 0;
949
+ const durationMs = Math.min(options.range?.durationMs ?? Infinity, preparedVideoDuration(reference) - startMs);
723
950
  for (const sample of samples) {
724
951
  preparedVector(sample.vector, manifest.dimensions);
725
952
  }
726
953
  addOrderedVideoVectorSignals(context, candidates, samples.map((sample) => ({
727
954
  vector: Float32Array.from(sample.vector),
728
- })), generation);
955
+ offsetMs: sample.offsetMs - startMs,
956
+ })), generation, durationMs);
729
957
  }
730
958
  }
731
- function addOrderedVideoVectorSignals(context, candidates, sample, generation) {
959
+ function addOrderedVideoVectorSignals(context, candidates, sample, generation, durationMs) {
732
960
  if (sample.length === 0)
733
961
  return;
734
- const all = orderedEmbeddings(context, generation, () => true);
735
- const byStream = new Map();
736
- for (const item of all) {
737
- const values = byStream.get(item.range.streamId) ?? [];
738
- values.push(item);
739
- byStream.set(item.range.streamId, values);
740
- }
962
+ const byStream = orderedStreams(context, generation);
963
+ const starts = orderedCandidateStarts(context, candidates, sample, generation, byStream);
964
+ const queryVectors = new Map();
741
965
  const windows = [];
742
- for (const values of byStream.values()) {
743
- values.sort((left, right) => left.range.startTick - right.range.startTick
744
- || left.segment.segment_id.localeCompare(right.segment.segment_id));
745
- if (values.length < sample.length)
746
- continue;
747
- for (let start = 0; start <= values.length - sample.length; start += 1) {
748
- const window = values.slice(start, start + sample.length);
749
- const aligned = sample.reduce((sum, item, index) => sum + cosineSimilarity(item.vector, window[index].vector), 0) / sample.length;
750
- const coherence = transitionCoherence(sample, window);
966
+ for (const [streamId, positions] of starts) {
967
+ const values = byStream.get(streamId);
968
+ for (const start of [...positions].sort((left, right) => left - right)) {
969
+ const first = values[start];
970
+ const startMs = sourceTickToMs(first.range.startTick, first.range);
971
+ const end = orderedPosition(values, startMs + durationMs);
972
+ const count = end - start;
973
+ const window = Array.from({ length: Math.min(count, 16) }, (_, index) => values[start + (count === 1 ? 0 : Math.round(index * (count - 1) / (Math.min(count, 16) - 1)))]);
974
+ const last = values[end - 1];
975
+ const availableMs = Math.min(durationMs, sourceTickToMs(sourceRangeEndTick(last.range), last.range) - startMs);
976
+ const timedWindow = window.map((item) => ({
977
+ ...item,
978
+ offsetMs: sourceTickToMs(item.range.startTick, item.range) - startMs,
979
+ }));
980
+ const offsets = [...new Set([...sample, ...timedWindow]
981
+ .map((item) => item.offsetMs)
982
+ .filter((offset) => offset >= 0 && offset < availableMs))]
983
+ .sort((left, right) => left - right);
984
+ if (offsets.length === 0)
985
+ continue;
986
+ const query = offsets.map((offset) => {
987
+ let vector = queryVectors.get(offset);
988
+ if (!vector) {
989
+ vector = videoVectorAt(sample, offset);
990
+ queryVectors.set(offset, vector);
991
+ }
992
+ return { vector };
993
+ });
994
+ const target = offsets.map((offset) => ({ vector: videoVectorAt(timedWindow, offset) }));
995
+ const aligned = query.reduce((sum, item, index) => sum + cosineSimilarity(item.vector, target[index].vector), 0) / query.length;
996
+ const coherence = transitionCoherence(query, target) * 0.75
997
+ + temporalContinuity(values, start, end) * 0.25;
751
998
  windows.push({
752
999
  values: window,
753
- score: aligned * 0.75 + coherence * 0.25,
1000
+ score: (aligned * 0.75 + coherence * 0.25) * availableMs / durationMs,
754
1001
  coherence,
1002
+ durationTicks: Math.max(1, Math.min(sourceRangeEndTick(last.range) - first.range.startTick, Math.round(availableMs * first.range.timeBase.denominator
1003
+ / (first.range.timeBase.numerator * 1_000)))),
755
1004
  });
756
1005
  }
757
1006
  }
@@ -763,7 +1012,7 @@ function addOrderedVideoVectorSignals(context, candidates, sample, generation) {
763
1012
  const last = window.values.at(-1);
764
1013
  const combined = {
765
1014
  ...first.range,
766
- durationTicks: sourceRangeEndTick(last.range) - first.range.startTick,
1015
+ durationTicks: window.durationTicks,
767
1016
  };
768
1017
  const segmentId = `ordered:${generation.generation}:${first.segment.segment_id}:${last.segment.segment_id}`;
769
1018
  const candidate = {
@@ -774,6 +1023,14 @@ function addOrderedVideoVectorSignals(context, candidates, sample, generation) {
774
1023
  representative_tick: first.range.startTick + Math.floor(combined.durationTicks / 2),
775
1024
  },
776
1025
  signals: [
1026
+ ...(isExactOrderedWindow(sample, window.values)
1027
+ ? [{
1028
+ kind: "exact",
1029
+ rank: 1,
1030
+ score: 1,
1031
+ explanation: "Exact source bytes and ordered range matched",
1032
+ }]
1033
+ : []),
777
1034
  {
778
1035
  kind: "visual",
779
1036
  rank: index + 1,
@@ -786,30 +1043,56 @@ function addOrderedVideoVectorSignals(context, candidates, sample, generation) {
786
1043
  candidates.set(segmentId, candidate);
787
1044
  });
788
1045
  }
1046
+ function orderedCandidateStarts(context, candidates, sample, generation, streams) {
1047
+ const starts = new Map();
1048
+ const index = vectorIndex(context, generation, "visual");
1049
+ const timedCandidates = new Map([...candidates].filter(([, candidate]) => candidate.segment.source_range_json !== null));
1050
+ const seedCount = Math.max(32, Math.floor(512 / sample.length));
1051
+ for (const querySample of sample) {
1052
+ for (const { value } of index.nearest(querySample.vector, seedCount, timedCandidates)) {
1053
+ if (!value.range)
1054
+ continue;
1055
+ const stream = streams.get(value.range.streamId);
1056
+ const desiredMs = sourceTickToMs(value.range.startTick, value.range) - querySample.offsetMs;
1057
+ const low = orderedPosition(stream, desiredMs);
1058
+ const positions = starts.get(value.range.streamId) ?? new Set();
1059
+ for (const position of [low - 1, low, low + 1]) {
1060
+ const item = stream[position];
1061
+ if (item && candidates.has(item.segment.segment_id))
1062
+ positions.add(position);
1063
+ }
1064
+ starts.set(value.range.streamId, positions);
1065
+ }
1066
+ }
1067
+ return starts;
1068
+ }
1069
+ function orderedPosition(stream, milliseconds) {
1070
+ let low = 0;
1071
+ let high = stream.length;
1072
+ while (low < high) {
1073
+ const middle = (low + high) >>> 1;
1074
+ const item = stream[middle];
1075
+ if (sourceTickToMs(item.range.startTick, item.range) < milliseconds)
1076
+ low = middle + 1;
1077
+ else
1078
+ high = middle;
1079
+ }
1080
+ return low;
1081
+ }
1082
+ function videoVectorAt(samples, offsetMs) {
1083
+ const next = samples.findIndex((sample) => sample.offsetMs >= offsetMs);
1084
+ if (next === -1)
1085
+ return samples.at(-1).vector;
1086
+ if (next === 0 || samples[next].offsetMs === offsetMs)
1087
+ return samples[next].vector;
1088
+ const before = samples[next - 1];
1089
+ const after = samples[next];
1090
+ const fraction = (offsetMs - before.offsetMs) / (after.offsetMs - before.offsetMs);
1091
+ return before.vector.map((value, index) => value * (1 - fraction) + after.vector[index] * fraction);
1092
+ }
789
1093
  function orderedEmbeddings(context, generation, include) {
790
- const rows = context.store.db
791
- .prepare(`SELECT s.segment_id, s.artifact_id, a.slug AS artifact_slug,
792
- a.kind AS artifact_kind, a.created_at AS artifact_created_at,
793
- s.stream_id, s.object_hash, s.source_range_json,
794
- s.source_path, s.segment_kind, s.representative_tick,
795
- s.generation, s.manifest_id, e.dimensions, e.vector_blob
796
- FROM runtime_media_segments s
797
- JOIN artifacts a ON a.artifact_id=s.artifact_id
798
- JOIN runtime_segment_embeddings e ON e.segment_id=s.segment_id
799
- WHERE s.generation=? AND e.embedding_space=?
800
- AND e.modality='visual' AND s.source_range_json IS NOT NULL
801
- ORDER BY s.stream_id, s.representative_tick, s.segment_id`)
802
- .all(generation.generation, generation.embedding_space);
803
- return rows.flatMap((row) => {
804
- const range = segmentRange(row);
805
- return range && include(range)
806
- ? [{
807
- segment: row,
808
- range,
809
- vector: blobToVector(row.vector_blob, row.dimensions),
810
- }]
811
- : [];
812
- });
1094
+ return [...orderedStreams(context, generation).values()]
1095
+ .flatMap((rows) => rows.filter((row) => include(row.range)));
813
1096
  }
814
1097
  function transitionCoherence(query, candidate) {
815
1098
  if (query.length < 2)
@@ -820,6 +1103,42 @@ function transitionCoherence(query, candidate) {
820
1103
  }
821
1104
  return score / (query.length - 1);
822
1105
  }
1106
+ function temporalContinuity(candidate, start, end) {
1107
+ if (end - start < 2)
1108
+ return 1;
1109
+ let prefix = continuityPrefixes.get(candidate);
1110
+ if (!prefix) {
1111
+ prefix = new Float64Array(candidate.length);
1112
+ for (let index = 1; index < candidate.length; index++) {
1113
+ const previous = candidate[index - 1].range;
1114
+ const current = candidate[index].range;
1115
+ const gapMs = Math.max(0, sourceTickToMs(current.startTick, current)
1116
+ - sourceTickToMs(sourceRangeEndTick(previous), previous));
1117
+ const scaleMs = Math.max(1, (sourceTickToMs(previous.durationTicks, previous)
1118
+ + sourceTickToMs(current.durationTicks, current)) / 2);
1119
+ prefix[index] = prefix[index - 1] + 1 / (1 + gapMs / scaleMs);
1120
+ }
1121
+ continuityPrefixes.set(candidate, prefix);
1122
+ }
1123
+ return (prefix[end - 1] - prefix[start]) / (end - start - 1);
1124
+ }
1125
+ function isExactOrderedWindow(query, candidate) {
1126
+ return query.length === candidate.length && query.every((item, index) => {
1127
+ const reference = item.range;
1128
+ const match = candidate[index]?.range;
1129
+ return Boolean(reference
1130
+ && match
1131
+ && reference.objectHash === match.objectHash
1132
+ && nearlyEqual(sourceTickToMs(reference.startTick, reference), sourceTickToMs(match.startTick, match))
1133
+ && nearlyEqual(sourceTickToMs(reference.durationTicks, reference), sourceTickToMs(match.durationTicks, match)));
1134
+ });
1135
+ }
1136
+ function sourceTickToMs(tick, range) {
1137
+ return (tick * range.timeBase.numerator * 1_000) / range.timeBase.denominator;
1138
+ }
1139
+ function nearlyEqual(left, right) {
1140
+ return Math.abs(left - right) <= 1e-6;
1141
+ }
823
1142
  function vectorDifference(left, right) {
824
1143
  const result = new Float32Array(left.length);
825
1144
  for (let index = 0; index < left.length; index += 1) {
@@ -838,31 +1157,13 @@ function evenlySample(values, count) {
838
1157
  });
839
1158
  }
840
1159
  function rankVectorRows(context, candidates, generation, queryVector, modality, explanation) {
841
- const rows = context.store.db
842
- .prepare(`SELECT segment_id, embedding_space, modality, dimensions, vector_blob
843
- FROM runtime_segment_embeddings
844
- WHERE generation=? AND embedding_space=?
845
- AND modality=?
846
- ORDER BY segment_id`)
847
- .all(generation.generation, generation.embedding_space, modality);
848
- rows
849
- .map((row) => ({
850
- row,
851
- score: cosineSimilarity(queryVector, blobToVector(row.vector_blob, row.dimensions)),
852
- }))
853
- .filter((item) => Number.isFinite(item.score))
854
- .sort((left, right) => right.score - left.score
855
- || left.row.segment_id.localeCompare(right.row.segment_id))
1160
+ vectorIndex(context, generation, modality)
1161
+ .nearest(queryVector, VECTOR_CANDIDATES, candidates)
856
1162
  .forEach((item, index) => {
857
- const candidate = candidates.get(item.row.segment_id);
1163
+ const candidate = candidates.get(item.key);
858
1164
  if (!candidate)
859
1165
  return;
860
- candidate.signals.push({
861
- kind: modality,
862
- rank: index + 1,
863
- score: item.score,
864
- explanation,
865
- });
1166
+ candidate.signals.push({ kind: modality, rank: index + 1, score: item.score, explanation });
866
1167
  });
867
1168
  }
868
1169
  function referenceEmbeddingRows(context, reference, generation) {
@@ -870,13 +1171,14 @@ function referenceEmbeddingRows(context, reference, generation) {
870
1171
  if (segments.length === 0)
871
1172
  return [];
872
1173
  const placeholders = segments.map(() => "?").join(",");
1174
+ const modality = reference.kind === "audio" ? "audio" : "visual";
873
1175
  return context.store.db
874
1176
  .prepare(`SELECT segment_id, embedding_space, modality, dimensions, vector_blob
875
1177
  FROM runtime_segment_embeddings
876
1178
  WHERE segment_id IN (${placeholders})
877
- AND embedding_space=?
1179
+ AND embedding_space=? AND modality=?
878
1180
  ORDER BY segment_id`)
879
- .all(...segments, generation.embedding_space);
1181
+ .all(...segments, generation.embedding_space, modality);
880
1182
  }
881
1183
  function referenceSegments(context, reference, generation) {
882
1184
  if (reference.kind === "image") {
@@ -1073,7 +1375,9 @@ function intervalPriority(interval) {
1073
1375
  function candidateToHit(candidate) {
1074
1376
  return {
1075
1377
  artifactId: candidate.segment.artifact_id,
1076
- artifactSlug: candidate.segment.artifact_slug,
1378
+ ...(candidate.segment.artifact_label
1379
+ ? { artifactLabel: candidate.segment.artifact_label }
1380
+ : {}),
1077
1381
  artifactKind: candidate.segment.artifact_kind,
1078
1382
  location: segmentLocation(candidate.segment),
1079
1383
  ...(candidate.segment.representative_tick === null
@@ -1297,6 +1601,13 @@ function validatePreparedReference(reference, options) {
1297
1601
  message: "Prepared video references require between 1 and 16 samples",
1298
1602
  });
1299
1603
  }
1604
+ if (reference.durationMs !== undefined && (!Number.isFinite(reference.durationMs) || reference.durationMs <= 0
1605
+ || reference.samples.some((sample) => sample.offsetMs >= reference.durationMs))) {
1606
+ throw new EngineFault({
1607
+ code: "INVALID_INPUT",
1608
+ message: "Prepared video duration must be finite, positive, and contain every sample",
1609
+ });
1610
+ }
1300
1611
  let previousOffset = -1;
1301
1612
  for (const sample of reference.samples) {
1302
1613
  if (!Number.isFinite(sample.offsetMs) || sample.offsetMs < 0) {
@@ -1334,11 +1645,9 @@ function validatePreparedReference(reference, options) {
1334
1645
  }
1335
1646
  function preparedVideoSamples(reference, options) {
1336
1647
  const range = options.range;
1337
- const endMs = range?.durationMs === undefined
1338
- ? Number.POSITIVE_INFINITY
1339
- : range.startMs + range.durationMs;
1648
+ const endMs = Math.min(preparedVideoDuration(reference), range?.durationMs === undefined ? Infinity : range.startMs + range.durationMs);
1340
1649
  const samples = range
1341
- ? reference.samples.filter((sample) => sample.offsetMs >= range.startMs && sample.offsetMs <= endMs)
1650
+ ? reference.samples.filter((sample) => sample.offsetMs >= range.startMs && sample.offsetMs < endMs)
1342
1651
  : reference.samples;
1343
1652
  if (samples.length === 0) {
1344
1653
  throw new EngineFault({
@@ -1348,6 +1657,13 @@ function preparedVideoSamples(reference, options) {
1348
1657
  }
1349
1658
  return samples;
1350
1659
  }
1660
+ function preparedVideoDuration(reference) {
1661
+ if (reference.durationMs !== undefined)
1662
+ return reference.durationMs;
1663
+ const last = reference.samples.at(-1);
1664
+ const intervals = reference.samples.slice(1).map((sample, index) => sample.offsetMs - reference.samples[index].offsetMs).sort((a, b) => a - b);
1665
+ return last.offsetMs + (intervals[Math.floor(intervals.length / 2)] ?? 1);
1666
+ }
1351
1667
  function preparedVector(vector, dimensions) {
1352
1668
  if (!Array.isArray(vector)
1353
1669
  || vector.length === 0