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