File size: 2,341 Bytes
e2c452c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 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: <canvas>, <img> 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;
}