@kelceyp/caw-server 1.0.189 → 1.0.191

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.
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * embedding-worker.js — Standalone Node.js child process for embedding generation.
4
+ *
5
+ * Used as a fallback when Bun's ONNX runtime cannot load @huggingface/transformers
6
+ * natively. Spawned by EmbeddingProvider when the native path fails.
7
+ *
8
+ * IPC protocol (JSON-lines over stdin/stdout):
9
+ * Startup: sends { ready: true } after pipeline is loaded
10
+ * sends { error: "..." } if startup fails
11
+ * Request: parent → worker: { texts: ["...", "..."] }\n
12
+ * Response: worker → parent: { vectors: [[...], ...] }\n
13
+ * or { error: "..." }\n
14
+ *
15
+ * Vectors are raw (un-normalised) Float32Arrays serialised as plain number arrays.
16
+ * Normalisation is performed by EmbeddingProvider after receiving the response.
17
+ *
18
+ * This script is intentionally standalone — no imports from the CAW codebase.
19
+ * It runs in Node.js, not Bun, to avoid the ONNX runtime incompatibility.
20
+ */
21
+
22
+ import { createInterface } from 'readline';
23
+
24
+ const MODEL_NAME = process.env.EMBEDDING_MODEL || 'nomic-ai/nomic-embed-text-v1.5';
25
+ const CACHE_DIR = process.env.EMBEDDING_CACHE_DIR || null;
26
+
27
+ const sendLine = (obj) => {
28
+ process.stdout.write(JSON.stringify(obj) + '\n');
29
+ };
30
+
31
+ const run = async () => {
32
+ let pipe;
33
+
34
+ try {
35
+ const { pipeline, env } = await import('@huggingface/transformers');
36
+
37
+ if (CACHE_DIR) {
38
+ env.cacheDir = CACHE_DIR;
39
+ }
40
+
41
+ // Log download progress to stderr (visible in parent process output)
42
+ pipe = await pipeline('feature-extraction', MODEL_NAME, {
43
+ quantized: true,
44
+ progress_callback: (progress) => {
45
+ if (progress.status === 'download') {
46
+ process.stderr.write(`[EmbeddingWorker] Downloading: ${progress.file} (${Math.round((progress.loaded / progress.total) * 100)}%)\n`);
47
+ } else if (progress.status === 'ready') {
48
+ process.stderr.write(`[EmbeddingWorker] Ready: ${progress.file}\n`);
49
+ }
50
+ }
51
+ });
52
+
53
+ sendLine({ ready: true });
54
+ } catch (err) {
55
+ sendLine({ error: `Pipeline load failed: ${err.message}` });
56
+ process.exit(1);
57
+ }
58
+
59
+ // Process requests from parent line by line
60
+ const rl = createInterface({ input: process.stdin });
61
+
62
+ rl.on('line', async (line) => {
63
+ let req;
64
+ try {
65
+ req = JSON.parse(line);
66
+ } catch {
67
+ sendLine({ error: `Invalid JSON request: ${line}` });
68
+ return;
69
+ }
70
+
71
+ const { texts } = req;
72
+ if (!Array.isArray(texts)) {
73
+ sendLine({ error: 'Request must have a texts array' });
74
+ return;
75
+ }
76
+
77
+ try {
78
+ const output = await pipe(texts, { pooling: 'mean', normalize: false });
79
+ // Serialise: output.data is a flat Float32Array [batch * dims]
80
+ const dims = output.dims[output.dims.length - 1];
81
+ const vectors = [];
82
+ for (let i = 0; i < texts.length; i++) {
83
+ const start = i * dims;
84
+ const end = start + dims;
85
+ vectors.push(Array.from(output.data.slice(start, end)));
86
+ }
87
+ sendLine({ vectors });
88
+ } catch (err) {
89
+ sendLine({ error: `Embedding failed: ${err.message}` });
90
+ }
91
+ });
92
+
93
+ rl.on('close', () => {
94
+ process.exit(0);
95
+ });
96
+ };
97
+
98
+ run();