Files
Project_Velocity/app/src/hooks/useMediapipeFaceLandmarker.ts
2026-04-13 02:35:24 +05:30

101 lines
2.7 KiB
TypeScript

import { useState, useEffect, useRef, useCallback } from 'react';
import {
FaceLandmarker,
FilesetResolver,
} from '@mediapipe/tasks-vision';
export interface BlendShapeCategory {
categoryName: string;
score: number;
displayName: string;
index: number;
}
export interface FaceLandmarkerResult {
faceBlendshapes: Array<{ categories: BlendShapeCategory[] }>;
faceLandmarks: Array<Array<{ x: number; y: number; z: number }>>;
}
const MODEL_URL =
import.meta.env.VITE_MEDIAPIPE_MODEL_URL ??
'/mediapipe/assets/face_landmarker.task';
const WASM_ROOT =
import.meta.env.VITE_MEDIAPIPE_WASM_ROOT ??
'/mediapipe/wasm';
interface UseFaceLandmarkerReturn {
isLoading: boolean;
isReady: boolean;
error: string | null;
detectFrame: (
videoElement: HTMLVideoElement,
timestampMs: number,
) => FaceLandmarkerResult | null;
}
export function useMediapipeFaceLandmarker(): UseFaceLandmarkerReturn {
const landmarkerRef = useRef<FaceLandmarker | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isReady, setIsReady] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
async function init() {
try {
const filesetResolver = await FilesetResolver.forVisionTasks(WASM_ROOT);
const landmarker = await FaceLandmarker.createFromOptions(filesetResolver, {
baseOptions: {
modelAssetPath: MODEL_URL,
delegate: 'GPU',
},
outputFaceBlendshapes: true,
runningMode: 'VIDEO',
numFaces: 3,
minFaceDetectionConfidence: 0.65,
minFacePresenceConfidence: 0.6,
minTrackingConfidence: 0.6,
});
if (cancelled) {
landmarker.close();
return;
}
landmarkerRef.current = landmarker;
setIsLoading(false);
setIsReady(true);
} catch (err) {
if (cancelled) return;
console.error('[MediaPipe] Initialization failed:', err);
setError(err instanceof Error ? err.message : 'MediaPipe failed to initialize.');
setIsLoading(false);
}
}
void init();
return () => {
cancelled = true;
landmarkerRef.current?.close();
landmarkerRef.current = null;
};
}, []);
const detectFrame = useCallback(
(videoElement: HTMLVideoElement, timestampMs: number): FaceLandmarkerResult | null => {
if (!landmarkerRef.current || !isReady) return null;
try {
return landmarkerRef.current.detectForVideo(videoElement, timestampMs) as FaceLandmarkerResult;
} catch {
return null;
}
},
[isReady],
);
return { isLoading, isReady, error, detectFrame };
}