hdoc-tools 0.61.0 → 0.62.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +89 -75
- package/editor/dist/assets/index-Blewr90z.css +1 -1
- package/editor/dist/assets/index-BtxvGZHW.js +111 -111
- package/editor/dist/assets/spell.worker-sryEKmOj.js +13 -13
- package/editor/dist/index.html +14 -14
- package/hdoc-build-db.js +275 -275
- package/hdoc-build-embeddings.js +202 -202
- package/hdoc-build-pdf.js +232 -232
- package/hdoc-bump.js +125 -125
- package/hdoc-create.js +110 -110
- package/hdoc-db.js +114 -114
- package/hdoc-help.js +60 -60
- package/hdoc-install-browser.js +145 -145
- package/hdoc-mermaid.js +204 -204
- package/hdoc-module.js +1102 -1102
- package/hdoc-validate-config.js +355 -355
- package/hdoc-validate-interbook.js +321 -321
- package/hdoc-validate.js +1231 -1231
- package/hdoc-ver.js +45 -45
- package/package.json +12 -1
- package/templates/doc-header-non-git.html +19 -19
- package/templates/doc-header.html +26 -26
- package/templates/init/.github/workflows/hdocbuild_onpull.yml +16 -16
- package/templates/init/.github/workflows/hdocbuild_onpush.yml +15 -15
- package/templates/init/LICENSE +21 -21
- package/templates/init/README.md +9 -9
- package/templates/init/_hdocbook/index.md +4 -4
- package/templates/init/gitignore +8 -8
- package/templates/init/resources/README.md +2 -2
- package/templates/pdf/css/custom-block.css +90 -90
- package/templates/pdf/css/fonts.css +221 -221
- package/templates/pdf/css/hdocs-pdf.css +495 -495
- package/templates/pdf/css/vars.css +404 -404
- package/templates/pdf/template-footer.html +19 -19
- package/templates/pdf/template-header.html +37 -37
- package/templates/pdf/template.html +20 -20
- package/templates/pdf-header-non-git.html +12 -12
- package/templates/pdf-header.html +16 -16
- package/ui/content/invalid-hdocbook-json.html +6 -6
- package/ui/content/invalid-hdocbook-json.md +7 -7
- package/ui/css/theme-default/styles/components/content.css +124 -124
- package/ui/css/theme-default/styles/components/sidebar.css +182 -182
- package/ui/css/theme-default/styles/htldoc.layouts.css +310 -310
- package/ui/index.html +419 -419
package/hdoc-build-embeddings.js
CHANGED
|
@@ -1,202 +1,202 @@
|
|
|
1
|
-
(() => {
|
|
2
|
-
const path = require("node:path");
|
|
3
|
-
const fs = require("node:fs");
|
|
4
|
-
const crypto = require("node:crypto");
|
|
5
|
-
|
|
6
|
-
// Model identity - must match what the docs service query embedder uses
|
|
7
|
-
// (see embedder.h in esp-docs-service). The service refuses embeddings
|
|
8
|
-
// built with a different model or dimension count.
|
|
9
|
-
const MODEL_ID = "all-MiniLM-L6-v2";
|
|
10
|
-
const HF_MODEL = "Xenova/all-MiniLM-L6-v2";
|
|
11
|
-
const DIMS = 384;
|
|
12
|
-
|
|
13
|
-
// SHA-256 of onnx/model.onnx as downloaded from Hugging Face. Matches the
|
|
14
|
-
// vendored copy the docs service runs (search_model/model.onnx). Verified
|
|
15
|
-
// after download, before any embeddings are generated - a hash mismatch
|
|
16
|
-
// (tampered/corrupt/re-published model) aborts the embedding step.
|
|
17
|
-
const MODEL_SHA256 = "759c3cd2b7fe7e93933ad23c4c9181b7396442a2ed746ec7c1d46192c469c46e";
|
|
18
|
-
|
|
19
|
-
// Chunking parameters (word based). MiniLM attends to ~256 wordpiece
|
|
20
|
-
// tokens; 180 words plus the title prefix stays comfortably inside that.
|
|
21
|
-
const CHUNK_WORDS = 180;
|
|
22
|
-
const CHUNK_OVERLAP_WORDS = 30;
|
|
23
|
-
const MAX_CHUNKS_PER_DOC = 40;
|
|
24
|
-
const EMBED_BATCH_SIZE = 16;
|
|
25
|
-
|
|
26
|
-
// Maximum characters of chunk text stored for use as a result snippet
|
|
27
|
-
const STORED_CHUNK_CHARS = 500;
|
|
28
|
-
|
|
29
|
-
// Split document plain text into overlapping word-window chunks
|
|
30
|
-
const chunk_text = (text) => {
|
|
31
|
-
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
32
|
-
if (words.length === 0) return [];
|
|
33
|
-
|
|
34
|
-
const chunks = [];
|
|
35
|
-
let start = 0;
|
|
36
|
-
while (start < words.length && chunks.length < MAX_CHUNKS_PER_DOC) {
|
|
37
|
-
const end = Math.min(start + CHUNK_WORDS, words.length);
|
|
38
|
-
chunks.push(words.slice(start, end).join(" "));
|
|
39
|
-
if (end >= words.length) break;
|
|
40
|
-
start = end - CHUNK_OVERLAP_WORDS;
|
|
41
|
-
}
|
|
42
|
-
return chunks;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
// Compute the resource_url for an index record. Must stay in step with
|
|
46
|
-
// the equivalent logic in populate_index (hdoc-build-db.js), as the docs
|
|
47
|
-
// service joins hdoc_embeddings to hdoc_index on this value.
|
|
48
|
-
const resource_url_for = (file) => {
|
|
49
|
-
let index_path_name = file.relative_path.replaceAll("\\", "/");
|
|
50
|
-
if (
|
|
51
|
-
index_path_name.endsWith("/index.md") ||
|
|
52
|
-
index_path_name.endsWith("/index.html") ||
|
|
53
|
-
index_path_name.endsWith("/index.htm")
|
|
54
|
-
) {
|
|
55
|
-
index_path_name = index_path_name.substring(0, index_path_name.lastIndexOf("/"));
|
|
56
|
-
}
|
|
57
|
-
index_path_name = `/${index_path_name.replace(path.extname(file.relative_path), "")}`;
|
|
58
|
-
return file.index_html.id !== null
|
|
59
|
-
? `${index_path_name}#${file.index_html.id}`
|
|
60
|
-
: index_path_name;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
// Generate semantic search embeddings for every indexed document and
|
|
64
|
-
// write them to the hdoc_embeddings table. Non-fatal on failure - a book
|
|
65
|
-
// without embeddings remains fully searchable via keyword search.
|
|
66
|
-
exports.populate_embeddings = async (db, doc_id, index_records, verbose = false) => {
|
|
67
|
-
const response = {
|
|
68
|
-
success: false,
|
|
69
|
-
error: "",
|
|
70
|
-
doc_count: 0,
|
|
71
|
-
chunk_count: 0,
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
let transformers = null;
|
|
75
|
-
try {
|
|
76
|
-
// @huggingface/transformers is ESM-only, load it dynamically
|
|
77
|
-
transformers = await import("@huggingface/transformers");
|
|
78
|
-
} catch (e) {
|
|
79
|
-
response.error = `Could not load @huggingface/transformers: ${e.message}`;
|
|
80
|
-
return response;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Keep the model cache inside the package, like the puppeteer browser
|
|
84
|
-
transformers.env.cacheDir = path.join(__dirname, ".model-cache");
|
|
85
|
-
|
|
86
|
-
console.log(`\nGenerating semantic search embeddings (${MODEL_ID})...`);
|
|
87
|
-
|
|
88
|
-
let extractor = null;
|
|
89
|
-
try {
|
|
90
|
-
// fp32 to match the model the docs service runs at query time
|
|
91
|
-
extractor = await transformers.pipeline("feature-extraction", HF_MODEL, {
|
|
92
|
-
dtype: "fp32",
|
|
93
|
-
});
|
|
94
|
-
} catch (e) {
|
|
95
|
-
response.error = `Could not load embedding model ${HF_MODEL}: ${e.message}`;
|
|
96
|
-
return response;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Verify the downloaded/cached model file against the pinned hash
|
|
100
|
-
// before generating any embeddings
|
|
101
|
-
const model_file = path.join(transformers.env.cacheDir, HF_MODEL, "onnx", "model.onnx");
|
|
102
|
-
try {
|
|
103
|
-
const hash = crypto
|
|
104
|
-
.createHash("sha256")
|
|
105
|
-
.update(fs.readFileSync(model_file))
|
|
106
|
-
.digest("hex");
|
|
107
|
-
if (hash !== MODEL_SHA256) {
|
|
108
|
-
response.error = `Embedding model checksum mismatch for ${model_file}: got ${hash}, expected ${MODEL_SHA256}. Delete the .model-cache folder to re-download, or update MODEL_SHA256 if the pinned model has intentionally changed.`;
|
|
109
|
-
return response;
|
|
110
|
-
}
|
|
111
|
-
} catch (e) {
|
|
112
|
-
response.error = `Could not verify embedding model checksum: ${e.message}`;
|
|
113
|
-
return response;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Build the chunk list across all documents
|
|
117
|
-
const chunks = [];
|
|
118
|
-
for (const file of index_records) {
|
|
119
|
-
if (file.inline) continue;
|
|
120
|
-
if (file.status === "draft") continue;
|
|
121
|
-
if (!file.index_html || !file.index_html.text) continue;
|
|
122
|
-
|
|
123
|
-
const url = resource_url_for(file);
|
|
124
|
-
const title = (file.index_html.fm_props && file.index_html.fm_props.title) || "";
|
|
125
|
-
|
|
126
|
-
const doc_chunks = chunk_text(file.index_html.text);
|
|
127
|
-
if (doc_chunks.length === 0) continue;
|
|
128
|
-
|
|
129
|
-
response.doc_count++;
|
|
130
|
-
for (let seq = 0; seq < doc_chunks.length; seq++) {
|
|
131
|
-
chunks.push({
|
|
132
|
-
url: url,
|
|
133
|
-
seq: seq,
|
|
134
|
-
text: doc_chunks[seq],
|
|
135
|
-
// Title provides context for the embedding but is not
|
|
136
|
-
// duplicated into the stored snippet text
|
|
137
|
-
embed_input: title ? `${title}\n${doc_chunks[seq]}` : doc_chunks[seq],
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (chunks.length === 0) {
|
|
143
|
-
response.success = true;
|
|
144
|
-
console.log("No documents to embed.");
|
|
145
|
-
return response;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const stmt = db.prepare(
|
|
149
|
-
"INSERT INTO hdoc_embeddings (resource_url, chunk_seq, chunk_text, embedding, model, dims) VALUES (?, ?, ?, ?, ?, ?)",
|
|
150
|
-
);
|
|
151
|
-
const insert_batch = db.transaction((rows) => {
|
|
152
|
-
for (const row of rows) {
|
|
153
|
-
stmt.run(row.url, row.seq, row.text.substring(0, STORED_CHUNK_CHARS), row.embedding, MODEL_ID, DIMS);
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
const start_time = Date.now();
|
|
158
|
-
|
|
159
|
-
try {
|
|
160
|
-
for (let i = 0; i < chunks.length; i += EMBED_BATCH_SIZE) {
|
|
161
|
-
const batch = chunks.slice(i, i + EMBED_BATCH_SIZE);
|
|
162
|
-
|
|
163
|
-
const output = await extractor(
|
|
164
|
-
batch.map((c) => c.embed_input),
|
|
165
|
-
{ pooling: "mean", normalize: true },
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
// output is a [batch, DIMS] tensor over one flat Float32Array
|
|
169
|
-
const data = output.data;
|
|
170
|
-
const dims = output.dims[output.dims.length - 1];
|
|
171
|
-
if (dims !== DIMS) {
|
|
172
|
-
response.error = `Embedding model returned ${dims} dimensions, expected ${DIMS}`;
|
|
173
|
-
return response;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
for (let j = 0; j < batch.length; j++) {
|
|
177
|
-
// Copy the row out of the shared tensor buffer
|
|
178
|
-
const vec = new Float32Array(data.slice(j * DIMS, (j + 1) * DIMS));
|
|
179
|
-
batch[j].embedding = Buffer.from(vec.buffer, 0, DIMS * 4);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
insert_batch(batch);
|
|
183
|
-
response.chunk_count += batch.length;
|
|
184
|
-
|
|
185
|
-
if (verbose || i % (EMBED_BATCH_SIZE * 10) === 0) {
|
|
186
|
-
process.stdout.write(`\rEmbedding chunks: ${Math.min(i + EMBED_BATCH_SIZE, chunks.length)}/${chunks.length}`);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
} catch (e) {
|
|
190
|
-
response.error = `Embedding generation failed: ${e.message}`;
|
|
191
|
-
return response;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const secs = Math.round((Date.now() - start_time) / 1000);
|
|
195
|
-
console.log(
|
|
196
|
-
`\rEmbedding Build Complete: ${response.chunk_count} chunks across ${response.doc_count} documents in ${secs}s.`,
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
response.success = true;
|
|
200
|
-
return response;
|
|
201
|
-
};
|
|
202
|
-
})();
|
|
1
|
+
(() => {
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const crypto = require("node:crypto");
|
|
5
|
+
|
|
6
|
+
// Model identity - must match what the docs service query embedder uses
|
|
7
|
+
// (see embedder.h in esp-docs-service). The service refuses embeddings
|
|
8
|
+
// built with a different model or dimension count.
|
|
9
|
+
const MODEL_ID = "all-MiniLM-L6-v2";
|
|
10
|
+
const HF_MODEL = "Xenova/all-MiniLM-L6-v2";
|
|
11
|
+
const DIMS = 384;
|
|
12
|
+
|
|
13
|
+
// SHA-256 of onnx/model.onnx as downloaded from Hugging Face. Matches the
|
|
14
|
+
// vendored copy the docs service runs (search_model/model.onnx). Verified
|
|
15
|
+
// after download, before any embeddings are generated - a hash mismatch
|
|
16
|
+
// (tampered/corrupt/re-published model) aborts the embedding step.
|
|
17
|
+
const MODEL_SHA256 = "759c3cd2b7fe7e93933ad23c4c9181b7396442a2ed746ec7c1d46192c469c46e";
|
|
18
|
+
|
|
19
|
+
// Chunking parameters (word based). MiniLM attends to ~256 wordpiece
|
|
20
|
+
// tokens; 180 words plus the title prefix stays comfortably inside that.
|
|
21
|
+
const CHUNK_WORDS = 180;
|
|
22
|
+
const CHUNK_OVERLAP_WORDS = 30;
|
|
23
|
+
const MAX_CHUNKS_PER_DOC = 40;
|
|
24
|
+
const EMBED_BATCH_SIZE = 16;
|
|
25
|
+
|
|
26
|
+
// Maximum characters of chunk text stored for use as a result snippet
|
|
27
|
+
const STORED_CHUNK_CHARS = 500;
|
|
28
|
+
|
|
29
|
+
// Split document plain text into overlapping word-window chunks
|
|
30
|
+
const chunk_text = (text) => {
|
|
31
|
+
const words = text.split(/\s+/).filter((w) => w.length > 0);
|
|
32
|
+
if (words.length === 0) return [];
|
|
33
|
+
|
|
34
|
+
const chunks = [];
|
|
35
|
+
let start = 0;
|
|
36
|
+
while (start < words.length && chunks.length < MAX_CHUNKS_PER_DOC) {
|
|
37
|
+
const end = Math.min(start + CHUNK_WORDS, words.length);
|
|
38
|
+
chunks.push(words.slice(start, end).join(" "));
|
|
39
|
+
if (end >= words.length) break;
|
|
40
|
+
start = end - CHUNK_OVERLAP_WORDS;
|
|
41
|
+
}
|
|
42
|
+
return chunks;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Compute the resource_url for an index record. Must stay in step with
|
|
46
|
+
// the equivalent logic in populate_index (hdoc-build-db.js), as the docs
|
|
47
|
+
// service joins hdoc_embeddings to hdoc_index on this value.
|
|
48
|
+
const resource_url_for = (file) => {
|
|
49
|
+
let index_path_name = file.relative_path.replaceAll("\\", "/");
|
|
50
|
+
if (
|
|
51
|
+
index_path_name.endsWith("/index.md") ||
|
|
52
|
+
index_path_name.endsWith("/index.html") ||
|
|
53
|
+
index_path_name.endsWith("/index.htm")
|
|
54
|
+
) {
|
|
55
|
+
index_path_name = index_path_name.substring(0, index_path_name.lastIndexOf("/"));
|
|
56
|
+
}
|
|
57
|
+
index_path_name = `/${index_path_name.replace(path.extname(file.relative_path), "")}`;
|
|
58
|
+
return file.index_html.id !== null
|
|
59
|
+
? `${index_path_name}#${file.index_html.id}`
|
|
60
|
+
: index_path_name;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Generate semantic search embeddings for every indexed document and
|
|
64
|
+
// write them to the hdoc_embeddings table. Non-fatal on failure - a book
|
|
65
|
+
// without embeddings remains fully searchable via keyword search.
|
|
66
|
+
exports.populate_embeddings = async (db, doc_id, index_records, verbose = false) => {
|
|
67
|
+
const response = {
|
|
68
|
+
success: false,
|
|
69
|
+
error: "",
|
|
70
|
+
doc_count: 0,
|
|
71
|
+
chunk_count: 0,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
let transformers = null;
|
|
75
|
+
try {
|
|
76
|
+
// @huggingface/transformers is ESM-only, load it dynamically
|
|
77
|
+
transformers = await import("@huggingface/transformers");
|
|
78
|
+
} catch (e) {
|
|
79
|
+
response.error = `Could not load @huggingface/transformers: ${e.message}`;
|
|
80
|
+
return response;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Keep the model cache inside the package, like the puppeteer browser
|
|
84
|
+
transformers.env.cacheDir = path.join(__dirname, ".model-cache");
|
|
85
|
+
|
|
86
|
+
console.log(`\nGenerating semantic search embeddings (${MODEL_ID})...`);
|
|
87
|
+
|
|
88
|
+
let extractor = null;
|
|
89
|
+
try {
|
|
90
|
+
// fp32 to match the model the docs service runs at query time
|
|
91
|
+
extractor = await transformers.pipeline("feature-extraction", HF_MODEL, {
|
|
92
|
+
dtype: "fp32",
|
|
93
|
+
});
|
|
94
|
+
} catch (e) {
|
|
95
|
+
response.error = `Could not load embedding model ${HF_MODEL}: ${e.message}`;
|
|
96
|
+
return response;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Verify the downloaded/cached model file against the pinned hash
|
|
100
|
+
// before generating any embeddings
|
|
101
|
+
const model_file = path.join(transformers.env.cacheDir, HF_MODEL, "onnx", "model.onnx");
|
|
102
|
+
try {
|
|
103
|
+
const hash = crypto
|
|
104
|
+
.createHash("sha256")
|
|
105
|
+
.update(fs.readFileSync(model_file))
|
|
106
|
+
.digest("hex");
|
|
107
|
+
if (hash !== MODEL_SHA256) {
|
|
108
|
+
response.error = `Embedding model checksum mismatch for ${model_file}: got ${hash}, expected ${MODEL_SHA256}. Delete the .model-cache folder to re-download, or update MODEL_SHA256 if the pinned model has intentionally changed.`;
|
|
109
|
+
return response;
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
response.error = `Could not verify embedding model checksum: ${e.message}`;
|
|
113
|
+
return response;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Build the chunk list across all documents
|
|
117
|
+
const chunks = [];
|
|
118
|
+
for (const file of index_records) {
|
|
119
|
+
if (file.inline) continue;
|
|
120
|
+
if (file.status === "draft") continue;
|
|
121
|
+
if (!file.index_html || !file.index_html.text) continue;
|
|
122
|
+
|
|
123
|
+
const url = resource_url_for(file);
|
|
124
|
+
const title = (file.index_html.fm_props && file.index_html.fm_props.title) || "";
|
|
125
|
+
|
|
126
|
+
const doc_chunks = chunk_text(file.index_html.text);
|
|
127
|
+
if (doc_chunks.length === 0) continue;
|
|
128
|
+
|
|
129
|
+
response.doc_count++;
|
|
130
|
+
for (let seq = 0; seq < doc_chunks.length; seq++) {
|
|
131
|
+
chunks.push({
|
|
132
|
+
url: url,
|
|
133
|
+
seq: seq,
|
|
134
|
+
text: doc_chunks[seq],
|
|
135
|
+
// Title provides context for the embedding but is not
|
|
136
|
+
// duplicated into the stored snippet text
|
|
137
|
+
embed_input: title ? `${title}\n${doc_chunks[seq]}` : doc_chunks[seq],
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (chunks.length === 0) {
|
|
143
|
+
response.success = true;
|
|
144
|
+
console.log("No documents to embed.");
|
|
145
|
+
return response;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const stmt = db.prepare(
|
|
149
|
+
"INSERT INTO hdoc_embeddings (resource_url, chunk_seq, chunk_text, embedding, model, dims) VALUES (?, ?, ?, ?, ?, ?)",
|
|
150
|
+
);
|
|
151
|
+
const insert_batch = db.transaction((rows) => {
|
|
152
|
+
for (const row of rows) {
|
|
153
|
+
stmt.run(row.url, row.seq, row.text.substring(0, STORED_CHUNK_CHARS), row.embedding, MODEL_ID, DIMS);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const start_time = Date.now();
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
for (let i = 0; i < chunks.length; i += EMBED_BATCH_SIZE) {
|
|
161
|
+
const batch = chunks.slice(i, i + EMBED_BATCH_SIZE);
|
|
162
|
+
|
|
163
|
+
const output = await extractor(
|
|
164
|
+
batch.map((c) => c.embed_input),
|
|
165
|
+
{ pooling: "mean", normalize: true },
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// output is a [batch, DIMS] tensor over one flat Float32Array
|
|
169
|
+
const data = output.data;
|
|
170
|
+
const dims = output.dims[output.dims.length - 1];
|
|
171
|
+
if (dims !== DIMS) {
|
|
172
|
+
response.error = `Embedding model returned ${dims} dimensions, expected ${DIMS}`;
|
|
173
|
+
return response;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (let j = 0; j < batch.length; j++) {
|
|
177
|
+
// Copy the row out of the shared tensor buffer
|
|
178
|
+
const vec = new Float32Array(data.slice(j * DIMS, (j + 1) * DIMS));
|
|
179
|
+
batch[j].embedding = Buffer.from(vec.buffer, 0, DIMS * 4);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
insert_batch(batch);
|
|
183
|
+
response.chunk_count += batch.length;
|
|
184
|
+
|
|
185
|
+
if (verbose || i % (EMBED_BATCH_SIZE * 10) === 0) {
|
|
186
|
+
process.stdout.write(`\rEmbedding chunks: ${Math.min(i + EMBED_BATCH_SIZE, chunks.length)}/${chunks.length}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch (e) {
|
|
190
|
+
response.error = `Embedding generation failed: ${e.message}`;
|
|
191
|
+
return response;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const secs = Math.round((Date.now() - start_time) / 1000);
|
|
195
|
+
console.log(
|
|
196
|
+
`\rEmbedding Build Complete: ${response.chunk_count} chunks across ${response.doc_count} documents in ${secs}s.`,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
response.success = true;
|
|
200
|
+
return response;
|
|
201
|
+
};
|
|
202
|
+
})();
|