fiptcha 0.1.0
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 +201 -0
- package/README.md +243 -0
- package/index.js +12 -0
- package/package.json +32 -0
- package/scripts/captcha-companion-requirements.txt +8 -0
- package/scripts/captcha-companion.js +222 -0
- package/scripts/captcha-container-probe.js +27 -0
- package/scripts/captcha-mlx-worker.py +66 -0
- package/scripts/install-captcha-companion.js +47 -0
- package/src/captcha-benchmark.js +25 -0
- package/src/captcha-companion-client.js +74 -0
- package/src/captcha-grid-solver.js +158 -0
- package/src/captcha-interceptor.js +39 -0
- package/src/captcha-local-solver.js +168 -0
- package/src/captcha-mlx-runtime.js +140 -0
- package/src/captcha-model-downloader.js +162 -0
- package/src/captcha-model-manager.js +375 -0
- package/src/captcha-model-manifest.js +74 -0
- package/src/captcha-resources.js +128 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const { MODEL_MANIFESTS } = require('./captcha-model-manifest');
|
|
5
|
+
const { downloadModelArtifacts, verifyArtifactDirectory, checksumFile } = require('./captcha-model-downloader');
|
|
6
|
+
const { getMemorySnapshot, getResourceSnapshot } = require('./captcha-resources');
|
|
7
|
+
const { detectWithCompanion } = require('./captcha-companion-client');
|
|
8
|
+
|
|
9
|
+
const MODEL_ROOT = path.resolve(process.env.CAPTCHA_MODEL_CACHE_DIR || path.join(process.cwd(), 'data', 'captcha-model'));
|
|
10
|
+
const RECONCILE_INTERVAL_MS = 60_000;
|
|
11
|
+
const PROBE_PNG = Buffer.from(
|
|
12
|
+
'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAIAAAD8GO2jAAAAGUlEQVR4nO3BAQ0AAADCoPdPbQ43oAAAAAAAAAB4Gx4gAAE7Bq0AAAAASUVORK5CYII=',
|
|
13
|
+
'base64'
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const parseFlag = (value) => ['1', 'true', 'yes', 'on'].includes(String(value || '').trim().toLowerCase());
|
|
17
|
+
|
|
18
|
+
function normalizedResources(input = getMemorySnapshot()) {
|
|
19
|
+
if (input.memory) return input;
|
|
20
|
+
return { memory: input, accelerators: [], companion: null, onnxBackends: ['cpu'] };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function acceleratorCapacity(resources) {
|
|
24
|
+
return (resources.accelerators || []).reduce((best, accelerator) => {
|
|
25
|
+
const totalMb = Number(accelerator.totalMb) || 0;
|
|
26
|
+
const availableMb = Number(accelerator.availableMb) || 0;
|
|
27
|
+
if (totalMb > best.totalMb || (totalMb === best.totalMb && availableMb > best.availableMb)) {
|
|
28
|
+
return { ...accelerator, totalMb, availableMb };
|
|
29
|
+
}
|
|
30
|
+
return best;
|
|
31
|
+
}, { type: null, totalMb: 0, availableMb: 0 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function qualifiesMemory(memory, definition) {
|
|
35
|
+
return Number(memory?.totalMb) >= definition.minimumTotalMb
|
|
36
|
+
&& Number(memory?.availableMb) >= definition.minimumAvailableMb;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function qualifyingAccelerator(resources, definition) {
|
|
40
|
+
return (resources.accelerators || []).find((accelerator) =>
|
|
41
|
+
Number(accelerator.totalMb) >= definition.minimumTotalMb
|
|
42
|
+
&& Number(accelerator.availableMb) >= definition.minimumAvailableMb);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function selectModelTier(input = getMemorySnapshot(), env = process.env) {
|
|
46
|
+
if (parseFlag(env.SKIP_LOCAL_CAPTCHA_MODEL)) return null;
|
|
47
|
+
const resources = normalizedResources(input);
|
|
48
|
+
const requested = String(env.CAPTCHA_MODEL_TIER || 'auto').trim().toLowerCase();
|
|
49
|
+
if (!['auto', 'owlvit', 'florence2'].includes(requested)) {
|
|
50
|
+
throw new Error(`Invalid CAPTCHA_MODEL_TIER "${requested}"; expected auto, owlvit, or florence2`);
|
|
51
|
+
}
|
|
52
|
+
const forceCpu = String(env.CAPTCHA_MODEL_DEVICE || 'auto').trim().toLowerCase() === 'cpu';
|
|
53
|
+
const owlQualified = qualifiesMemory(resources.memory, MODEL_MANIFESTS.owlvit)
|
|
54
|
+
|| (!forceCpu && Boolean(qualifyingAccelerator(resources, MODEL_MANIFESTS.owlvit)));
|
|
55
|
+
if (!owlQualified) return null;
|
|
56
|
+
const florenceQualified = qualifiesMemory(resources.memory, MODEL_MANIFESTS.florence2)
|
|
57
|
+
|| (!forceCpu && Boolean(qualifyingAccelerator(resources, MODEL_MANIFESTS.florence2)));
|
|
58
|
+
if (requested === 'florence2' && !florenceQualified) {
|
|
59
|
+
throw new Error('CAPTCHA_MODEL_TIER=florence2 requires at least 8 GiB effective memory and 2 GiB available');
|
|
60
|
+
}
|
|
61
|
+
if (requested === 'owlvit') return 'owlvit';
|
|
62
|
+
return florenceQualified ? 'florence2' : 'owlvit';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function selectModelBackend(tier, input) {
|
|
66
|
+
const resources = normalizedResources(input);
|
|
67
|
+
const forceCpu = String(process.env.CAPTCHA_MODEL_DEVICE || 'auto').trim().toLowerCase() === 'cpu';
|
|
68
|
+
const companion = (resources.accelerators || []).find((item) => item.type === 'companion');
|
|
69
|
+
const definition = MODEL_MANIFESTS[tier];
|
|
70
|
+
if (!forceCpu && companion && qualifiesMemory(companion, definition)) {
|
|
71
|
+
return { type: 'companion', device: companion.backend || 'coreml' };
|
|
72
|
+
}
|
|
73
|
+
const device = selectModelDevice(process.env, resources);
|
|
74
|
+
if (qualifiesMemory(resources.memory, definition)) return { type: 'local', device };
|
|
75
|
+
const accelerator = (resources.accelerators || []).find((item) => item.type === device && qualifiesMemory(item, definition));
|
|
76
|
+
if (device !== 'cpu' && accelerator && qualifiesMemory(resources.memory, MODEL_MANIFESTS.owlvit)) {
|
|
77
|
+
return { type: 'local', device };
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function selectModelDevice(env = process.env, resources = { onnxBackends: ['cpu'], accelerators: [] }) {
|
|
83
|
+
const requested = String(env.CAPTCHA_MODEL_DEVICE || 'auto').trim().toLowerCase();
|
|
84
|
+
if (!['auto', 'cpu'].includes(requested)) {
|
|
85
|
+
throw new Error(`Invalid CAPTCHA_MODEL_DEVICE "${requested}"; expected auto or cpu`);
|
|
86
|
+
}
|
|
87
|
+
if (requested === 'cpu') return 'cpu';
|
|
88
|
+
if (parseFlag(env.CAPTCHA_COMPANION_MODE) && process.platform === 'darwin' && resources.onnxBackends?.includes('coreml')) return 'coreml';
|
|
89
|
+
if (resources.accelerators?.some((item) => item.type === 'cuda') && resources.onnxBackends?.includes('cuda')) return 'cuda';
|
|
90
|
+
if (resources.accelerators?.some((item) => item.type === 'webgpu') && resources.onnxBackends?.includes('webgpu')) return 'webgpu';
|
|
91
|
+
if (process.platform === 'darwin' && resources.onnxBackends?.includes('coreml')) return 'coreml';
|
|
92
|
+
return 'cpu';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isCapacityFailure(error) {
|
|
96
|
+
return /out of memory|oom|allocation|capacity|execution provider|device.*lost|cuda|coreml|webgpu/i.test(error?.message || '');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function modelThreshold(tier, env = process.env) {
|
|
100
|
+
const variable = tier === 'florence2' ? 'CAPTCHA_FLORENCE2_THRESHOLD' : 'CAPTCHA_OWLVIT_THRESHOLD';
|
|
101
|
+
const configured = Number(env[variable]);
|
|
102
|
+
return Number.isFinite(configured) && configured >= 0 && configured <= 1
|
|
103
|
+
? configured
|
|
104
|
+
: MODEL_MANIFESTS[tier].threshold;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
class CaptchaModelManager {
|
|
108
|
+
constructor(options = {}) {
|
|
109
|
+
this.activeTier = null;
|
|
110
|
+
this.activeRuntime = null;
|
|
111
|
+
this.activeBackend = null;
|
|
112
|
+
this.reconcilePromise = null;
|
|
113
|
+
this.inferenceQueue = Promise.resolve();
|
|
114
|
+
this.timer = null;
|
|
115
|
+
this.lastError = null;
|
|
116
|
+
this.resourceProvider = options.resourceProvider || (() => getResourceSnapshot({ skipCompanion: parseFlag(process.env.CAPTCHA_COMPANION_MODE) }));
|
|
117
|
+
this.downloader = options.downloader || downloadModelArtifacts;
|
|
118
|
+
this.verifier = options.verifier || verifyArtifactDirectory;
|
|
119
|
+
this.runtimeLoader = options.runtimeLoader || ((tier, modelDir, device) => this._loadLocalRuntime(tier, modelDir, device));
|
|
120
|
+
this.modelRoot = options.modelRoot ? path.resolve(options.modelRoot) : MODEL_ROOT;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
isSkipped() {
|
|
124
|
+
return parseFlag(process.env.SKIP_LOCAL_CAPTCHA_MODEL);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
cachePath(...segments) {
|
|
128
|
+
const target = path.resolve(this.modelRoot, ...segments);
|
|
129
|
+
if (target !== this.modelRoot && !target.startsWith(`${this.modelRoot}${path.sep}`)) throw new Error('Unsafe CAPTCHA cache path');
|
|
130
|
+
return target;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async start() {
|
|
134
|
+
if (this.isSkipped() || this.timer) return;
|
|
135
|
+
await fs.promises.mkdir(this.modelRoot, { recursive: true });
|
|
136
|
+
const entries = await fs.promises.readdir(this.modelRoot, { withFileTypes: true });
|
|
137
|
+
for (const entry of entries) if (entry.name.startsWith('.staging-')) {
|
|
138
|
+
await fs.promises.rm(this.cachePath(entry.name), { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
this.timer = setInterval(() => this.reconcile().catch((error) => {
|
|
141
|
+
this.lastError = error;
|
|
142
|
+
console.warn('[CAPTCHA_MODEL] Reconciliation failed:', error.message);
|
|
143
|
+
}), RECONCILE_INTERVAL_MS);
|
|
144
|
+
this.timer.unref?.();
|
|
145
|
+
await this.reconcile();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async stop() {
|
|
149
|
+
if (this.timer) clearInterval(this.timer);
|
|
150
|
+
this.timer = null;
|
|
151
|
+
await this.activeRuntime?.dispose?.().catch?.(() => undefined);
|
|
152
|
+
this.activeRuntime = null;
|
|
153
|
+
this.activeTier = null;
|
|
154
|
+
this.activeBackend = null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async reconcile() {
|
|
158
|
+
if (this.reconcilePromise) return this.reconcilePromise;
|
|
159
|
+
this.reconcilePromise = this._reconcile().finally(() => { this.reconcilePromise = null; });
|
|
160
|
+
return this.reconcilePromise;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async _reconcile() {
|
|
164
|
+
if (this.isSkipped()) {
|
|
165
|
+
await this.stop();
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const resources = await this.resourceProvider();
|
|
169
|
+
const targetTier = selectModelTier(resources);
|
|
170
|
+
if (!targetTier) {
|
|
171
|
+
await this.activeRuntime?.dispose?.().catch?.(() => undefined);
|
|
172
|
+
this.activeRuntime = null;
|
|
173
|
+
this.activeTier = null;
|
|
174
|
+
this.activeBackend = null;
|
|
175
|
+
this.lastError = new Error('No local or companion CAPTCHA model has sufficient effective memory');
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
const targetBackend = selectModelBackend(targetTier, resources);
|
|
179
|
+
if (!targetBackend) {
|
|
180
|
+
await this.activeRuntime?.dispose?.().catch?.(() => undefined);
|
|
181
|
+
this.activeRuntime = null;
|
|
182
|
+
this.activeTier = null;
|
|
183
|
+
this.activeBackend = null;
|
|
184
|
+
this.lastError = new Error('No healthy CAPTCHA inference backend is available');
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
if (this.activeTier === targetTier
|
|
188
|
+
&& this.activeBackend?.type === targetBackend.type
|
|
189
|
+
&& this.activeBackend?.device === targetBackend.device
|
|
190
|
+
&& this.activeRuntime) return this.activeRuntime;
|
|
191
|
+
|
|
192
|
+
let runtime;
|
|
193
|
+
if (targetBackend.type === 'companion') {
|
|
194
|
+
runtime = this._companionRuntime(targetTier, targetBackend.device);
|
|
195
|
+
await runtime.probe();
|
|
196
|
+
} else {
|
|
197
|
+
const stagingDir = this.cachePath(`.staging-${targetTier}`);
|
|
198
|
+
const targetDir = this.cachePath(targetTier);
|
|
199
|
+
let hasCachedModel = await this.verifier(targetTier, targetDir).catch(() => false);
|
|
200
|
+
if (!hasCachedModel) {
|
|
201
|
+
await fs.promises.rm(targetDir, { recursive: true, force: true });
|
|
202
|
+
await fs.promises.rm(stagingDir, { recursive: true, force: true });
|
|
203
|
+
await fs.promises.mkdir(stagingDir, { recursive: true });
|
|
204
|
+
try {
|
|
205
|
+
await this.downloader(targetTier, stagingDir);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
const outOfDisk = error?.code === 'ENOSPC' || /no space left|ENOSPC/i.test(error?.message || '');
|
|
208
|
+
if (!outOfDisk || !this.activeTier || this.activeBackend?.type !== 'local') throw error;
|
|
209
|
+
await this.activeRuntime?.dispose?.().catch?.(() => undefined);
|
|
210
|
+
this.activeRuntime = null;
|
|
211
|
+
const oldTier = this.activeTier;
|
|
212
|
+
this.activeTier = null;
|
|
213
|
+
this.activeBackend = null;
|
|
214
|
+
await fs.promises.rm(this.cachePath(oldTier), { recursive: true, force: true });
|
|
215
|
+
await fs.promises.rm(stagingDir, { recursive: true, force: true });
|
|
216
|
+
await fs.promises.mkdir(stagingDir, { recursive: true });
|
|
217
|
+
await this.downloader(targetTier, stagingDir);
|
|
218
|
+
}
|
|
219
|
+
if (!await this.verifier(targetTier, stagingDir)) throw new Error(`Downloaded ${targetTier} model failed verification`);
|
|
220
|
+
await fs.promises.rename(stagingDir, targetDir);
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
runtime = await this.runtimeLoader(targetTier, targetDir, targetBackend.device);
|
|
224
|
+
await runtime.probe();
|
|
225
|
+
} catch (error) {
|
|
226
|
+
await runtime?.dispose?.().catch?.(() => undefined);
|
|
227
|
+
if (this.activeTier !== targetTier) await fs.promises.rm(targetDir, { recursive: true, force: true });
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const previous = { runtime: this.activeRuntime, tier: this.activeTier };
|
|
233
|
+
this.activeRuntime = runtime;
|
|
234
|
+
this.activeTier = targetTier;
|
|
235
|
+
this.activeBackend = targetBackend;
|
|
236
|
+
this.lastError = null;
|
|
237
|
+
await fs.promises.mkdir(this.modelRoot, { recursive: true });
|
|
238
|
+
const activeFile = this.cachePath('active.json');
|
|
239
|
+
const activeStaging = this.cachePath('.active.json.tmp');
|
|
240
|
+
await fs.promises.writeFile(activeStaging, JSON.stringify({
|
|
241
|
+
tier: targetTier,
|
|
242
|
+
model: MODEL_MANIFESTS[targetTier].id,
|
|
243
|
+
revision: MODEL_MANIFESTS[targetTier].revision,
|
|
244
|
+
backend: targetBackend.type,
|
|
245
|
+
device: targetBackend.device,
|
|
246
|
+
activatedAt: new Date().toISOString()
|
|
247
|
+
}, null, 2), { mode: 0o600 });
|
|
248
|
+
await fs.promises.rename(activeStaging, activeFile);
|
|
249
|
+
await previous.runtime?.dispose?.().catch?.(() => undefined);
|
|
250
|
+
for (const tier of Object.keys(MODEL_MANIFESTS)) {
|
|
251
|
+
if (tier !== targetTier) await fs.promises.rm(this.cachePath(tier), { recursive: true, force: true });
|
|
252
|
+
}
|
|
253
|
+
for (const tier of Object.keys(MODEL_MANIFESTS)) {
|
|
254
|
+
await fs.promises.rm(this.cachePath(`.staging-${tier}`), { recursive: true, force: true });
|
|
255
|
+
}
|
|
256
|
+
console.log(`[CAPTCHA_MODEL] Activated ${targetTier} via ${targetBackend.type}/${targetBackend.device}${previous.tier ? ` (replaced ${previous.tier})` : ''}`);
|
|
257
|
+
return runtime;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
_companionRuntime(tier, device) {
|
|
261
|
+
return {
|
|
262
|
+
tier,
|
|
263
|
+
device,
|
|
264
|
+
async detect(imageBuffer, label, threshold) {
|
|
265
|
+
const result = await detectWithCompanion(imageBuffer, label, tier, threshold);
|
|
266
|
+
return result.detections;
|
|
267
|
+
},
|
|
268
|
+
async probe() {
|
|
269
|
+
await detectWithCompanion(PROBE_PNG, 'object', tier, 0.99, 20_000);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async _loadLocalRuntime(tier, modelDir, device) {
|
|
275
|
+
const definition = MODEL_MANIFESTS[tier];
|
|
276
|
+
const transformers = await import('@huggingface/transformers');
|
|
277
|
+
transformers.env.allowRemoteModels = false;
|
|
278
|
+
transformers.env.allowLocalModels = true;
|
|
279
|
+
transformers.env.useFSCache = false;
|
|
280
|
+
const common = { dtype: definition.dtype, device, local_files_only: true };
|
|
281
|
+
if (definition.kind === 'owlvit') {
|
|
282
|
+
const detector = await transformers.pipeline('zero-shot-object-detection', modelDir, common);
|
|
283
|
+
return {
|
|
284
|
+
tier,
|
|
285
|
+
device,
|
|
286
|
+
async detect(imageBuffer, label, threshold = definition.threshold) {
|
|
287
|
+
const image = await transformers.RawImage.fromBlob(new Blob([imageBuffer], { type: 'image/png' }));
|
|
288
|
+
const detections = await detector(image, [label], { threshold });
|
|
289
|
+
return Array.isArray(detections) ? detections : [];
|
|
290
|
+
},
|
|
291
|
+
async probe() { await this.detect(PROBE_PNG, 'object', 0.99); },
|
|
292
|
+
async dispose() { await detector.dispose?.(); }
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
const model = await transformers.Florence2ForConditionalGeneration.from_pretrained(modelDir, common);
|
|
296
|
+
const processor = await transformers.AutoProcessor.from_pretrained(modelDir, { local_files_only: true });
|
|
297
|
+
const tokenizer = await transformers.AutoTokenizer.from_pretrained(modelDir, { local_files_only: true });
|
|
298
|
+
return {
|
|
299
|
+
tier,
|
|
300
|
+
device,
|
|
301
|
+
async detect(imageBuffer, label, threshold = definition.threshold) {
|
|
302
|
+
const image = await transformers.RawImage.fromBlob(new Blob([imageBuffer], { type: 'image/png' }));
|
|
303
|
+
const task = `<OPEN_VOCABULARY_DETECTION>${label}`;
|
|
304
|
+
const prompts = processor.construct_prompts(task);
|
|
305
|
+
const inputs = await processor(image, prompts);
|
|
306
|
+
const generated = await model.generate({ ...inputs, max_new_tokens: 64, num_beams: 1 });
|
|
307
|
+
const decoded = tokenizer.batch_decode(generated, { skip_special_tokens: false })[0];
|
|
308
|
+
const parsed = processor.post_process_generation(decoded, task, image.size);
|
|
309
|
+
const value = parsed?.[task] || parsed?.['<OPEN_VOCABULARY_DETECTION>'] || parsed || {};
|
|
310
|
+
const boxes = value.bboxes || value.boxes || [];
|
|
311
|
+
const labels = value.labels || [];
|
|
312
|
+
return boxes.map((box, index) => ({ box, label: labels[index] || label, score: threshold }));
|
|
313
|
+
},
|
|
314
|
+
async probe() { await this.detect(PROBE_PNG, 'object', 0.99); },
|
|
315
|
+
async dispose() { await model.dispose?.(); }
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async detect(imageBuffer, label, threshold) {
|
|
320
|
+
if (this.isSkipped()) throw new Error('Local CAPTCHA model disabled by SKIP_LOCAL_CAPTCHA_MODEL');
|
|
321
|
+
const run = async () => {
|
|
322
|
+
const runtime = await this.reconcile();
|
|
323
|
+
if (!runtime) throw this.lastError || new Error('Local CAPTCHA model is unavailable');
|
|
324
|
+
try {
|
|
325
|
+
return await runtime.detect(imageBuffer, label, threshold ?? modelThreshold(this.activeTier));
|
|
326
|
+
} catch (error) {
|
|
327
|
+
if (isCapacityFailure(error)) {
|
|
328
|
+
await runtime.dispose?.().catch?.(() => undefined);
|
|
329
|
+
this.activeRuntime = null;
|
|
330
|
+
this.activeTier = null;
|
|
331
|
+
this.activeBackend = null;
|
|
332
|
+
this.lastError = error;
|
|
333
|
+
this.reconcile().catch(() => undefined);
|
|
334
|
+
}
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
const resultPromise = this.inferenceQueue.then(run, run);
|
|
339
|
+
this.inferenceQueue = resultPromise.catch(() => undefined);
|
|
340
|
+
return resultPromise;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
status() {
|
|
344
|
+
return {
|
|
345
|
+
healthy: Boolean(this.activeRuntime),
|
|
346
|
+
activeTier: this.activeTier,
|
|
347
|
+
backend: this.activeBackend?.type || null,
|
|
348
|
+
device: this.activeBackend?.device || null,
|
|
349
|
+
error: this.lastError?.message || null
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const captchaModelManager = new CaptchaModelManager();
|
|
355
|
+
|
|
356
|
+
function checksumBuffer(buffer) {
|
|
357
|
+
return crypto.createHash('sha256').update(buffer).digest('hex');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
module.exports = {
|
|
361
|
+
MODEL_DEFINITIONS: MODEL_MANIFESTS,
|
|
362
|
+
MODEL_MANIFESTS,
|
|
363
|
+
CaptchaModelManager,
|
|
364
|
+
captchaModelManager,
|
|
365
|
+
getMemorySnapshot,
|
|
366
|
+
selectModelTier,
|
|
367
|
+
selectModelBackend,
|
|
368
|
+
selectModelDevice,
|
|
369
|
+
qualifiesMemory,
|
|
370
|
+
checksumBuffer,
|
|
371
|
+
checksumFile,
|
|
372
|
+
parseFlag,
|
|
373
|
+
modelThreshold,
|
|
374
|
+
isCapacityFailure
|
|
375
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const MODEL_MANIFESTS = Object.freeze({
|
|
2
|
+
owlvit: Object.freeze({
|
|
3
|
+
id: 'Xenova/owlvit-base-patch32',
|
|
4
|
+
revision: 'b75f4e52949639c3bb0b96546ea4149482f6e7ef',
|
|
5
|
+
dtype: 'q8',
|
|
6
|
+
kind: 'owlvit',
|
|
7
|
+
// 2 GB in provider/VM sizing is decimal bytes (~1907 MiB), not 2 GiB.
|
|
8
|
+
minimumTotalMb: 1907,
|
|
9
|
+
minimumAvailableMb: 512,
|
|
10
|
+
threshold: 0.12,
|
|
11
|
+
files: Object.freeze([
|
|
12
|
+
{ path: 'config.json', size: 583, sha256: 'd64b634b32bcc85f1580d103f731a6949d9cbb9749f19eb75b6e06c86b81eb28' },
|
|
13
|
+
{ path: 'merges.txt', size: 524619, sha256: '9fd691f7c8039210e0fced15865466c65820d09b63988b0174bfe25de299051a' },
|
|
14
|
+
{ path: 'preprocessor_config.json', size: 576, sha256: '7f1386f6b36981b62f76447375ec20470656179fea4e47b6e22e2d896376e8ce' },
|
|
15
|
+
{ path: 'special_tokens_map.json', size: 460, sha256: 'f118ab3a983206e4f32583448de6bd6aae4ee21869135cef1f5848a753cdaab6' },
|
|
16
|
+
{ path: 'tokenizer.json', size: 2224253, sha256: '50b24e94079fb11cb1503f65fef0ed08044186a1cd118c79683991554d9a5b23' },
|
|
17
|
+
{ path: 'tokenizer_config.json', size: 925, sha256: '2c49c1bc8de9f6f5189fb3e6cbadfe656431174a83064be3aac4f6e0f131f7d0' },
|
|
18
|
+
{ path: 'vocab.json', size: 862328, sha256: '5047b556ce86ccaf6aa22b3ffccfc52d391ea4accdab9c2f2407da5b742d4363' },
|
|
19
|
+
{ path: 'onnx/model_quantized.onnx', size: 155431700, sha256: '06af49bd5db977936bcddad2b45d0031072673517579d15b8cb42bf015a94156' }
|
|
20
|
+
])
|
|
21
|
+
}),
|
|
22
|
+
florence2: Object.freeze({
|
|
23
|
+
id: 'onnx-community/Florence-2-base-ft',
|
|
24
|
+
revision: 'e88a44eaf3791a35eae0c5a47b3dbcd36e67eb6f',
|
|
25
|
+
dtype: Object.freeze({
|
|
26
|
+
embed_tokens: 'fp16',
|
|
27
|
+
vision_encoder: 'fp16',
|
|
28
|
+
encoder_model: 'q4',
|
|
29
|
+
decoder_model_merged: 'q4'
|
|
30
|
+
}),
|
|
31
|
+
kind: 'florence2',
|
|
32
|
+
minimumTotalMb: 8192,
|
|
33
|
+
minimumAvailableMb: 2048,
|
|
34
|
+
threshold: 0.18,
|
|
35
|
+
files: Object.freeze([
|
|
36
|
+
{ path: 'added_tokens.json', size: 22410, sha256: 'ba67457c577895525996e7a42d7da60a05856506ea4fd25ff9a023a64662f74b' },
|
|
37
|
+
{ path: 'config.json', size: 5432, sha256: 'd90c22ed72eb55291f183fcd9b98ebd3bd3d92bfcffb6c7f6e1606085e793525' },
|
|
38
|
+
{ path: 'generation_config.json', size: 292, sha256: '7b8eb17bbd6cf8a07f619ad83ae03881eff05b6b9237bab89005b40e77783c29' },
|
|
39
|
+
{ path: 'merges.txt', size: 456318, sha256: '1ce1664773c50f3e0cc8842619a93edc4624525b728b188a9e0be33b7726adc5' },
|
|
40
|
+
{ path: 'preprocessor_config.json', size: 2673, sha256: 'c892857e34a7082284983a7717717d39c9bf7e574f1f41d80d4c918c97502efa' },
|
|
41
|
+
{ path: 'special_tokens_map.json', size: 146627, sha256: '72ff172dc769bc1551b1b4211628ce3271643bc60379e4da45d85a9be9332c39' },
|
|
42
|
+
{ path: 'tokenizer.json', size: 2297961, sha256: 'd69dcdb2323e124ac4f800cb9863ddccea0d7bb11e16125e8df3bd60f2f8aeac' },
|
|
43
|
+
{ path: 'tokenizer_config.json', size: 197658, sha256: 'd8e64607233cb53b619fb46664f6cad08176c26e0e8735b2d30d888364f19600' },
|
|
44
|
+
{ path: 'vocab.json', size: 1099884, sha256: '394fdc63c71aabe0a9b97117f5d62fb5fcc4d59b2b3ea929a3929e6a53217b3c' },
|
|
45
|
+
{ path: 'onnx/embed_tokens_fp16.onnx', size: 78780290, sha256: 'da2607930eea5e21e4a2bd5fd069de550f1acc30316a4e8f824551a95232ba39' },
|
|
46
|
+
{ path: 'onnx/vision_encoder_fp16.onnx', size: 183930536, sha256: 'a7abcd77199c5d0089cf985ede4dd8089acd84f30fb3fb1462d5930345c688b3' },
|
|
47
|
+
{ path: 'onnx/encoder_model_q4.onnx', size: 30058778, sha256: '34b17bcf191dacb79bd482b94bad5cf1ba39bc770f6a4c9ae26f28b89c235e4b' },
|
|
48
|
+
{ path: 'onnx/decoder_model_merged_q4.onnx', size: 64393474, sha256: 'be7a2f33e65f8d65538024772fda4d1c5a7752d60a7159aadf53f9f4798b90fa' }
|
|
49
|
+
])
|
|
50
|
+
})
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const APPLE_MLX_MANIFEST = Object.freeze({
|
|
54
|
+
id: 'mlx-community/Florence-2-base-ft-4bit',
|
|
55
|
+
revision: 'cfed5ed3e86826b6aa2902abf6cf2e406fd94504',
|
|
56
|
+
kind: 'florence2-mlx',
|
|
57
|
+
threshold: 0.18,
|
|
58
|
+
files: Object.freeze([
|
|
59
|
+
{ path: 'added_tokens.json', size: 22410, sha256: 'ba67457c577895525996e7a42d7da60a05856506ea4fd25ff9a023a64662f74b' },
|
|
60
|
+
{ path: 'config.json', size: 6703, sha256: '7f45016c8c9941e33939269e28c28ea2adca3dfe8ae2b8e21f9ceab4953f396f' },
|
|
61
|
+
{ path: 'merges.txt', size: 456318, sha256: '1ce1664773c50f3e0cc8842619a93edc4624525b728b188a9e0be33b7726adc5' },
|
|
62
|
+
{ path: 'model.safetensors', size: 163524711, sha256: '904321eedba8cf7bb1d7258e08d4efb6b8b2f62dc154d530b7ff663f240df533' },
|
|
63
|
+
{ path: 'model.safetensors.index.json', size: 96965, sha256: '3a5d577309a948bacd44141544167fd60d6dc7e8ad8825e0ec728fbce56369a1' },
|
|
64
|
+
{ path: 'preprocessor_config.json', size: 603, sha256: '1396ec5a0a7adfe1c04fb777b09e8ba753be6dbb5868212ab3c3ef39d91fe031' },
|
|
65
|
+
{ path: 'processing_florence2.py', size: 46372, sha256: '4bd7158536cbf1c7891fc8efd94437d79fd09f07f539c7398fab8a885d7d8bca' },
|
|
66
|
+
{ path: 'processor_config.json', size: 130, sha256: '6eb1a2e487fa8b7521691a3382b5afd92a78e8ea34b385f8824fa07563e92245' },
|
|
67
|
+
{ path: 'special_tokens_map.json', size: 439927, sha256: '20f95a5768e5d9d14ab9bebbf844c1750ece27765109ce35be9a629db630aedc' },
|
|
68
|
+
{ path: 'tokenizer.json', size: 3747961, sha256: '9b466914d9e7f9a39936c9bbe2ac28c86cd8b35c83a905699b01f83178f27c51' },
|
|
69
|
+
{ path: 'tokenizer_config.json', size: 232317, sha256: '06e57de7118812e63652f83c779ed1608712418c37cd8f7e4cec4380c6f43810' },
|
|
70
|
+
{ path: 'vocab.json', size: 798293, sha256: 'ed19656ea1707df69134c4af35c8ceda2cc9860bf2c3495026153a133670ab5e' }
|
|
71
|
+
])
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
module.exports = { MODEL_MANIFESTS, APPLE_MLX_MANIFEST };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const { execFile, execFileSync } = require('child_process');
|
|
4
|
+
const { promisify } = require('util');
|
|
5
|
+
const { probeCompanion } = require('./captcha-companion-client');
|
|
6
|
+
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
8
|
+
|
|
9
|
+
function readNumericFile(filePath, readFile = fs.readFileSync) {
|
|
10
|
+
try {
|
|
11
|
+
const raw = readFile(filePath, 'utf8').trim();
|
|
12
|
+
if (!raw || raw === 'max') return null;
|
|
13
|
+
const value = Number(raw);
|
|
14
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseMacAvailableMemory(vmStatOutput) {
|
|
21
|
+
const text = String(vmStatOutput || '');
|
|
22
|
+
const pageSize = Number(text.match(/page size of\s+(\d+)\s+bytes/i)?.[1]);
|
|
23
|
+
if (!Number.isFinite(pageSize) || pageSize <= 0) return null;
|
|
24
|
+
const pages = { free: 0, inactive: 0, speculative: 0 };
|
|
25
|
+
for (const match of text.matchAll(/Pages\s+(free|inactive|speculative):\s*(\d+)\.?/gi)) {
|
|
26
|
+
pages[match[1].toLowerCase()] = Number(match[2]);
|
|
27
|
+
}
|
|
28
|
+
const availablePages = pages.free + pages.inactive + pages.speculative;
|
|
29
|
+
return availablePages > 0 ? availablePages * pageSize : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readMacAvailableMemory(runVmStat = () => execFileSync('vm_stat', { encoding: 'utf8', timeout: 1500, maxBuffer: 64 * 1024 })) {
|
|
33
|
+
try {
|
|
34
|
+
return parseMacAvailableMemory(runVmStat());
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getMemorySnapshot(overrides = {}) {
|
|
41
|
+
const readFile = overrides.readFile || fs.readFileSync;
|
|
42
|
+
const platform = overrides.platform || process.platform;
|
|
43
|
+
const hostTotalBytes = overrides.hostTotalBytes ?? os.totalmem();
|
|
44
|
+
const macAvailableBytes = overrides.macAvailableBytes
|
|
45
|
+
?? (platform === 'darwin' ? readMacAvailableMemory(overrides.runVmStat) : null);
|
|
46
|
+
const hostAvailableBytes = overrides.hostAvailableBytes
|
|
47
|
+
?? macAvailableBytes
|
|
48
|
+
?? os.freemem();
|
|
49
|
+
const cgroupLimitBytes = overrides.cgroupLimitBytes
|
|
50
|
+
?? readNumericFile('/sys/fs/cgroup/memory.max', readFile)
|
|
51
|
+
?? readNumericFile('/sys/fs/cgroup/memory/memory.limit_in_bytes', readFile);
|
|
52
|
+
const cgroupUsedBytes = overrides.cgroupUsedBytes
|
|
53
|
+
?? readNumericFile('/sys/fs/cgroup/memory.current', readFile)
|
|
54
|
+
?? readNumericFile('/sys/fs/cgroup/memory/memory.usage_in_bytes', readFile);
|
|
55
|
+
const effectiveTotalBytes = cgroupLimitBytes && cgroupLimitBytes < hostTotalBytes ? cgroupLimitBytes : hostTotalBytes;
|
|
56
|
+
const cgroupAvailableBytes = cgroupLimitBytes && cgroupUsedBytes !== null ? Math.max(0, cgroupLimitBytes - cgroupUsedBytes) : null;
|
|
57
|
+
const effectiveAvailableBytes = cgroupAvailableBytes === null ? hostAvailableBytes : Math.min(hostAvailableBytes, cgroupAvailableBytes);
|
|
58
|
+
return {
|
|
59
|
+
totalMb: effectiveTotalBytes / (1024 * 1024),
|
|
60
|
+
availableMb: effectiveAvailableBytes / (1024 * 1024),
|
|
61
|
+
cgroupLimitMb: cgroupLimitBytes ? cgroupLimitBytes / (1024 * 1024) : null,
|
|
62
|
+
swapQualified: false
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function probeNvidia() {
|
|
67
|
+
try {
|
|
68
|
+
const { stdout } = await execFileAsync('nvidia-smi', [
|
|
69
|
+
'--query-gpu=memory.total,memory.free',
|
|
70
|
+
'--format=csv,noheader,nounits'
|
|
71
|
+
], { timeout: 1500, maxBuffer: 64 * 1024 });
|
|
72
|
+
const devices = stdout.trim().split(/\r?\n/).map((line) => {
|
|
73
|
+
const [totalMb, availableMb] = line.split(',').map((value) => Number(value.trim()));
|
|
74
|
+
return { totalMb, availableMb };
|
|
75
|
+
}).filter((item) => Number.isFinite(item.totalMb) && Number.isFinite(item.availableMb));
|
|
76
|
+
if (!devices.length) return null;
|
|
77
|
+
return devices.reduce((best, current) => current.availableMb > best.availableMb ? current : best);
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function probeOnnxBackends() {
|
|
84
|
+
try {
|
|
85
|
+
const ort = require('onnxruntime-node');
|
|
86
|
+
const backends = await ort.listSupportedBackends();
|
|
87
|
+
return backends.filter((item) => item.bundled).map((item) => item.name);
|
|
88
|
+
} catch {
|
|
89
|
+
return ['cpu'];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function getResourceSnapshot({ skipCompanion = false } = {}) {
|
|
94
|
+
const memory = getMemorySnapshot();
|
|
95
|
+
const [nvidia, backends, companion] = await Promise.all([
|
|
96
|
+
probeNvidia(),
|
|
97
|
+
probeOnnxBackends(),
|
|
98
|
+
skipCompanion ? Promise.resolve(null) : probeCompanion()
|
|
99
|
+
]);
|
|
100
|
+
const accelerators = [];
|
|
101
|
+
if (nvidia) accelerators.push({ type: 'cuda', ...nvidia });
|
|
102
|
+
if (globalThis.navigator?.gpu) {
|
|
103
|
+
accelerators.push({ type: 'webgpu', totalMb: null, availableMb: null });
|
|
104
|
+
}
|
|
105
|
+
if (process.platform === 'darwin' && backends.includes('coreml')) {
|
|
106
|
+
accelerators.push({ type: 'coreml', totalMb: memory.totalMb, availableMb: memory.availableMb });
|
|
107
|
+
}
|
|
108
|
+
if (companion) {
|
|
109
|
+
accelerators.push({
|
|
110
|
+
type: 'companion',
|
|
111
|
+
totalMb: companion.memory.totalMb,
|
|
112
|
+
availableMb: companion.memory.availableMb,
|
|
113
|
+
backend: companion.backend,
|
|
114
|
+
activeTier: companion.activeTier || null
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return { memory, accelerators, onnxBackends: backends, companion };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
readNumericFile,
|
|
122
|
+
parseMacAvailableMemory,
|
|
123
|
+
readMacAvailableMemory,
|
|
124
|
+
getMemorySnapshot,
|
|
125
|
+
probeNvidia,
|
|
126
|
+
probeOnnxBackends,
|
|
127
|
+
getResourceSnapshot
|
|
128
|
+
};
|