borkiss's picture
Upload folder using huggingface_hub
68b311d verified
Raw
History Blame Contribute Delete
35.3 kB
// worker.js — drop-in замена inference.worker для falcon-perception:
// протокол load/detect -> progress/loaded/result/error, весь языковой стек —
// наш WGSL-движок (model.js). Маски пока null (anyup/segm_head — отдельный шаг).
import { Engine } from './model.js';
import { Tracker, toGray } from './tracker.js';
// URL весов/токенизатора: дефолты для локальной разработки; продакшен
// (OSS-деплой) передаёт их в load-сообщении ({weightsUrl, transformersUrl}).
const WEIGHTS = new URL('../weights/falcon-q8-lm4.bin', import.meta.url).href;
const TRANSFORMERS = '/assets/transformers.web-DaZ4EEMB.js';
const HF_ID = 'onnx-community/falcon-perception-onnx-webgpu';
const FALLBACK_TOKENS = [37462, 1978, 20528, 821, 790, 6883, 549, 264, 258]; // "…athletes<|REF_SEG|>"
const CFG = {
patch: 16, dim: 1024, coordTok: 240, sizeTok: 241, segTok: 262, eos: 11,
imgId: 227, clsId: 244, imgEnd: 230, presence: 268, absence: 269,
endQuery: 263, ropeTheta: 1e4, maxSteps: 510, L: 1024,
};
const REGISTERS = [245, 246, 247, 248];
const MAX_S = 1088, MAX_T = 1664;
let eng = null, tokenizer = null, busy = false, queued = null;
let tracker = null; // отдельный GPUDevice: ридбеки трека не ждут тяжёлую очередь
let panDX = 0, panDY = 0; // накопленный пан треков с момента старта heavy-детекции
const post = (m) => self.postMessage(m);
// Эвристики видеорежима (message {type:'config', ...} мержится сюда):
// spec — спекулятивный декод кейфрейма по драфту из трекера (батчевый
// verify; семантика акцепта — точное совпадение токенов, бины
// голов в пределах specTol);
// specTol — допуск бинов coord/size при акцепте (0 = строгий, бит-в-бит
// с последовательным декодом по логике выбора);
// gate — скип кейфреймов на статике: дифф серого против кадра последнего
// кейфрейма вне боксов (кейфреймовых и текущих трекерных);
// форс-кейфрейм не реже gateMaxMs.
// maxVideoSteps — страховка видеорежима от редких разносов декода (модель
// в пан-артефактах может уйти в сотни генерик-токенов до maxSteps:
// наблюдали 510 шагов = 6.2 с стоячего кейфрейма).
// videoRes — сторона квадратного стретча кейфрейма в видеорежиме (кратно 16,
// ≤512). 256 — быстрый дефолт (~2 Гц); 384/512 ловят мелкие/
// камуфляжные цели ценой префилла (256→576→1024 патчей).
// decodeCap — жёсткий потолок шагов декода в ЛЮБОМ режиме (0 = выкл). Фото-путь
// (RECORD) не подпадает под maxVideoSteps, а модель на редких кадрах
// уходит в вырожденный разнос (наблюдали 39 боксов-столбиком за ~120
// шагов) — RECORD ставит сюда ~90, ограничивая и мусор, и время кадра.
const OPTS = { spec: true, specTol: 6,
gate: false, gateMaxMs: 2500, gateDiffThr: 0.015,
maxVideoSteps: 120, videoRes: 256, decodeCap: 0 };
// Память последнего кейфрейма: fed-последовательность декода для драфта
// спекуляции + серый кадр/боксы/время для гейтинга.
let lastEmission = null; // {query, entries:[{id,kind:'g'|'c'|'s',bins?,obj?}], terminal, objMap}
let lastKey = null; // {gray, w, h, time, query, boxes:[{x1..y2}]}
let specBias = [0, 0]; // самокалибровка драфта: EMA медианного резидуала
// (модельные coord-бины - драфт-бины) прошлого кейфрейма;
// выедает систематику учёта пана трекером за 1-2 кейфрейма
let specHist = []; // последние кейфреймы: 1 = спекуляция окупилась (≥6 строк)
let specSkips = 0; // счётчик пропусков при адаптивно выключенной спекуляции:
// сцены с глобальным паном/перетасовкой объектов драфту
// не поддаются — там verify только жжёт время; каждый
// 5-й кейфрейм зондируем, чтобы включиться обратно
// ---------------------------------------------------------------- load
async function fetchProgress(url, label, sizeHint) {
const r = await fetch(url);
if (!r.ok) throw new Error(`fetch ${label}: ${r.status}`);
const total = +r.headers.get('content-length') || sizeHint || 0;
const reader = r.body.getReader();
const chunks = []; let loaded = 0;
for (;;) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value); loaded += value.length;
post({ type: 'progress', progress: Math.round(loaded / (total || loaded) * 100),
loaded, total, file: label });
}
const out = new Uint8Array(loaded); let o = 0;
for (const c of chunks) { out.set(c, o); o += c.length; }
return out.buffer;
}
async function load(opts = {}) {
try {
const transformersUrl = opts.transformersUrl || TRANSFORMERS;
const weightsUrl = opts.weightsUrl || WEIGHTS;
const tokP = import(transformersUrl)
.then(({ AutoTokenizer }) => AutoTokenizer.from_pretrained(HF_ID))
.then((t) => { tokenizer = t; console.log('tokenizer loaded'); })
.catch((e) => console.warn('tokenizer fallback:', e.message));
// фичи проверяем ДО скачивания 724 МБ — на неподдерживаемом устройстве
// (Паскаль без f16, Safari без subgroups) падаем сразу, не сжигая трафик;
// ранний adapter только для чека — для requestDevice берём свежий после fetch
const probe = await (navigator.gpu?.requestAdapter({ powerPreference: 'high-performance' })
?? Promise.resolve(null));
if (!probe) throw new Error('нет WebGPU — нужен Chrome/Edge 125+');
for (const f of ['shader-f16', 'subgroups'])
if (!probe.features.has(f)) throw new Error(`нет WebGPU-фичи ${f}`);
const wbuf = await fetchProgress(weightsUrl, 'falcon-q8-lm4.bin', 724248832);
const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' });
const feats = ['shader-f16', 'subgroups'];
if (adapter.features.has('chromium-experimental-subgroup-matrix'))
feats.push('chromium-experimental-subgroup-matrix');
const device = await adapter.requestDevice({
requiredFeatures: feats,
requiredLimits: {
maxStorageBufferBindingSize: Math.min(1 << 30, adapter.limits.maxStorageBufferBindingSize),
maxBufferSize: Math.min(1 << 30, adapter.limits.maxBufferSize),
},
});
device.addEventListener('uncapturederror', (e) => console.error('WebGPU:', e.error.message));
// device.lost -> страница показывает фатальный баннер (combined-design §5)
device.lost.then((i) => post({ type: 'error', fatal: true, message: `engine device lost: ${i.message}` }));
eng = await Engine.create(device, wbuf, { maxS: MAX_S, maxT: MAX_T, sgmat: false });
// adapter «consumed» после requestDevice — трекеру нужен свой adapter
const adapter2 = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' });
const tdev = await adapter2.requestDevice(); // лёгкий device для трекера
tdev.lost.then((i) => post({ type: 'error', fatal: true, message: `tracker device lost: ${i.message}` }));
tracker = await new Tracker(tdev).init();
await tokP;
post({ type: 'loaded' });
} catch (e) {
post({ type: 'error', message: e.message || String(e) });
}
}
// ---------------------------------------------------------------- препроцесс
function preprocess(image, video = false) {
const p = CFG.patch;
let w, h;
if (video) { w = h = OPTS.videoRes; } // видеорежим: квадратный стретч (videoRes²)
else {
const scale = Math.min(512 / image.width, 512 / image.height, 1);
w = Math.max(p, Math.round(image.width * scale / p) * p);
h = Math.max(p, Math.round(image.height * scale / p) * p);
}
const ctx = new OffscreenCanvas(w, h).getContext('2d');
ctx.drawImage(image, 0, 0, w, h);
const imgData = ctx.getImageData(0, 0, w, h);
const img = imgData.data;
const gray = toGray(imgData, w, h);
const ph = h / p, pw = w / p, n = ph * pw, pl = p * p * 3;
const patches = new Float32Array(n * pl);
for (let py = 0; py < ph; py++)
for (let px = 0; px < pw; px++) {
const base = (py * pw + px) * pl;
for (let r = 0; r < p; r++)
for (let c = 0; c < p; c++) {
const src = ((py * p + r) * w + px * p + c) * 4;
const dst = base + (r * p + c) * 3;
patches[dst] = img[src] / 255 * 2 - 1;
patches[dst + 1] = img[src + 1] / 255 * 2 - 1;
patches[dst + 2] = img[src + 2] / 255 * 2 - 1;
}
}
return { patches, ph, pw, gray, w, h };
}
function posGrid(ph, pw) {
const sy = Math.sqrt(pw / ph), sx = Math.sqrt(ph / pw);
const g = new Float32Array(ph * pw * 2);
for (let i = 0; i < ph; i++) {
const rv = ph > 1 ? sx * (2 * i / (ph - 1) - 1) : 0;
for (let j = 0; j < pw; j++) {
const cv = pw > 1 ? sy * (2 * j / (pw - 1) - 1) : 0;
g[(i * pw + j) * 2] = rv;
g[(i * pw + j) * 2 + 1] = cv;
}
}
return g;
}
// ---------------------------------------------------------------- detect
const iou = (a, b) => {
const w = Math.min(a.x2, b.x2) - Math.max(a.x1, b.x1);
const h = Math.min(a.y2, b.y2) - Math.max(a.y1, b.y1);
if (w <= 0 || h <= 0) return 0;
const ar = (r) => Math.max(0, r.x2 - r.x1) * Math.max(0, r.y2 - r.y1);
const i = w * h;
return i / (ar(a) + ar(b) - i);
};
function nms(dets, thr = 0.6) {
const order = dets.map((d, i) => [((d.box.x2 - d.box.x1) * (d.box.y2 - d.box.y1)), i])
.sort((a, b) => b[0] - a[0]);
const keepIdx = new Set(), keptBoxes = [];
for (const [, i] of order) {
if (!keptBoxes.some((k) => iou(dets[i].box, k) > thr)) {
keepIdx.add(i); keptBoxes.push(dets[i].box);
}
}
return dets.filter((_, i) => keepIdx.has(i));
}
const argmax = (a, o = 0, n = a.length) => {
let bi = 0, bv = -Infinity;
for (let i = 0; i < n; i++) if (a[o + i] > bv) { bv = a[o + i]; bi = i; }
return bi;
};
function frameGray(image, video) {
const fit = (d) => (video ? OPTS.videoRes : Math.max(CFG.patch,
Math.round(d * Math.min(512 / image.width, 512 / image.height, 1) / CFG.patch) * CFG.patch));
const w = fit(image.width), h = fit(image.height);
const ctx = new OffscreenCanvas(w, h).getContext('2d');
ctx.drawImage(image, 0, 0, w, h);
return { gray: toGray(ctx.getImageData(0, 0, w, h), w, h), w, h };
}
// Дешёвый корреляционный трек кадра (движок занят либо кейфрейм загейчен).
async function trackFrame(msg, accumulatePan, pre = null) {
const { image, query, requestId, sourceTimestampMs, capturedAtMs, video } = msg;
try {
const t0 = performance.now();
const { gray, w, h } = pre || frameGray(image, video);
const dets = await tracker.track(gray, w, h);
if (accumulatePan) {
panDX += tracker.lastShift?.dx || 0;
panDY += tracker.lastShift?.dy || 0;
}
post({ type: 'result', source: 'track', detections: dets,
inferenceMs: performance.now() - t0,
query, requestId, sourceTimestampMs, capturedAtMs, tracked: true });
} catch (e) {
console.warn('[engine] track error:', e.message);
} finally { image.close?.(); }
}
// Гейт кейфрейма: сцена статична против кадра последнего кейфрейма?
// Дифф серого по сетке (шаг 4) ВНЕ боксов (кейфреймовых и текущих трекерных,
// с полем 10%): движение самих объектов ведёт трекер, а дифф ловит новый
// контент — въехавший объект, смену сцены, пан. Форс-кейфрейм — по таймауту.
function gateStatic(gray, w, h, query) {
if (!lastKey || lastKey.w !== w || lastKey.h !== h || lastKey.query !== query) return false;
if (performance.now() - lastKey.time > OPTS.gateMaxMs) return false;
const boxes = [...lastKey.boxes, ...tracker.boxes.map((t) => t.box)].map((b) => {
const mx = (b.x2 - b.x1) * 0.1 + 0.01, my = (b.y2 - b.y1) * 0.1 + 0.01;
return { x1: (b.x1 - mx) * w, x2: (b.x2 + mx) * w,
y1: (b.y1 - my) * h, y2: (b.y2 + my) * h };
});
let sum = 0, n = 0, masked = 0, total = 0;
for (let y = 2; y < h; y += 4)
for (let x = 2; x < w; x += 4) {
total++;
if (boxes.some((b) => x >= b.x1 && x <= b.x2 && y >= b.y1 && y <= b.y2)) { masked++; continue; }
sum += Math.abs(gray[y * w + x] - lastKey.gray[y * w + x]); n++;
}
if (n < 256) return false; // слишком мало фоновых сэмплов — диффу нет веры
return sum / n < OPTS.gateDiffThr;
}
async function detect(msg) {
const { image, query, video } = msg;
if (busy) { await trackFrame(msg, true); return; } // движок занят -> трек
if (video && OPTS.gate && tracker) {
try {
const pre = frameGray(image, true);
if (gateStatic(pre.gray, pre.w, pre.h, query)) {
await trackFrame(msg, false, pre);
return;
}
} catch (e) { console.warn('[engine] gate error:', e.message); }
}
busy = true;
panDX = 0; panDY = 0; // треки, пришедшие во время heavy, накопят пан сюда
await heavyDetect(msg);
}
async function heavyDetect(msg) {
const { image, query, requestId, sourceTimestampMs, capturedAtMs, video } = msg;
const reply = (dets, ms, stats) => post({ type: 'result', source: 'detect', detections: dets,
inferenceMs: ms, query, requestId, sourceTimestampMs, capturedAtMs, stats });
try {
const t0 = performance.now();
const { patches, ph, pw, gray, w: fw, h: fh } = preprocess(image, video);
const nP = ph * pw;
let text = FALLBACK_TOKENS;
if (tokenizer) {
text = Array.from(tokenizer.encode(
`Segment these expressions in the image:<|start_of_query|>${query}<|REF_SEG|>`,
{ add_special_tokens: false }));
}
const imgStart = 1 + REGISTERS.length;
const seq = [CFG.clsId, ...REGISTERS,
...Array(nP).fill(CFG.imgId), CFG.imgEnd, ...text];
const C = seq.length;
const bidirEnd = imgStart + nP + 1;
if (C > MAX_S) throw new Error(`seq_len ${C} > maxS ${MAX_S}`);
// позиции (спецтокены img-блока не двигают счётчик) + rope-таблицы
const skip = new Set([...REGISTERS, CFG.imgId, CFG.imgEnd]);
const pos = new Float32Array(C);
let acc = 0;
for (let i = 0; i < C; i++) { acc += (skip.has(seq[i]) && i < bidirEnd) ? 0 : 1; pos[i] = acc - 1; }
const r1 = new Float32Array(MAX_T * 32 * 2);
for (let t = 0; t < MAX_T; t++) {
const P = t < C ? pos[t] : pos[C - 1] + 1 + (t - C); // слоты декода
for (let f = 0; f < 32; f++) {
const a = P / CFG.ropeTheta ** (2 * f / 64);
r1[(t * 32 + f) * 2] = Math.cos(a);
r1[(t * 32 + f) * 2 + 1] = Math.sin(a);
}
}
const r2 = new Float32Array(MAX_T * 16 * 32 * 2);
for (let i = 0; i < MAX_T * 16 * 32; i++) r2[i * 2] = 1; // identity
const grid = posGrid(ph, pw);
const golden = eng.golden; // [16,32,2]
for (let k = 0; k < nP; k++) {
const s = imgStart + k, p0 = grid[k * 2], p1 = grid[k * 2 + 1];
for (let hh = 0; hh < 16; hh++)
for (let f = 0; f < 32; f++) {
const th = p0 * golden[(hh * 32 + f) * 2] + p1 * golden[(hh * 32 + f) * 2 + 1];
const o = ((s * 16 + hh) * 32 + f) * 2;
r2[o] = Math.cos(th); r2[o + 1] = Math.sin(th);
}
}
eng.setRopeTables(r1, r2);
// эмбеддинги (GPU gather + проекция патчей) и префилл
const tp0 = performance.now();
await eng.buildPrefillEmbeddings(new Uint32Array(seq), patches, imgStart);
await eng.prefill(null, bidirEnd, {}, C);
let logits = await eng.readF32(eng.b.logits, 65536);
const tPrefill = performance.now() - tp0;
let tHeads = 0, tSteps = 0, nSteps = 0, tSpec = 0, nSpecOk = 0, nSpecRun = 0;
const presence = logits[CFG.presence], absence = logits[CFG.absence];
console.log(`[engine] prefill ${C} ток: presence=${presence.toFixed(2)} absence=${absence.toFixed(2)}`);
const score = 1 / (1 + Math.exp(-(presence - absence)));
const dets = [];
const presStats = { presence: +presence.toFixed(2), absence: +absence.toFixed(2), score: +score.toFixed(3) };
if (presence <= absence && presence < 0) {
reply([], performance.now() - t0, { ...presStats, gatedOut: true, prefillMs: Math.round(tPrefill) });
busy = false; runQueued(image); return;
}
// ---- декод: спекуляция по драфту из трекера + последовательный хвост ----
const STOP = new Set([CFG.eos, CFG.endQuery]);
const emission = []; // fed-токены этого кейфрейма — драфт для следующего
let terminal = null; // токен, остановивший декод (eos/endQuery/absence)
const mnL = Math.log2(1 / CFG.L);
const coordFromBins = (bi, bj) => [bi / (CFG.L - 1), bj / (CFG.L - 1)];
const sizeFromBins = (bi, bj) =>
[2 ** (bi / (CFG.L - 1) * -mnL + mnL), 2 ** (bj / (CFG.L - 1) * -mnL + mnL)];
// coord-бины с dedup против уже принятых dets (мутирует hl) — как было
const coordBinsDedup = (hl) => {
let bi, bj;
for (let tr = 0; tr < 100; tr++) {
bi = argmax(hl, 0, CFG.L); bj = argmax(hl, CFG.L, CFG.L);
const cx = bi / (CFG.L - 1), cy = bj / (CFG.L - 1);
if (!dets.some((d) => Math.abs((d.box.x1 + d.box.x2) / 2 - cx) < 0.01 &&
Math.abs((d.box.y1 + d.box.y2) / 2 - cy) < 0.01)) break;
hl[bi] = -Infinity; hl[CFG.L + bj] = -Infinity;
}
return [bi, bj];
};
const pushDet = (cBins, sBins) => {
const coord = coordFromBins(...cBins), size = sizeFromBins(...sBins);
dets.push({ label: query, score,
box: { x1: Math.max(0, coord[0] - size[1] / 2), y1: Math.max(0, coord[1] - size[0] / 2),
x2: Math.min(1, coord[0] + size[1] / 2), y2: Math.min(1, coord[1] + size[0] / 2) },
mask: null });
post({ type: 'result', source: 'detect', detections: [...dets],
inferenceMs: performance.now() - t0,
query, requestId, sourceTimestampMs, capturedAtMs, partial: true });
};
// Последовательный декод (прежний путь). preHead — логиты голов строки,
// уже посчитанные батчевым verify для точки рассинхрона (hidden строки 0
// после verify не соответствует nxt — runHead там читать нельзя).
const seqDecode = async (nxt, preHead) => {
let cBins = null, sBins = null;
let stepCap = video ? Math.min(CFG.maxSteps, OPTS.maxVideoSteps) : CFG.maxSteps;
if (OPTS.decodeCap > 0) stepCap = Math.min(stepCap, OPTS.decodeCap);
for (let st = 0; st < stepCap && eng.T < MAX_T - 1; st++) {
if (st === stepCap - 1) console.warn(`[engine] декод упёрся в потолок ${stepCap} шагов — обрыв`);
if (STOP.has(nxt) || nxt === CFG.absence) { terminal = nxt; return; }
let embNext = null, entry = { id: nxt, kind: 'g' };
if (nxt === CFG.coordTok) {
let hl;
if (preHead) { hl = preHead.coord; preHead = null; }
else {
const th0 = performance.now();
hl = await eng.runHead('coord');
tHeads += performance.now() - th0;
}
cBins = coordBinsDedup(hl);
embNext = eng.encodeCoords('coord', coordFromBins(...cBins));
entry = { id: nxt, kind: 'c', bins: cBins };
} else if (nxt === CFG.sizeTok) {
let hl;
if (preHead) { hl = preHead.size; preHead = null; }
else {
const th0 = performance.now();
hl = await eng.runHead('size');
tHeads += performance.now() - th0;
}
sBins = [argmax(hl, 0, CFG.L), argmax(hl, CFG.L, CFG.L)];
embNext = eng.encodeCoords('size', sizeFromBins(...sBins));
entry = { id: nxt, kind: 's', bins: sBins };
} else if (nxt === CFG.segTok && cBins && sBins) {
pushDet(cBins, sBins);
entry.obj = true;
cBins = sBins = null;
if (dets.length >= 100) return; // как раньше: сегток не фидим
}
emission.push(entry);
let useGather = true;
if (embNext) { eng.writeX(embNext); useGather = false; }
else if (st === 0) eng.device.queue.writeBuffer(eng.b.tokId, 0, new Uint32Array([nxt]));
const ts0 = performance.now();
nxt = await eng.decodeStepAuto(useGather);
tSteps += performance.now() - ts0; nSteps++;
}
};
// Драфт: fed-последовательность прошлого кейфрейма, coord-бины сдвинуты
// на дельту трека своего объекта (пропавшие в NMS — на медианную дельту),
// size-бины как были; последним — ожидаемый терминал.
const clampBin = (v) => Math.max(0, Math.min(CFG.L - 1, Math.round(v)));
const buildDraft = () => {
if (!lastEmission || lastEmission.query !== query || lastEmission.terminal == null) return null;
const { entries, terminal: prevTerm, objMap } = lastEmission;
if (!entries.length) return null;
// матчед-объекты: бины = центр ТЕКУЩЕГО трекнутого бокса (абсолютная
// позиция, корреляция каждый кадр снапит её к пикселям — устойчиво к
// ошибкам учёта пана); анматчед — прежние бины + медианный сдвиг центров
const centerBins = (b) => [clampBin((b.x1 + b.x2) / 2 * (CFG.L - 1)),
clampBin((b.y1 + b.y2) / 2 * (CFG.L - 1))];
const shifts = tracker ? tracker.boxes.map((t) => [
(t.box.x1 + t.box.x2 - t.det.box.x1 - t.det.box.x2) / 2 * (CFG.L - 1),
(t.box.y1 + t.box.y2 - t.det.box.y1 - t.det.box.y2) / 2 * (CFG.L - 1)]) : [];
const med = (a) => (a.length ? a.slice().sort((x, y) => x - y)[a.length >> 1] : 0);
const mShift = [med(shifts.map((s) => s[0])), med(shifts.map((s) => s[1]))];
let obj = 0;
const out = entries.map((e) => {
if (e.kind === 'c') {
const t = tracker && tracker.boxes[objMap[obj]];
const raw = t ? centerBins(t.box)
: [clampBin(e.bins[0] + mShift[0]), clampBin(e.bins[1] + mShift[1])];
const bins = [clampBin(raw[0] + specBias[0]), clampBin(raw[1] + specBias[1])];
return { id: e.id, kind: 'c', bins,
emb: eng.encodeCoords('coord', coordFromBins(...bins)) };
}
if (e.kind === 's') {
return { id: e.id, kind: 's', bins: e.bins.slice(),
emb: eng.encodeCoords('size', sizeFromBins(...e.bins)) };
}
if (e.obj) obj++;
return { id: e.id, kind: 'g' };
});
out.push({ id: prevTerm, kind: 'g' }); // ожидаемый терминал (не фидится)
return out;
};
// Спекулятивный цикл: чанки ≤ maxK через батчевый verify; акцепт — точное
// совпадение токена, для coord/size ещё и бины голов в пределах specTol
// (бины при этом берутся модельные — свежий якорь против дрейфа трека).
// Возврат: {done:true} — терминал достигнут, либо {nxt, preHead} — резюм
// последовательного декода с точки рассинхрона.
const binDiffs = []; // (модель - драфт) по coord-строкам — калибровка specBias
const runSpec = async (draft) => {
emission.push({ id: draft[0].id, kind: 'g' });
let cBins = null, sBins = null, pos = 0, lastRows = null, repairs = 0;
// Ремонт драфта: модель эмитит объект не тем порядком/со сдвигом — мы уже
// знаем её точные coord-бины из батчевых голов. Ближайший объект хвоста
// (L∞ ≤ 64 бина ~ 16 px) переставляется вперёд с модельными бинами,
// verify перезапускается с этого места (~1 проход вместо отката в seq).
const tryRepair = (fromIdx, bins) => {
let best = null, bestD = 65;
for (let j = fromIdx; j < draft.length - 1; j++) {
const e = draft[j];
if (e.kind !== 'c') continue;
if (!draft[j + 1] || draft[j + 1].kind !== 's' ||
!draft[j + 2] || draft[j + 2].id !== CFG.segTok) continue;
const d = Math.max(Math.abs(e.bins[0] - bins[0]), Math.abs(e.bins[1] - bins[1]));
if (d < bestD) { bestD = d; best = j; }
}
if (best == null) return null;
const candBins = draft[best].bins;
const tail = [
{ id: CFG.coordTok, kind: 'c', bins,
emb: eng.encodeCoords('coord', coordFromBins(...bins)) },
draft[best + 1], draft[best + 2],
...draft.slice(fromIdx, best), ...draft.slice(best + 3),
];
draft.length = fromIdx;
draft.push(...tail);
return candBins; // бины сматченного объекта — для калибровки
};
while (pos < draft.length - 1) {
const k = Math.min(eng.maxK, draft.length - 1 - pos, MAX_T - 1 - eng.T);
if (k <= 0) { // бюджет T исчерпан: этот токен перезапишет seqDecode
emission.pop();
return { nxt: draft[pos].id, preHead: lastRows };
}
const items = draft.slice(pos, pos + k).map((e) => ({ id: e.id, emb: e.emb || null }));
const tv0 = performance.now();
const r = await eng.verifyStep(items);
tSpec += performance.now() - tv0; nSpecRun++;
let repaired = false;
for (let i = 0; i < k; i++) {
const m = r.tokens[i];
const exp = draft[pos + i + 1];
const rows = () => ({
coord: r.coordLogits.slice(i * 2048, (i + 1) * 2048),
size: r.sizeLogits.slice(i * 2048, (i + 1) * 2048) });
if (STOP.has(m) || m === CFG.absence) { // модель закончила декод
eng.T = r.T0 + i + 1; terminal = m; nSpecOk += i;
return { done: true };
}
if (m !== exp.id) { // рассинхрон по токену
eng.T = r.T0 + i + 1; nSpecOk += i;
return { nxt: m, preHead: rows() };
}
if (exp.kind === 'c') {
const bins = coordBinsDedup(rows().coord);
if (Math.abs(bins[0] - exp.bins[0]) > OPTS.specTol ||
Math.abs(bins[1] - exp.bins[1]) > OPTS.specTol) {
const candBins = repairs < 2 ? tryRepair(pos + i + 1, bins) : null;
if (candBins) {
binDiffs.push([bins[0] - candBins[0], bins[1] - candBins[1]]);
repairs++; nSpecOk += i + 1;
cBins = bins; sBins = null;
emission.push({ id: CFG.coordTok, kind: 'c', bins });
eng.T = r.T0 + i + 1;
pos = pos + i + 1;
repaired = true;
break; // новый verify-чанк с ремонтного места
}
eng.T = r.T0 + i + 1; nSpecOk += i;
return { nxt: m, preHead: rows() }; // немутированные копии
}
binDiffs.push([bins[0] - exp.bins[0], bins[1] - exp.bins[1]]);
cBins = bins;
emission.push({ id: exp.id, kind: 'c', bins });
} else if (exp.kind === 's') {
const hl = rows().size;
const bins = [argmax(hl, 0, CFG.L), argmax(hl, CFG.L, CFG.L)];
if (Math.abs(bins[0] - exp.bins[0]) > OPTS.specTol ||
Math.abs(bins[1] - exp.bins[1]) > OPTS.specTol) {
if (repairs < 2) { // размер уплыл (масштаб) — рефид модельных бинов
repairs++; nSpecOk += i + 1;
const tail = [{ id: CFG.sizeTok, kind: 's', bins,
emb: eng.encodeCoords('size', sizeFromBins(...bins)) },
...draft.slice(pos + i + 2)];
draft.length = pos + i + 1;
draft.push(...tail);
sBins = bins;
emission.push({ id: CFG.sizeTok, kind: 's', bins });
eng.T = r.T0 + i + 1;
pos = pos + i + 1;
repaired = true;
break;
}
eng.T = r.T0 + i + 1; nSpecOk += i;
return { nxt: m, preHead: rows() };
}
sBins = bins;
emission.push({ id: exp.id, kind: 's', bins });
} else {
const entry = { id: exp.id, kind: 'g' };
if (exp.id === CFG.segTok && cBins && sBins) {
pushDet(cBins, sBins);
entry.obj = true;
cBins = sBins = null;
if (dets.length >= 100) { eng.T = r.T0 + i + 1; return { done: true }; }
}
emission.push(entry);
}
if (i === k - 1) lastRows = rows();
}
if (repaired) continue; // перечанк с ремонтного места
eng.T = r.T0 + k; nSpecOk += k; pos += k;
}
return { done: true };
};
let nxt = argmax(logits), preHead = null, specDone = false;
if (OPTS.spec && lastEmission) {
const recent = specHist.slice(-4);
const hopeless = recent.length >= 4 && !recent.some(Boolean);
if (!hopeless || ++specSkips % 5 === 0) {
const draft = buildDraft();
// драфт применим, если первый токен совпал и он обычный (gather-фид)
if (draft && draft.length > 1 && !draft[0].emb && draft[0].id === nxt) {
const fed0 = draft.length - 1;
const rs = await runSpec(draft);
if (rs.done) specDone = true;
else { nxt = rs.nxt; preHead = rs.preHead; }
specHist.push(nSpecOk >= fed0 * 0.5 ? 1 : 0); // окупилась = принята ≥половина драфта
if (specHist.length > 16) specHist.shift();
}
}
}
if (!specDone) await seqDecode(nxt, preHead);
// калибровка specBias: медианный резидуал coord-строк этого кейфрейма
if (binDiffs.length) {
const med = (a) => a.slice().sort((x, y) => x - y)[a.length >> 1];
const cl = (v) => Math.max(-200, Math.min(200, v));
specBias = [cl(Math.round(specBias[0] / 2 + med(binDiffs.map((d) => d[0])) / 2)),
cl(Math.round(specBias[1] / 2 + med(binDiffs.map((d) => d[1])) / 2))];
}
const final = nms(dets, 0.6);
lastEmission = { query, entries: emission, terminal,
objMap: dets.map((d) => final.indexOf(d)) };
// кадр детекции устарел на время heavy: сдвигаем боксы на пан,
// накопленный параллельными треками, чтобы поиск (+-16px) не терял цель
const shifted = final.map((d) => ({
x1: d.box.x1 + panDX, y1: d.box.y1 + panDY,
x2: d.box.x2 + panDX, y2: d.box.y2 + panDY }));
tracker.setKeyframe(final, gray, fw, fh, shifted); // шаблоны по кадру детекции,
// старт треков — с паном
lastKey = { gray, w: fw, h: fh, time: performance.now(), query,
boxes: final.map((d) => ({ ...d.box })) };
const ms = performance.now() - t0;
console.log(`[engine] done ${ms.toFixed(0)}ms, ${final.length} detections (${dets.length} raw); ` +
`prefill=${tPrefill.toFixed(0)} steps=${tSteps.toFixed(0)}(${nSteps}x${(tSteps / Math.max(1, nSteps)).toFixed(1)}) heads=${tHeads.toFixed(0)}` +
(nSpecRun ? ` spec=${tSpec.toFixed(0)}(${nSpecRun} verify, ${nSpecOk} принято)` : ''));
reply(final, ms, { ...presStats, prefillMs: Math.round(tPrefill),
decodeMs: Math.round(tSteps + tSpec + tHeads),
seqSteps: nSteps, specRuns: nSpecRun, specAccepted: nSpecOk });
} catch (e) {
console.error('[engine] detect error:', e);
post({ type: 'error', message: e.message || String(e) });
} finally {
image.close?.();
busy = false;
runQueued();
}
}
function runQueued() { const q = queued; queued = null; if (q) detect(q); }
self.onmessage = async (e) => {
if (e.data.type === 'load') await load(e.data);
else if (e.data.type === 'detect') detect(e.data);
else if (e.data.type === 'config') {
const { type, ...opts } = e.data;
Object.assign(OPTS, opts);
// videoRes: кратно 16, в [128, 512] (потолок патчей MAX_S=1088)
OPTS.videoRes = Math.max(128, Math.min(512, Math.round(OPTS.videoRes / 16) * 16));
console.log('[engine] config:', JSON.stringify(OPTS));
}
};