eddie-jekyll 0.2.0 → 0.2.4
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.
- checksums.yaml +4 -4
- data/assets/eddie-wasm.js +460 -0
- data/assets/eddie-widget.js +1043 -0
- data/assets/eddie-worker.js +226 -0
- data/assets/eddie.wasm +0 -0
- data/exe/eddie-jekyll-install +8 -2
- data/lib/eddie/jekyll.rb +1 -1
- data/scripts/install.sh +14 -2
- metadata +26 -2
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
|
|
3
|
+
// Eddie Web Worker
|
|
4
|
+
//
|
|
5
|
+
// Loads WASM module, downloads and caches the ML model in IndexedDB,
|
|
6
|
+
// and handles search queries from the main thread.
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
// -- Configuration --
|
|
11
|
+
const HF_CDN = "https://huggingface.co";
|
|
12
|
+
const MODEL_FILES = ["config.json", "tokenizer.json", "model.safetensors"];
|
|
13
|
+
const IDB_NAME = "eddie-models";
|
|
14
|
+
const IDB_STORE = "files";
|
|
15
|
+
|
|
16
|
+
// -- State --
|
|
17
|
+
let baseUrl = "";
|
|
18
|
+
let initialized = false;
|
|
19
|
+
|
|
20
|
+
// -- Message handler --
|
|
21
|
+
self.onmessage = async function (e) {
|
|
22
|
+
const msg = e.data;
|
|
23
|
+
|
|
24
|
+
if (msg.type === "init") {
|
|
25
|
+
try {
|
|
26
|
+
await initialize(msg.indexUrl, msg.baseUrl);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
postStatus("error", { error: err.message || String(err) });
|
|
29
|
+
}
|
|
30
|
+
} else if (msg.type === "search") {
|
|
31
|
+
try {
|
|
32
|
+
if (!initialized) {
|
|
33
|
+
throw new Error("Engine not initialized");
|
|
34
|
+
}
|
|
35
|
+
const mode = msg.mode || "hybrid";
|
|
36
|
+
const topK = msg.topK || 5;
|
|
37
|
+
if (typeof wasm_bindgen.search_with_answer !== "function") {
|
|
38
|
+
throw new Error("WASM does not expose search_with_answer");
|
|
39
|
+
}
|
|
40
|
+
const bundle = wasm_bindgen.search_with_answer(
|
|
41
|
+
msg.query,
|
|
42
|
+
topK,
|
|
43
|
+
msg.answerTopK || 5,
|
|
44
|
+
mode,
|
|
45
|
+
!!msg.answerMode,
|
|
46
|
+
msg.qaSubject || ""
|
|
47
|
+
);
|
|
48
|
+
self.postMessage({
|
|
49
|
+
type: "search_result",
|
|
50
|
+
requestId: msg.requestId,
|
|
51
|
+
results: bundle.results || [],
|
|
52
|
+
answer: bundle.answer || null,
|
|
53
|
+
});
|
|
54
|
+
} catch (err) {
|
|
55
|
+
// Keep UX alive on occasional wasm/runtime faults by degrading to keyword lane.
|
|
56
|
+
try {
|
|
57
|
+
if (typeof wasm_bindgen.search_with_answer !== "function") {
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
const fallback = wasm_bindgen.search_with_answer(
|
|
61
|
+
msg.query,
|
|
62
|
+
msg.topK || 5,
|
|
63
|
+
msg.answerTopK || 5,
|
|
64
|
+
"keyword",
|
|
65
|
+
false,
|
|
66
|
+
msg.qaSubject || ""
|
|
67
|
+
);
|
|
68
|
+
self.postMessage({
|
|
69
|
+
type: "search_result",
|
|
70
|
+
requestId: msg.requestId,
|
|
71
|
+
results: fallback.results || [],
|
|
72
|
+
answer: null,
|
|
73
|
+
degraded: true,
|
|
74
|
+
laneError: err.message || String(err),
|
|
75
|
+
});
|
|
76
|
+
} catch (fallbackErr) {
|
|
77
|
+
self.postMessage({
|
|
78
|
+
type: "error",
|
|
79
|
+
requestId: msg.requestId,
|
|
80
|
+
error: fallbackErr.message || fallbackErr.toString(),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
async function initialize(indexUrl, workerBaseUrl) {
|
|
88
|
+
baseUrl = workerBaseUrl || "";
|
|
89
|
+
|
|
90
|
+
// 1. Load WASM glue + instantiate
|
|
91
|
+
postStatus("loading_wasm");
|
|
92
|
+
const wasmGlueUrl = resolveUrl("eddie-wasm.js");
|
|
93
|
+
importScripts(wasmGlueUrl);
|
|
94
|
+
const wasmBinaryUrl = resolveUrl("eddie.wasm");
|
|
95
|
+
await wasm_bindgen(wasmBinaryUrl);
|
|
96
|
+
|
|
97
|
+
// 2. Fetch index
|
|
98
|
+
postStatus("loading_index");
|
|
99
|
+
const indexResponse = await fetch(indexUrl);
|
|
100
|
+
if (!indexResponse.ok) {
|
|
101
|
+
throw new Error(`Failed to fetch index: ${indexResponse.status}`);
|
|
102
|
+
}
|
|
103
|
+
const indexBytes = new Uint8Array(await indexResponse.arrayBuffer());
|
|
104
|
+
|
|
105
|
+
// 3. Parse model ID from index bytes (supports raw .bin and compressed .ed)
|
|
106
|
+
const modelId = wasm_bindgen.extract_model_id(indexBytes);
|
|
107
|
+
|
|
108
|
+
// 4. Fetch model files (with IndexedDB cache)
|
|
109
|
+
postStatus("checking_cache");
|
|
110
|
+
const db = await openModelDB();
|
|
111
|
+
|
|
112
|
+
const config = await getCachedOrFetch(
|
|
113
|
+
db,
|
|
114
|
+
modelId,
|
|
115
|
+
"config.json",
|
|
116
|
+
(loaded, total) => postStatus("downloading_model", { progress: loaded / total, file: "config.json" })
|
|
117
|
+
);
|
|
118
|
+
const tokenizer = await getCachedOrFetch(
|
|
119
|
+
db,
|
|
120
|
+
modelId,
|
|
121
|
+
"tokenizer.json",
|
|
122
|
+
(loaded, total) => postStatus("downloading_model", { progress: loaded / total, file: "tokenizer.json" })
|
|
123
|
+
);
|
|
124
|
+
const weights = await getCachedOrFetch(
|
|
125
|
+
db,
|
|
126
|
+
modelId,
|
|
127
|
+
"model.safetensors",
|
|
128
|
+
(loaded, total) => postStatus("downloading_model", { progress: loaded / total, file: "model.safetensors" })
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// 5. Initialize WASM engine
|
|
132
|
+
postStatus("initializing");
|
|
133
|
+
wasm_bindgen.init_engine(
|
|
134
|
+
new Uint8Array(config),
|
|
135
|
+
new Uint8Array(tokenizer),
|
|
136
|
+
new Uint8Array(weights),
|
|
137
|
+
indexBytes
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
initialized = true;
|
|
141
|
+
postStatus("ready");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// -- IndexedDB helpers --
|
|
145
|
+
function openModelDB() {
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
const req = indexedDB.open(IDB_NAME, 1);
|
|
148
|
+
req.onupgradeneeded = () => {
|
|
149
|
+
req.result.createObjectStore(IDB_STORE);
|
|
150
|
+
};
|
|
151
|
+
req.onsuccess = () => resolve(req.result);
|
|
152
|
+
req.onerror = () => reject(req.error);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function idbGet(db, key) {
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
const tx = db.transaction(IDB_STORE, "readonly");
|
|
159
|
+
const req = tx.objectStore(IDB_STORE).get(key);
|
|
160
|
+
req.onsuccess = () => resolve(req.result);
|
|
161
|
+
req.onerror = () => reject(req.error);
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function idbPut(db, key, value) {
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
const tx = db.transaction(IDB_STORE, "readwrite");
|
|
168
|
+
const req = tx.objectStore(IDB_STORE).put(value, key);
|
|
169
|
+
req.onsuccess = () => resolve();
|
|
170
|
+
req.onerror = () => reject(req.error);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function getCachedOrFetch(db, modelId, filename, onProgress) {
|
|
175
|
+
const key = `${modelId}/${filename}`;
|
|
176
|
+
const cached = await idbGet(db, key);
|
|
177
|
+
if (cached) {
|
|
178
|
+
return cached;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const url = `${HF_CDN}/${modelId}/resolve/main/${filename}`;
|
|
182
|
+
const response = await fetch(url);
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
throw new Error(`Failed to download ${filename}: ${response.status}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const contentLength = parseInt(response.headers.get("Content-Length") || "0", 10);
|
|
188
|
+
const reader = response.body.getReader();
|
|
189
|
+
const chunks = [];
|
|
190
|
+
let loaded = 0;
|
|
191
|
+
|
|
192
|
+
while (true) {
|
|
193
|
+
const { done, value } = await reader.read();
|
|
194
|
+
if (done) break;
|
|
195
|
+
chunks.push(value);
|
|
196
|
+
loaded += value.length;
|
|
197
|
+
if (contentLength > 0) {
|
|
198
|
+
onProgress(loaded, contentLength);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Concatenate chunks into single ArrayBuffer
|
|
203
|
+
const total = chunks.reduce((sum, c) => sum + c.length, 0);
|
|
204
|
+
const result = new Uint8Array(total);
|
|
205
|
+
let offset = 0;
|
|
206
|
+
for (const chunk of chunks) {
|
|
207
|
+
result.set(chunk, offset);
|
|
208
|
+
offset += chunk.length;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const buffer = result.buffer;
|
|
212
|
+
await idbPut(db, key, buffer);
|
|
213
|
+
return buffer;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// -- Utilities --
|
|
217
|
+
function resolveUrl(filename) {
|
|
218
|
+
if (baseUrl) {
|
|
219
|
+
return baseUrl.replace(/\/$/, "") + "/" + filename;
|
|
220
|
+
}
|
|
221
|
+
return filename;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function postStatus(state, extra) {
|
|
225
|
+
self.postMessage({ type: "status", state, ...extra });
|
|
226
|
+
}
|
data/assets/eddie.wasm
ADDED
|
Binary file
|
data/exe/eddie-jekyll-install
CHANGED
|
@@ -11,6 +11,12 @@ end
|
|
|
11
11
|
|
|
12
12
|
root = Pathname.new(__dir__).join("..").expand_path
|
|
13
13
|
script = root.join("scripts", "install.sh")
|
|
14
|
-
|
|
14
|
+
env = {
|
|
15
|
+
"EDDIE_PACKAGE_ROOT" => root.to_s
|
|
16
|
+
}
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
if ARGV[1]
|
|
19
|
+
exec(env, "bash", script.to_s, File.expand_path(site_dir), File.expand_path(ARGV[1]))
|
|
20
|
+
else
|
|
21
|
+
exec(env, "bash", script.to_s, File.expand_path(site_dir))
|
|
22
|
+
end
|
data/lib/eddie/jekyll.rb
CHANGED
data/scripts/install.sh
CHANGED
|
@@ -2,7 +2,19 @@
|
|
|
2
2
|
set -euo pipefail
|
|
3
3
|
|
|
4
4
|
SITE_DIR="${1:?usage: install.sh <jekyll-site-dir>}"
|
|
5
|
-
ASSET_ROOT="${2
|
|
5
|
+
ASSET_ROOT="${2:-}"
|
|
6
|
+
PACKAGE_ROOT="${EDDIE_PACKAGE_ROOT:-}"
|
|
7
|
+
ASSETS=(eddie-widget.js eddie-worker.js eddie-wasm.js eddie.wasm)
|
|
8
|
+
|
|
9
|
+
if [[ -z "$ASSET_ROOT" && -n "$PACKAGE_ROOT" ]]; then
|
|
10
|
+
ASSET_ROOT="$PACKAGE_ROOT/assets"
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
if [[ -z "$ASSET_ROOT" ]]; then
|
|
14
|
+
echo "No asset root provided and no packaged assets found." >&2
|
|
15
|
+
echo "Pass an explicit asset-root or set EDDIE_PACKAGE_ROOT." >&2
|
|
16
|
+
exit 1
|
|
17
|
+
fi
|
|
6
18
|
|
|
7
19
|
require_asset() {
|
|
8
20
|
local asset_name="$1"
|
|
@@ -13,7 +25,7 @@ require_asset() {
|
|
|
13
25
|
fi
|
|
14
26
|
}
|
|
15
27
|
|
|
16
|
-
for asset in
|
|
28
|
+
for asset in "${ASSETS[@]}"; do
|
|
17
29
|
require_asset "$asset"
|
|
18
30
|
done
|
|
19
31
|
|
metadata
CHANGED
|
@@ -1,14 +1,34 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: eddie-jekyll
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jason Grey
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: jt55401-eddie-cli
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.2'
|
|
19
|
+
- - ">="
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: 0.2.4
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - "~>"
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '0.2'
|
|
29
|
+
- - ">="
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: 0.2.4
|
|
12
32
|
description: Provides a CLI helper that runs Eddie's Jekyll installer script.
|
|
13
33
|
executables:
|
|
14
34
|
- eddie-jekyll-install
|
|
@@ -17,6 +37,10 @@ extra_rdoc_files: []
|
|
|
17
37
|
files:
|
|
18
38
|
- LICENSE.txt
|
|
19
39
|
- README.md
|
|
40
|
+
- assets/eddie-wasm.js
|
|
41
|
+
- assets/eddie-widget.js
|
|
42
|
+
- assets/eddie-worker.js
|
|
43
|
+
- assets/eddie.wasm
|
|
20
44
|
- exe/eddie-jekyll-install
|
|
21
45
|
- lib/eddie/jekyll.rb
|
|
22
46
|
- scripts/install.sh
|