// Sightfern on-device detector: Google MediaPipe Object Detector (EfficientDet-Lite0, COCO classes, Apache-2.0), // loaded only when the player opens the camera or picks a photo. The library and model files are downloaded once // (then cached for offline use by sw.js); every image is analysed inside the browser and never uploaded. // (c) 2026 CyberMax. All rights reserved. MediaPipe (c) Google LLC, Apache License 2.0. import { SPECIES } from './core.js'; export const MP_VERSION = '0.10.14'; export const MP_BASE = `https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${MP_VERSION}`; export const MODEL = 'https://storage.googleapis.com/mediapipe-models/object_detector/efficientdet_lite0/int8/1/efficientdet_lite0.tflite'; export const MIN_SCORE = 0.4; let pending = null; /** Load once. Returns { detect(source) -> { ms, items }, delegate }. Rejects when the device cannot run it. */ export function loadDetector(prefer = 'CPU') { // CPU (WASM) by default: measured fast and exact (tests/detect-speed.mjs); GPU is opt-in if (pending) return pending; pending = (async () => { const vision = await import(/* @vite-ignore */ `${MP_BASE}/vision_bundle.mjs`); const files = await vision.FilesetResolver.forVisionTasks(`${MP_BASE}/wasm`); const opts = (delegate) => ({ baseOptions: { modelAssetPath: MODEL, delegate }, runningMode: 'IMAGE', scoreThreshold: 0.3, maxResults: 5, categoryAllowlist: Object.keys(SPECIES) }); let det, delegate = prefer; try { det = await vision.ObjectDetector.createFromOptions(files, opts(prefer)); } catch { delegate = 'CPU'; det = await vision.ObjectDetector.createFromOptions(files, opts('CPU')); } return { delegate, /** source: , or ImageBitmap. Boxes are in the source's pixel space. */ detect(source) { const t = performance.now(); const r = det.detect(source); const items = (r.detections || []).map((d) => ({ species: d.categories[0].categoryName, score: d.categories[0].score, box: { x: d.boundingBox.originX, y: d.boundingBox.originY, w: d.boundingBox.width, h: d.boundingBox.height } })) .filter((d) => d.species in SPECIES).sort((a, b) => b.score - a.score); return { ms: performance.now() - t, items }; }, }; })(); pending.catch(() => { pending = null; }); return pending; }