@reftrixmcp/ml 0.1.7

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 (97) hide show
  1. package/LICENSE +661 -0
  2. package/dist/dinov2/index.d.ts +7 -0
  3. package/dist/dinov2/index.d.ts.map +1 -0
  4. package/dist/dinov2/index.js +7 -0
  5. package/dist/dinov2/index.js.map +1 -0
  6. package/dist/dinov2/service.d.ts +106 -0
  7. package/dist/dinov2/service.d.ts.map +1 -0
  8. package/dist/dinov2/service.js +446 -0
  9. package/dist/dinov2/service.js.map +1 -0
  10. package/dist/dinov2/worker-thread-types.d.ts +77 -0
  11. package/dist/dinov2/worker-thread-types.d.ts.map +1 -0
  12. package/dist/dinov2/worker-thread-types.js +4 -0
  13. package/dist/dinov2/worker-thread-types.js.map +1 -0
  14. package/dist/dinov2/worker-thread.d.ts +2 -0
  15. package/dist/dinov2/worker-thread.d.ts.map +1 -0
  16. package/dist/dinov2/worker-thread.js +231 -0
  17. package/dist/dinov2/worker-thread.js.map +1 -0
  18. package/dist/embeddings/index.d.ts +12 -0
  19. package/dist/embeddings/index.d.ts.map +1 -0
  20. package/dist/embeddings/index.js +13 -0
  21. package/dist/embeddings/index.js.map +1 -0
  22. package/dist/embeddings/multimodal-embedding.service.d.ts +400 -0
  23. package/dist/embeddings/multimodal-embedding.service.d.ts.map +1 -0
  24. package/dist/embeddings/multimodal-embedding.service.js +650 -0
  25. package/dist/embeddings/multimodal-embedding.service.js.map +1 -0
  26. package/dist/embeddings/service.d.ts +261 -0
  27. package/dist/embeddings/service.d.ts.map +1 -0
  28. package/dist/embeddings/service.js +1093 -0
  29. package/dist/embeddings/service.js.map +1 -0
  30. package/dist/embeddings/style-embedding.service.d.ts +117 -0
  31. package/dist/embeddings/style-embedding.service.d.ts.map +1 -0
  32. package/dist/embeddings/style-embedding.service.js +157 -0
  33. package/dist/embeddings/style-embedding.service.js.map +1 -0
  34. package/dist/embeddings/types.d.ts +83 -0
  35. package/dist/embeddings/types.d.ts.map +1 -0
  36. package/dist/embeddings/types.js +4 -0
  37. package/dist/embeddings/types.js.map +1 -0
  38. package/dist/embeddings/vision-embedding.service.d.ts +70 -0
  39. package/dist/embeddings/vision-embedding.service.d.ts.map +1 -0
  40. package/dist/embeddings/vision-embedding.service.js +221 -0
  41. package/dist/embeddings/vision-embedding.service.js.map +1 -0
  42. package/dist/embeddings/vision-embedding.types.d.ts +102 -0
  43. package/dist/embeddings/vision-embedding.types.d.ts.map +1 -0
  44. package/dist/embeddings/vision-embedding.types.js +4 -0
  45. package/dist/embeddings/vision-embedding.types.js.map +1 -0
  46. package/dist/embeddings/worker-thread-types.d.ts +152 -0
  47. package/dist/embeddings/worker-thread-types.d.ts.map +1 -0
  48. package/dist/embeddings/worker-thread-types.js +4 -0
  49. package/dist/embeddings/worker-thread-types.js.map +1 -0
  50. package/dist/embeddings/worker-thread.d.ts +4 -0
  51. package/dist/embeddings/worker-thread.d.ts.map +1 -0
  52. package/dist/embeddings/worker-thread.js +441 -0
  53. package/dist/embeddings/worker-thread.js.map +1 -0
  54. package/dist/index.d.ts +18 -0
  55. package/dist/index.d.ts.map +1 -0
  56. package/dist/index.js +19 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/search/fulltext-helpers.d.ts +30 -0
  59. package/dist/search/fulltext-helpers.d.ts.map +1 -0
  60. package/dist/search/fulltext-helpers.js +40 -0
  61. package/dist/search/fulltext-helpers.js.map +1 -0
  62. package/dist/search/hybrid.d.ts +40 -0
  63. package/dist/search/hybrid.d.ts.map +1 -0
  64. package/dist/search/hybrid.js +61 -0
  65. package/dist/search/hybrid.js.map +1 -0
  66. package/dist/search/index.d.ts +12 -0
  67. package/dist/search/index.d.ts.map +1 -0
  68. package/dist/search/index.js +11 -0
  69. package/dist/search/index.js.map +1 -0
  70. package/dist/search/rrf.d.ts +69 -0
  71. package/dist/search/rrf.d.ts.map +1 -0
  72. package/dist/search/rrf.js +108 -0
  73. package/dist/search/rrf.js.map +1 -0
  74. package/dist/search/types.d.ts +130 -0
  75. package/dist/search/types.d.ts.map +1 -0
  76. package/dist/search/types.js +4 -0
  77. package/dist/search/types.js.map +1 -0
  78. package/package.json +65 -0
  79. package/src/dinov2/index.ts +21 -0
  80. package/src/dinov2/service.ts +539 -0
  81. package/src/dinov2/worker-thread-types.ts +103 -0
  82. package/src/dinov2/worker-thread.ts +306 -0
  83. package/src/embeddings/index.ts +74 -0
  84. package/src/embeddings/multimodal-embedding.service.ts +992 -0
  85. package/src/embeddings/service.ts +1287 -0
  86. package/src/embeddings/style-embedding.service.ts +201 -0
  87. package/src/embeddings/types.ts +95 -0
  88. package/src/embeddings/vision-embedding.service.ts +273 -0
  89. package/src/embeddings/vision-embedding.types.ts +117 -0
  90. package/src/embeddings/worker-thread-types.ts +194 -0
  91. package/src/embeddings/worker-thread.ts +529 -0
  92. package/src/index.ts +70 -0
  93. package/src/search/fulltext-helpers.ts +45 -0
  94. package/src/search/hybrid.ts +107 -0
  95. package/src/search/index.ts +25 -0
  96. package/src/search/rrf.ts +154 -0
  97. package/src/search/types.ts +141 -0
