Spaces:
Running
Running
| // MediaPipe Tasks Vision — HandLandmarker. Loaded from CDN (online delivery, per plan). | |
| // The heavy library is imported LAZILY (dynamic import inside initHandTracking) so page load and | |
| // the Start button never depend on the CDN; a CDN failure becomes a catchable startup error. | |
| const LIB_URL = "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.14/vision_bundle.mjs"; | |
| const WASM_PATH = "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.14/wasm"; | |
| const MODEL_URL = | |
| "https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task"; | |
| // Landmarks that sit around the palm; averaging them gives a stable anchor for the hand icon | |
| // (steadier than a single fingertip, and matches a big palm-shaped cursor). | |
| const PALM_LANDMARKS = [0, 5, 9, 13, 17]; | |
| let handLandmarker = null; | |
| export async function initHandTracking() { | |
| const { HandLandmarker, FilesetResolver } = await import(LIB_URL); | |
| const vision = await FilesetResolver.forVisionTasks(WASM_PATH); | |
| const options = { | |
| baseOptions: { modelAssetPath: MODEL_URL, delegate: "GPU" }, | |
| runningMode: "video", | |
| numHands: 1, | |
| }; | |
| try { | |
| handLandmarker = await HandLandmarker.createFromOptions(vision, options); | |
| } catch (err) { | |
| // Some machines/browsers lack a usable GPU delegate — fall back to CPU. | |
| console.warn("HandLandmarker GPU init failed, falling back to CPU:", err); | |
| options.baseOptions.delegate = "CPU"; | |
| handLandmarker = await HandLandmarker.createFromOptions(vision, options); | |
| } | |
| return handLandmarker; | |
| } | |
| // Returns { x, y } normalized [0,1] palm anchor in the UN-flipped video frame, or null if no hand. | |
| // `timestampMs` must be monotonically increasing (the requestAnimationFrame timestamp works). | |
| export function detectHand(video, timestampMs) { | |
| if (!handLandmarker || !video.videoWidth) return null; | |
| let result; | |
| try { | |
| result = handLandmarker.detectForVideo(video, timestampMs); | |
| } catch (err) { | |
| // A rare frame can throw (e.g. timestamp hiccup) — skip it rather than crash the loop. | |
| return null; | |
| } | |
| if (!result || !result.landmarks || result.landmarks.length === 0) return null; | |
| const lm = result.landmarks[0]; | |
| let x = 0; | |
| let y = 0; | |
| for (const i of PALM_LANDMARKS) { | |
| x += lm[i].x; | |
| y += lm[i].y; | |
| } | |
| x /= PALM_LANDMARKS.length; | |
| y /= PALM_LANDMARKS.length; | |
| return { x, y }; | |
| } | |