{"version":3,"file":"FaceDetector-FhIAuqGI.cjs","sources":["../src/FaceDetector.ts"],"sourcesContent":["import {\n  Detection,\n  FaceDetector as FaceDetectorMediaPipe,\n  FilesetResolver,\n  ImageEmbedder,\n} from '@mediapipe/tasks-vision';\nimport {\n  Embedding,\n  EmbeddingRequest,\n  EmbeddingResult,\n  FaceDetection,\n  FaceDetectionOptions,\n  FaceDetectorState\n} from './types';\n\nconst DEFAULT_DETECTION_MODEL =\n  'https://storage.googleapis.com/mediapipe-models/face_detector/blaze_face_short_range/float16/1/blaze_face_short_range.tflite';\nconst DEFAULT_WASM_PATH = 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision/wasm';\n\n/**\n * A class for detecting and embedding faces.\n * \n * @example\n * ```ts\n * const faceDetector = new FaceDetector({\n *   device: 'GPU',\n *   mode: 'IMAGE',\n *   minDetectionConfidence: 0.5,\n * });\n * await faceDetector.initialize();\n * const detections = faceDetector.detectFromImage(imageElement);\n * const embeddings = faceDetector.embed({\n *   source: imageElement,\n *   detection: detections[0],\n * });\n * const similarity = FaceDetector.cosineSimilarity(embeddings[0], embeddings[1]);\n * if (similarity > 0.5) {\n *   console.log('The faces are similar');\n * } else {\n *   console.log('The faces are different');\n * }\n * ```\n */\nexport class FaceDetector {\n  private faceDetector: FaceDetectorMediaPipe | null = null;\n  private faceEmbedder: ImageEmbedder | null = null;\n  private options: FaceDetectionOptions;\n  private _state: FaceDetectorState = 'not_initialized';\n  private _error: Error | null = null;\n\n  constructor(options: FaceDetectionOptions) {\n    this.options = options;\n  }\n\n  get state() {\n    return this._state;\n  }\n\n  get error() {\n    return this._error;\n  }\n\n  async initialize() {\n    if (this.state === 'initialized') {\n      return;\n    }\n    if (this.state === 'initializing') {\n      return;\n    }\n\n    try {\n      this._state = 'initializing';\n      this._error = null;\n      const vision = await FilesetResolver.forVisionTasks(\n        this.options.wasmPath || DEFAULT_WASM_PATH\n      );\n\n      if (this.options.embeddingModelPath) {\n        // create face detector and face embedder in parallel\n        const [faceDetector, faceEmbedder] = await Promise.all([\n          FaceDetectorMediaPipe.createFromOptions(vision, {\n            baseOptions: {\n              modelAssetPath: this.options.detectionModelPath || DEFAULT_DETECTION_MODEL,\n              delegate: this.options.device,\n            },\n            runningMode: this.options.mode,\n            minDetectionConfidence: this.options.minDetectionConfidence,\n          }),\n          ImageEmbedder.createFromOptions(vision, {\n            baseOptions: {\n              modelAssetPath: this.options.embeddingModelPath,\n              delegate: this.options.device,\n            },\n            runningMode: this.options.mode,\n          })\n        ]);\n\n        this.faceDetector = faceDetector;\n        this.faceEmbedder = faceEmbedder;\n      } else {\n        // Create face detector but not face embedder if no embedding model path\n        this.faceDetector = await FaceDetectorMediaPipe.createFromOptions(vision, {\n          baseOptions: {\n            modelAssetPath: this.options.detectionModelPath || DEFAULT_DETECTION_MODEL,\n            delegate: this.options.device,\n          },\n          runningMode: this.options.mode,\n          minDetectionConfidence: this.options.minDetectionConfidence,\n        });\n      }\n\n      this._state = 'initialized';\n    } catch (error) {\n      this._state = 'error';\n      this._error = error instanceof Error ? error : new Error(String(error));\n      throw error;\n    }\n  }\n\n  /**\n   * Detects faces from an image element.\n   * \n   * @example\n   * ```ts\n   * const detections = faceDetector.detectFromImage(imageElement);\n   * if (detections.length > 0) {\n   *   console.log('The image contains ' + detections.length + ' faces');\n   * } else {\n   *   console.log('The image does not contain any faces');\n   * }\n   * ```\n   * @param imageElement - The image element to detect faces from.\n   * @returns The detections. If no faces are detected, returns an empty array.\n   */\n  detectFromImage(imageElement: HTMLImageElement): FaceDetection[] {\n    if (this.state !== 'initialized') {\n      throw new Error('Face detector not initialized');\n    }\n    return this.faceDetector?.detect(imageElement).detections ?? [];\n  }\n\n  /**\n   * Detects faces from a video element.\n   * \n   * @example\n   * ```ts\n   * const detections = faceDetector.detectFromVideo(videoElement, timestamp);\n   * if (detections.length > 0) {\n   *   console.log('The video contains ' + detections.length + ' faces at timestamp ' + timestamp);\n   * } else {\n   *   console.log('The video does not contain any faces');\n   * }\n   * ```\n   * @param videoElement - The video element to detect faces from.\n   * @param timestamp - The timestamp of the video element.\n   * @returns The detections.\n   */\n  detectFromVideo(\n    videoElement: HTMLVideoElement,\n    timestamp: number\n  ): FaceDetection[] {\n    if (this.state !== 'initialized') {\n      throw new Error('Face detector not initialized');\n    }\n    const detections = this.faceDetector?.detectForVideo(videoElement, timestamp);\n    return detections?.detections ?? [];\n  }\n\n  /**\n   * Validates and normalizes a bounding box.\n   * @param bbox - The bounding box to validate and normalize.\n   * @param mediaWidth - The width of the media.\n   * @param mediaHeight - The height of the media.\n   * @returns The normalized bounding box.\n   */\n  private validateAndNormalizeRegion(\n    bbox: NonNullable<Detection['boundingBox']>,\n    mediaWidth: number,\n    mediaHeight: number\n  ) {\n    const isNormalized =\n      bbox.originX <= 1 && bbox.originY <= 1 && bbox.width <= 1 && bbox.height <= 1;\n    let left, top, right, bottom;\n    if (isNormalized) {\n      left = bbox.originX;\n      top = bbox.originY;\n      right = bbox.originX + bbox.width;\n      bottom = bbox.originY + bbox.height;\n    } else {\n      left = bbox.originX / mediaWidth;\n      top = bbox.originY / mediaHeight;\n      right = (bbox.originX + bbox.width) / mediaWidth;\n      bottom = (bbox.originY + bbox.height) / mediaHeight;\n    }\n    left = Math.max(0, Math.min(1, left));\n    top = Math.max(0, Math.min(1, top));\n    right = Math.max(0, Math.min(1, right));\n    bottom = Math.max(0, Math.min(1, bottom));\n    if (right <= left || bottom <= top) {\n      throw new Error('Invalid bounding box dimensions');\n    }\n    return { left, top, right, bottom };\n  }\n\n  /**\n   * Embeds a detected face into a tensor.\n   * The resulting tensor can be then compared to other embeddings using cosine similarity or other distance metrics.\n   * \n   * @example\n   * ```ts\n   * const result = imageFaceDetector.embed({\n   *   source: imageElement,\n   *   detection: faceDetectedFromImageElement,\n   * });\n   * \n   * const result2 = videoFaceDetector.embed({\n   *   source: videoElement,\n   *   detection: faceDetectedFromVideoElement,\n   *   timestamp: performance.now()\n   * });\n   *\n   * const similarity = FaceDetector.cosineSimilarity(result.embeddings[0], result2.embeddings[0]);\n   * if (similarity > 0.5) {\n   *   console.log('The faces are similar');\n   * } else {\n   *   console.log('The faces are different');\n   * }\n   * ```\n   * @param request - The request for embedding a face.\n   * @returns The embedded face tensor or null if no bounding box is found.\n   * @throws Error if face embedder is not initialized (embeddingModelPath not provided)\n   */\n  embed(request: EmbeddingRequest): EmbeddingResult | null {\n    if (this.state !== 'initialized') {\n      throw new Error('Face detector not initialized');\n    }\n\n    if (!this.faceEmbedder) {\n      throw new Error(\n        'Face embedder not initialized. Please provide embeddingModelPath in options.'\n      );\n    }\n\n    const { source, detection, timestamp } = request;\n    const bbox = detection.boundingBox;\n    if (!bbox) {\n      console.warn('No bounding box found for embedding');\n      return null;\n    }\n    let mediaWidth: number, mediaHeight: number;\n    if (source instanceof HTMLImageElement) {\n      mediaWidth = source.naturalWidth;\n      mediaHeight = source.naturalHeight;\n      if (this.options.mode !== 'IMAGE') {\n        throw new Error('Cannot embed image in video mode');\n      }\n      const regionOfInterest = this.validateAndNormalizeRegion(bbox, mediaWidth, mediaHeight);\n      return this.faceEmbedder.embed(source, {\n        regionOfInterest,\n        rotationDegrees: bbox.angle,\n      });\n    } else if (source instanceof HTMLVideoElement) {\n      mediaWidth = source.videoWidth;\n      mediaHeight = source.videoHeight;\n      if (this.options.mode !== 'VIDEO') {\n        throw new Error('Cannot embed video in image mode');\n      }\n      const regionOfInterest = this.validateAndNormalizeRegion(bbox, mediaWidth, mediaHeight);\n      return this.faceEmbedder.embedForVideo(source, timestamp ?? performance.now(), {\n        regionOfInterest,\n        rotationDegrees: bbox.angle,\n      });\n    }\n\n    throw new Error('Invalid source type');\n  }\n\n  /**\n   * Computes the cosine similarity between two embeddings.\n   * The cosine similarity score is between -1 and 1, where 1 means the embeddings are identical, and -1 means they are completely different.\n   * \n   * @example\n   * ```ts\n   * const similarity = FaceDetector.cosineSimilarity(faceEmbedding1, faceEmbedding2);\n   * if (similarity > 0.5) {\n   *   console.log('The embeddings are similar');\n   * } else {\n   *   console.log('The embeddings are different');\n   * }\n   * ```\n   * @param a - The first embedding.\n   * @param b - The second embedding.\n   * @returns The cosine similarity score.\n   */\n  static cosineSimilarity(a: Embedding, b: Embedding): number {\n    return ImageEmbedder.cosineSimilarity(a, b);\n  }\n}\n"],"names":["FilesetResolver","FaceDetectorMediaPipe","ImageEmbedder"],"mappings":"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"}