@@ -0,0 +1,529 @@
1
+ // SPDX-FileCopyrightText: 2026 TKMD and Reftrix Contributors
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ /**
5
+ * ONNX Embedding Worker Thread
6
+ *
7
+ * Runs ONNX inference in a separate thread so CPU-bound inference
8
+ * does not block the main thread's event loop. This allows BullMQ
9
+ * heartbeats, IPC messages, and other async operations to proceed
10
+ * while inference is in progress.
11
+ *
12
+ * Architecture:
13
+ * - Main thread sends prefixed texts via parentPort messages.
14
+ * - This worker owns the entire ONNX pipeline lifecycle
15
+ * (init, inference, recycle, dispose).
16
+ * - Results are sent back as plain number[][] (structured clone safe).
17
+ * - ONNX Session/Tensor objects never cross thread boundaries.
18
+ *
19
+ * @module embeddings/worker-thread
20
+ */
21
+
22
+ import { parentPort } from "node:worker_threads";
23
+ import { createRequire } from "node:module";
24
+ import fs from "node:fs";
25
+ import nodePath from "node:path";
26
+ import type { WorkerMessage, WorkerResponse, WorkerErrorResponse } from "./worker-thread-types.js";
27
+
28
+ // =====================================================
29
+ // Pipeline types (mirrors service.ts DisposablePipeline)
30
+ // =====================================================
31
+
32
+ interface DisposablePipeline {
33
+ (
34
+ texts: string | string[],
35
+ options?: { pooling?: string; normalize?: boolean }
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ ): Promise<any>;
38
+ dispose?: () => Promise<unknown[]>;
39
+ }
40
+
41
+ // =====================================================
42
+ // GPU execution provider detection
43
+ // =====================================================
44
+
45
+ type ExecutionProvider = "cpu" | "cuda";
46
+
47
+ /**
48
+ * Detect the ONNX execution provider based on environment configuration.
49
+ *
50
+ * Checks ONNX_EXECUTION_PROVIDER env var and verifies that the CUDA
51
+ * provider shared library is available on disk. Falls back to CPU
52
+ * gracefully if the provider is not installed.
53
+ */
54
+ function detectExecutionProvider(): ExecutionProvider {
55
+ const envProvider = process.env.ONNX_EXECUTION_PROVIDER;
56
+
57
+ if (envProvider === "cuda" || envProvider === "rocm") {
58
+ const cudaAvailable = verifyCudaAvailability();
59
+ if (cudaAvailable) {
60
+ return "cuda";
61
+ }
62
+ console.warn(
63
+ "[EmbeddingWorker] ONNX_EXECUTION_PROVIDER=%s but CUDA provider not available, falling back to CPU",
64
+ envProvider
65
+ );
66
+ return "cpu";
67
+ }
68
+
69
+ return "cpu";
70
+ }
71
+
72
+ /**
73
+ * Verify that CUDA provider (libonnxruntime_providers_cuda.so) is available.
74
+ *
75
+ * onnxruntime-node can download the CUDA provider shared library via
76
+ * `ONNXRUNTIME_NODE_INSTALL_CUDA=v12 node .../install.js`. This function
77
+ * checks for its presence on disk rather than trying to resolve a
78
+ * non-existent npm package (onnxruntime-gpu is Python-only).
79
+ */
80
+ function verifyCudaAvailability(): boolean {
81
+ try {
82
+ const esmRequire = createRequire(import.meta.url);
83
+ const ortNodePath = esmRequire.resolve("onnxruntime-node");
84
+ // require.resolve returns e.g. .../onnxruntime-node/dist/index.js
85
+ // Walk up to package root by finding the directory containing package.json
86
+ let packageDir = nodePath.dirname(ortNodePath);
87
+ for (let i = 0; i < 5; i++) {
88
+ if (fs.existsSync(nodePath.join(packageDir, "package.json"))) break;
89
+ packageDir = nodePath.dirname(packageDir);
90
+ }
91
+
92
+ // Search across napi versions (v3, v6, etc.) for CUDA provider
93
+ const binDir = nodePath.join(packageDir, "bin");
94
+ if (fs.existsSync(binDir)) {
95
+ const napiDirs = fs.readdirSync(binDir).filter((d) => d.startsWith("napi-v"));
96
+ for (const napiDir of napiDirs) {
97
+ const cudaProviderPath = nodePath.join(
98
+ binDir,
99
+ napiDir,
100
+ "linux",
101
+ "x64",
102
+ "libonnxruntime_providers_cuda.so"
103
+ );
104
+ if (fs.existsSync(cudaProviderPath)) {
105
+ return true;
106
+ }
107
+ }
108
+ }
109
+
110
+ console.warn("[EmbeddingWorker] CUDA provider not found in: %s", binDir);
111
+ return false;
112
+ } catch {
113
+ console.warn("[EmbeddingWorker] Cannot verify CUDA provider: onnxruntime-node not found");
114
+ return false;
115
+ }
116
+ }
117
+
118
+ // =====================================================
119
+ // Worker state
120
+ // =====================================================
121
+
122
+ let pipeline: DisposablePipeline | null = null;
123
+ let initPromise: Promise<void> | null = null;
124
+ let inferencesSinceRecycle = 0;
125
+ let totalRecycles = 0;
126
+ let resolvedProvider: ExecutionProvider = "cpu";
127
+ /** Whether resolvedProvider was explicitly set by a switch-provider message. */
128
+ let providerExplicitlySet = false;
129
+ let config = {
130
+ modelId: "Xenova/multilingual-e5-base",
131
+ cacheDir: "./.cache/models",
132
+ device: "cpu",
133
+ dtype: "fp32",
134
+ pipelineRecycleThreshold: 30,
135
+ };
136
+
137
+ const EMBEDDING_DIMENSION = 768;
138
+
139
+ // =====================================================
140
+ // Pipeline management
141
+ // =====================================================
142
+
143
+ function normalizeVector(vector: number[]): number[] {
144
+ const norm = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
145
+ if (norm === 0) return vector;
146
+ return vector.map((val) => val / norm);
147
+ }
148
+
149
+ /**
150
+ * Verify that LD_LIBRARY_PATH is set at the OS level (not just process.env).
151
+ *
152
+ * dlopen() reads LD_LIBRARY_PATH from the kernel environment (/proc/self/environ),
153
+ * NOT from process.env modifications made at runtime. If LD_LIBRARY_PATH was set
154
+ * after process startup (e.g. by loadEnvLocal()), dlopen() cannot find CUDA
155
+ * libraries and ONNX Runtime CUDA provider initialization will segfault or throw.
156
+ *
157
+ * Returns true only if LD_LIBRARY_PATH was present in the original process environment.
158
+ */
159
+ function isLdLibraryPathSetAtOsLevel(): boolean {
160
+ try {
161
+ const procEnv = fs.readFileSync("/proc/self/environ", "utf-8");
162
+ return procEnv.includes("LD_LIBRARY_PATH");
163
+ } catch {
164
+ // /proc/self/environ not available (non-Linux) — assume set
165
+ return true;
166
+ }
167
+ }
168
+
169
+ async function initializePipeline(): Promise<void> {
170
+ if (pipeline) return;
171
+ if (initPromise) return initPromise;
172
+
173
+ initPromise = (async (): Promise<void> => {
174
+ try {
175
+ // Only auto-detect from env on first init. If provider was explicitly
176
+ // set by a switch-provider message, respect that setting.
177
+ if (!providerExplicitlySet) {
178
+ resolvedProvider = detectExecutionProvider();
179
+ }
180
+
181
+ // Safety check: if CUDA is requested but LD_LIBRARY_PATH was not set at
182
+ // the OS level (only set via loadEnvLocal at runtime), CUDA init will crash
183
+ // because dlopen() can't find CUDA shared libraries. Fall back to CPU.
184
+ if (resolvedProvider === "cuda" && !isLdLibraryPathSetAtOsLevel()) {
185
+ console.warn(
186
+ "[EmbeddingWorker] CUDA requested but LD_LIBRARY_PATH not set at OS level. " +
187
+ "dlopen() cannot find CUDA libraries. Falling back to CPU. " +
188
+ "To use CUDA, set LD_LIBRARY_PATH before starting the Node.js process."
189
+ );
190
+ resolvedProvider = "cpu";
191
+ }
192
+
193
+ const effectiveDevice = resolvedProvider === "cuda" ? "cuda" : config.device;
194
+
195
+ // eslint-disable-next-line no-console
196
+ console.log("[EmbeddingWorker] Initializing ONNX pipeline", {
197
+ model: config.modelId,
198
+ requestedDevice: config.device,
199
+ resolvedProvider,
200
+ effectiveDevice,
201
+ dtype: config.dtype,
202
+ recycleThreshold: config.pipelineRecycleThreshold,
203
+ });
204
+
205
+ const { pipeline: createPipeline } = await import("@huggingface/transformers");
206
+
207
+ // Attempt to create pipeline with the resolved device.
208
+ // If CUDA init fails (e.g. library not found), catch and retry with CPU.
209
+ try {
210
+ pipeline = (await createPipeline("feature-extraction", config.modelId, {
211
+ dtype: config.dtype as "fp32",
212
+ device: effectiveDevice as "cpu",
213
+ })) as unknown as DisposablePipeline;
214
+ } catch (deviceError) {
215
+ if (effectiveDevice !== "cpu") {
216
+ // CUDA/GPU init failed — fallback to CPU
217
+ const deviceErrorMsg =
218
+ deviceError instanceof Error ? deviceError.message : String(deviceError);
219
+ console.warn(
220
+ "[EmbeddingWorker] %s pipeline creation failed, falling back to CPU: %s",
221
+ effectiveDevice,
222
+ deviceErrorMsg
223
+ );
224
+ resolvedProvider = "cpu";
225
+
226
+ pipeline = (await createPipeline("feature-extraction", config.modelId, {
227
+ dtype: config.dtype as "fp32",
228
+ device: "cpu",
229
+ })) as unknown as DisposablePipeline;
230
+ } else {
231
+ throw deviceError;
232
+ }
233
+ }
234
+
235
+ // eslint-disable-next-line no-console
236
+ console.log("[EmbeddingWorker] ONNX pipeline ready (provider: %s)", resolvedProvider);
237
+ } catch (error) {
238
+ initPromise = null;
239
+ const message = error instanceof Error ? error.message : "Unknown error";
240
+ throw new Error(`Failed to load embedding model in worker thread: ${message}`);
241
+ }
242
+ })();
243
+
244
+ return initPromise;
245
+ }
246
+
247
+ async function disposePipeline(): Promise<void> {
248
+ if (pipeline && typeof pipeline.dispose === "function") {
249
+ try {
250
+ await pipeline.dispose();
251
+ } catch {
252
+ // Disposal failure should not break the workflow
253
+ }
254
+ }
255
+ pipeline = null;
256
+ initPromise = null;
257
+ inferencesSinceRecycle = 0;
258
+ }
259
+
260
+ async function recyclePipelineIfNeeded(inferenceCount: number): Promise<void> {
261
+ inferencesSinceRecycle += inferenceCount;
262
+
263
+ const threshold = config.pipelineRecycleThreshold;
264
+ if (threshold <= 0) return;
265
+
266
+ if (inferencesSinceRecycle >= threshold) {
267
+ await disposePipeline();
268
+ totalRecycles++;
269
+
270
+ if (typeof global.gc === "function") {
271
+ global.gc();
272
+ }
273
+ }
274
+ }
275
+
276
+ // =====================================================
277
+ // Inference functions
278
+ // =====================================================
279
+
280
+ /**
281
+ * Extract number[] from a pipeline output, disposing the tensor.
282
+ */
283
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
284
+ function extractSingleEmbedding(output: any): number[] {
285
+ let embedding: number[];
286
+ if (output && typeof output.tolist === "function") {
287
+ const result = output.tolist();
288
+ embedding = Array.isArray(result[0]) ? result[0] : result;
289
+ if (typeof output.dispose === "function") {
290
+ output.dispose();
291
+ }
292
+ } else if (Array.isArray(output)) {
293
+ embedding = output;
294
+ } else {
295
+ throw new Error("Unexpected embedding output format");
296
+ }
297
+
298
+ if (embedding.length !== EMBEDDING_DIMENSION) {
299
+ // Non-fatal: warn but continue
300
+ }
301
+
302
+ return normalizeVector(embedding);
303
+ }
304
+
305
+ /**
306
+ * Extract number[][] from a batch pipeline output, disposing the tensor.
307
+ */
308
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
309
+ function extractBatchEmbeddings(output: any): number[][] {
310
+ let batchEmbeddings: number[][];
311
+ if (output && typeof output.tolist === "function") {
312
+ batchEmbeddings = output.tolist();
313
+ if (typeof output.dispose === "function") {
314
+ output.dispose();
315
+ }
316
+ } else if (Array.isArray(output)) {
317
+ batchEmbeddings = output;
318
+ } else {
319
+ throw new Error("Unexpected batch embedding output format");
320
+ }
321
+
322
+ return batchEmbeddings.map(normalizeVector);
323
+ }
324
+
325
+ async function generateSingle(text: string): Promise<number[]> {
326
+ await initializePipeline();
327
+ if (!pipeline) throw new Error("Pipeline not initialized");
328
+
329
+ const output = await pipeline(text, { pooling: "mean", normalize: true });
330
+ const embedding = extractSingleEmbedding(output);
331
+
332
+ await recyclePipelineIfNeeded(1);
333
+ return embedding;
334
+ }
335
+
336
+ async function generateBatch(texts: string[]): Promise<number[][]> {
337
+ if (texts.length === 0) return [];
338
+
339
+ const BATCH_SIZE = 32;
340
+ const allEmbeddings: number[][] = [];
341
+
342
+ for (let batchStart = 0; batchStart < texts.length; batchStart += BATCH_SIZE) {
343
+ const batchEnd = Math.min(batchStart + BATCH_SIZE, texts.length);
344
+ const batch = texts.slice(batchStart, batchEnd);
345
+
346
+ // Re-initialize if recycled during previous batch
347
+ await initializePipeline();
348
+ if (!pipeline) throw new Error("Pipeline not initialized after recycle");
349
+
350
+ const output = await pipeline(batch, { pooling: "mean", normalize: true });
351
+ const batchEmbeddings = extractBatchEmbeddings(output);
352
+ allEmbeddings.push(...batchEmbeddings);
353
+
354
+ await recyclePipelineIfNeeded(batch.length);
355
+ }
356
+
357
+ return allEmbeddings;
358
+ }
359
+
360
+ // =====================================================
361
+ // Message handler
362
+ // =====================================================
363
+
364
+ function sendResponse(response: WorkerResponse): void {
365
+ parentPort?.postMessage(response);
366
+ }
367
+
368
+ function sendError(requestId: string, originalType: WorkerMessage["type"], error: unknown): void {
369
+ const errorMessage = error instanceof Error ? error.message : String(error);
370
+ const response: WorkerErrorResponse = {
371
+ type: "error",
372
+ requestId,
373
+ success: false,
374
+ error: errorMessage,
375
+ originalType,
376
+ };
377
+ parentPort?.postMessage(response);
378
+ }
379
+
380
+ async function handleMessage(message: WorkerMessage): Promise<void> {
381
+ switch (message.type) {
382
+ case "init": {
383
+ config = { ...config, ...message.config };
384
+ const startTime = Date.now();
385
+ await initializePipeline();
386
+ sendResponse({
387
+ type: "init",
388
+ requestId: message.requestId,
389
+ success: true,
390
+ loadTimeMs: Date.now() - startTime,
391
+ executionProvider: resolvedProvider,
392
+ });
393
+ break;
394
+ }
395
+
396
+ case "generate": {
397
+ const startTime = Date.now();
398
+ const embedding = await generateSingle(message.text);
399
+ sendResponse({
400
+ type: "generate",
401
+ requestId: message.requestId,
402
+ success: true,
403
+ embedding,
404
+ inferenceTimeMs: Date.now() - startTime,
405
+ });
406
+ break;
407
+ }
408
+
409
+ case "generateBatch": {
410
+ const startTime = Date.now();
411
+ const embeddings = await generateBatch(message.texts);
412
+ sendResponse({
413
+ type: "generateBatch",
414
+ requestId: message.requestId,
415
+ success: true,
416
+ embeddings,
417
+ inferenceTimeMs: Date.now() - startTime,
418
+ });
419
+ break;
420
+ }
421
+
422
+ case "dispose": {
423
+ await disposePipeline();
424
+ sendResponse({
425
+ type: "dispose",
426
+ requestId: message.requestId,
427
+ success: true,
428
+ });
429
+ break;
430
+ }
431
+
432
+ case "terminate": {
433
+ await disposePipeline();
434
+ sendResponse({
435
+ type: "terminate",
436
+ requestId: message.requestId,
437
+ success: true,
438
+ });
439
+ // Allow the response to be flushed before exiting
440
+ setTimeout(() => process.exit(0), 50);
441
+ break;
442
+ }
443
+
444
+ case "switch-provider": {
445
+ const targetProvider = message.provider;
446
+
447
+ // If switching to CUDA, verify availability first
448
+ if (targetProvider === "cuda") {
449
+ const canUseCuda = verifyCudaAvailability();
450
+ if (!canUseCuda) {
451
+ // Cannot switch to CUDA — respond with current (cpu) provider
452
+ sendResponse({
453
+ type: "switch-provider",
454
+ requestId: message.requestId,
455
+ success: true,
456
+ provider: "cpu",
457
+ });
458
+ break;
459
+ }
460
+ }
461
+
462
+ // Dispose current pipeline before switching
463
+ await disposePipeline();
464
+
465
+ // Update provider and config — mark as explicitly set so
466
+ // initializePipeline() won't override via detectExecutionProvider()
467
+ resolvedProvider = targetProvider;
468
+ providerExplicitlySet = true;
469
+ config = { ...config, device: targetProvider };
470
+
471
+ // eslint-disable-next-line no-console
472
+ console.log("[EmbeddingWorker] Switched provider to:", resolvedProvider);
473
+
474
+ sendResponse({
475
+ type: "switch-provider",
476
+ requestId: message.requestId,
477
+ success: true,
478
+ provider: resolvedProvider,
479
+ });
480
+ break;
481
+ }
482
+
483
+ case "release-gpu": {
484
+ // Dispose pipeline and revert to CPU
485
+ await disposePipeline();
486
+ resolvedProvider = "cpu";
487
+ providerExplicitlySet = true;
488
+ config = { ...config, device: "cpu" };
489
+
490
+ // eslint-disable-next-line no-console
491
+ console.log("[EmbeddingWorker] Released GPU, reverted to CPU");
492
+
493
+ sendResponse({
494
+ type: "release-gpu",
495
+ requestId: message.requestId,
496
+ success: true,
497
+ });
498
+ break;
499
+ }
500
+
501
+ default: {
502
+ // SEC-M2: Reject unknown message types for defense-in-depth
503
+ const unknownType = (message as Record<string, unknown>).type ?? "unknown";
504
+ sendError(
505
+ ((message as Record<string, unknown>).requestId as string) ?? "unknown",
506
+ unknownType as WorkerMessage["type"],
507
+ new Error(`Unknown worker message type: ${String(unknownType)}`)
508
+ );
509
+ break;
510
+ }
511
+ }
512
+ }
513
+
514
+ // =====================================================
515
+ // Setup parentPort listener
516
+ // =====================================================
517
+
518
+ if (!parentPort) {
519
+ throw new Error("worker-thread.ts must be run as a Worker Thread (no parentPort)");
520
+ }
521
+
522
+ parentPort.on("message", (message: WorkerMessage) => {
523
+ handleMessage(message).catch((error) => {
524
+ sendError(message.requestId, message.type, error);
525
+ });
526
+ });
527
+
528
+ // Export recycle count for monitoring (accessible via workerData if needed)
529
+ export { totalRecycles, inferencesSinceRecycle };
package/src/index.ts ADDED
@@ -0,0 +1,70 @@
1
+ // SPDX-FileCopyrightText: 2026 TKMD and Reftrix Contributors
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ /**
5
+ * @reftrixmcp/ml
6
+ * ML/Embedding and Search services for Reftrix
7
+ */
8
+
9
+ // Embedding service exports
10
+ export { EmbeddingService, embeddingService, cosineSimilarity } from "./embeddings/index.js";
11
+ export type {
12
+ EmbeddingTextType,
13
+ EmbeddingServiceConfig,
14
+ CacheStats,
15
+ EmbeddingResult,
16
+ BatchEmbeddingResult,
17
+ } from "./embeddings/index.js";
18
+
19
+ // Style Feature Embedding exports
20
+ export {
21
+ StyleEmbeddingService,
22
+ styleEmbeddingService,
23
+ createStyleEmbedding,
24
+ createBatchStyleEmbeddings,
25
+ createQueryEmbedding,
26
+ } from "./embeddings/index.js";
27
+ export type { StyleEmbeddingConfig } from "./embeddings/index.js";
28
+
29
+ // Vision Feature Embedding exports
30
+ export {
31
+ VisionEmbeddingService,
32
+ visionEmbeddingService,
33
+ createVisionEmbedding,
34
+ createBatchVisionEmbeddings,
35
+ visionFeaturesToText,
36
+ } from "./embeddings/index.js";
37
+ export type {
38
+ VisionFeatures,
39
+ VisionRhythm,
40
+ VisionDensity,
41
+ VisionGravity,
42
+ VisionTheme,
43
+ VisionEmbeddingServiceConfig,
44
+ VisionCacheStats,
45
+ } from "./embeddings/index.js";
46
+
47
+ // DINOv2 visual embedding exports
48
+ export { DINOv2Service, DINOV2_EMBEDDING_DIMENSION, DINOV2_INPUT_SIZE } from "./dinov2/index.js";
49
+ export type { DINOv2ServiceConfig } from "./dinov2/index.js";
50
+ export type { DINOv2WorkerMessage, DINOv2WorkerResponse } from "./dinov2/index.js";
51
+
52
+ // Search utilities exports (SearchService removed in v0.1.0)
53
+ export { calculateRRF, mergeWithRRF, normalizeRRFScore, toRankedItems } from "./search/index.js";
54
+ export { executeHybridSearch } from "./search/index.js";
55
+ export { buildFulltextConditions, buildFulltextRankExpression } from "./search/index.js";
56
+ export type {
57
+ SearchFilters,
58
+ SearchOptions,
59
+ HybridSearchOptions,
60
+ SearchResult,
61
+ SummarySearchResult,
62
+ SearchResultItem,
63
+ SummarySearchResultItem,
64
+ VectorSearchRawResult,
65
+ FullTextSearchRawResult,
66
+ RankedItem,
67
+ RRFScoredItem,
68
+ HybridSearchConfig,
69
+ HybridSearchResult,
70
+ } from "./search/index.js";
@@ -0,0 +1,45 @@
1
+ // SPDX-FileCopyrightText: 2026 TKMD and Reftrix Contributors
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ /**
5
+ * Full-text search SQL helpers for Hybrid Search.
6
+ *
7
+ * Provides shared SQL clause builders used across layout, motion,
8
+ * background, and narrative hybrid search implementations.
9
+ *
10
+ * @see TDA-HS-R2: DRY principle for fulltext SQL construction
11
+ */
12
+
13
+ /**
14
+ * Build WHERE conditions for PostgreSQL tsvector full-text search.
15
+ *
16
+ * Generates the standard three-part condition:
17
+ * 1. search_vector IS NOT NULL
18
+ * 2. plainto_tsquery('english', $N) <> ''::tsquery (skip empty queries)
19
+ * 3. search_vector @@ plainto_tsquery('english', $N) (actual match)
20
+ *
21
+ * @param searchVectorColumn - Fully qualified column name (e.g., 'se.search_vector')
22
+ * @param paramIndex - The $N parameter index for the query text
23
+ * @returns SQL condition string (without leading AND/WHERE)
24
+ */
25
+ export function buildFulltextConditions(searchVectorColumn: string, paramIndex: number): string {
26
+ return [
27
+ `${searchVectorColumn} IS NOT NULL`,
28
+ `plainto_tsquery('english', $${paramIndex}) <> ''::tsquery`,
29
+ `${searchVectorColumn} @@ plainto_tsquery('english', $${paramIndex})`,
30
+ ].join(" AND ");
31
+ }
32
+
33
+ /**
34
+ * Build ts_rank_cd expression for full-text search ranking.
35
+ *
36
+ * @param searchVectorColumn - Fully qualified column name (e.g., 'se.search_vector')
37
+ * @param paramIndex - The $N parameter index for the query text
38
+ * @returns SQL expression string for use in SELECT or ORDER BY
39
+ */
40
+ export function buildFulltextRankExpression(
41
+ searchVectorColumn: string,
42
+ paramIndex: number
43
+ ): string {
44
+ return `ts_rank_cd(${searchVectorColumn}, plainto_tsquery('english', $${paramIndex}))`;
45
+ }