From 77f3503a0bcee2bccc1467f8678fdd83798a241c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 28 May 2024 12:31:03 +0530 Subject: [PATCH 01/21] Make space --- .../components/Search/SearchBar/searchInput/MenuWithPeople.tsx | 2 +- web/apps/photos/src/components/ml/PeopleList.tsx | 2 +- web/apps/photos/src/services/face/{db.ts => db-old.ts} | 0 .../src/services/machineLearning/machineLearningService.ts | 2 +- web/apps/photos/src/services/machineLearning/mlWorkManager.ts | 2 +- web/apps/photos/src/services/searchService.ts | 2 +- web/apps/photos/src/types/search/index.ts | 2 +- 7 files changed, 6 insertions(+), 6 deletions(-) rename web/apps/photos/src/services/face/{db.ts => db-old.ts} (100%) diff --git a/web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx b/web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx index 3b739520e2..aaca1c3906 100644 --- a/web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx +++ b/web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx @@ -5,7 +5,7 @@ import { t } from "i18next"; import { AppContext } from "pages/_app"; import { useContext } from "react"; import { components } from "react-select"; -import { IndexStatus } from "services/face/db"; +import { IndexStatus } from "services/face/db-old"; import { Suggestion, SuggestionType } from "types/search"; const { Menu } = components; diff --git a/web/apps/photos/src/components/ml/PeopleList.tsx b/web/apps/photos/src/components/ml/PeopleList.tsx index da003d97d5..c77bb38a67 100644 --- a/web/apps/photos/src/components/ml/PeopleList.tsx +++ b/web/apps/photos/src/components/ml/PeopleList.tsx @@ -4,7 +4,7 @@ import { Skeleton, styled } from "@mui/material"; import { Legend } from "components/PhotoViewer/styledComponents/Legend"; import { t } from "i18next"; import React, { useEffect, useState } from "react"; -import mlIDbStorage from "services/face/db"; +import mlIDbStorage from "services/face/db-old"; import type { Person } from "services/face/people"; import { EnteFile } from "types/file"; diff --git a/web/apps/photos/src/services/face/db.ts b/web/apps/photos/src/services/face/db-old.ts similarity index 100% rename from web/apps/photos/src/services/face/db.ts rename to web/apps/photos/src/services/face/db-old.ts diff --git a/web/apps/photos/src/services/machineLearning/machineLearningService.ts b/web/apps/photos/src/services/machineLearning/machineLearningService.ts index 954a88c66d..f871584743 100644 --- a/web/apps/photos/src/services/machineLearning/machineLearningService.ts +++ b/web/apps/photos/src/services/machineLearning/machineLearningService.ts @@ -4,7 +4,7 @@ import PQueue from "p-queue"; import mlIDbStorage, { ML_SEARCH_CONFIG_NAME, type MinimalPersistedFileData, -} from "services/face/db"; +} from "services/face/db-old"; import { putFaceEmbedding } from "services/face/remote"; import { getLocalFiles } from "services/fileService"; import { EnteFile } from "types/file"; diff --git a/web/apps/photos/src/services/machineLearning/mlWorkManager.ts b/web/apps/photos/src/services/machineLearning/mlWorkManager.ts index c1b2ef6a70..0ec5f29541 100644 --- a/web/apps/photos/src/services/machineLearning/mlWorkManager.ts +++ b/web/apps/photos/src/services/machineLearning/mlWorkManager.ts @@ -8,7 +8,7 @@ import { getToken, getUserID } from "@ente/shared/storage/localStorage/helpers"; import debounce from "debounce"; import PQueue from "p-queue"; import { createFaceComlinkWorker } from "services/face"; -import mlIDbStorage from "services/face/db"; +import mlIDbStorage from "services/face/db-old"; import type { DedicatedMLWorker } from "services/face/face.worker"; import { EnteFile } from "types/file"; diff --git a/web/apps/photos/src/services/searchService.ts b/web/apps/photos/src/services/searchService.ts index 4bbab115c3..b48778f690 100644 --- a/web/apps/photos/src/services/searchService.ts +++ b/web/apps/photos/src/services/searchService.ts @@ -2,7 +2,7 @@ import { FILE_TYPE } from "@/media/file-type"; import log from "@/next/log"; import * as chrono from "chrono-node"; import { t } from "i18next"; -import mlIDbStorage from "services/face/db"; +import mlIDbStorage from "services/face/db-old"; import type { Person } from "services/face/people"; import { defaultMLVersion } from "services/machineLearning/machineLearningService"; import { Collection } from "types/collection"; diff --git a/web/apps/photos/src/types/search/index.ts b/web/apps/photos/src/types/search/index.ts index 33f5eba9a0..3f3de9b460 100644 --- a/web/apps/photos/src/types/search/index.ts +++ b/web/apps/photos/src/types/search/index.ts @@ -1,5 +1,5 @@ import { FILE_TYPE } from "@/media/file-type"; -import { IndexStatus } from "services/face/db"; +import { IndexStatus } from "services/face/db-old"; import type { Person } from "services/face/people"; import { City } from "services/locationSearchService"; import { LocationTagData } from "types/entity"; From 8ea7a742b1ea50b227cdc88c9f5daabe53291547 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 28 May 2024 12:57:40 +0530 Subject: [PATCH 02/21] Outline --- web/apps/photos/src/services/face/db.ts | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 web/apps/photos/src/services/face/db.ts diff --git a/web/apps/photos/src/services/face/db.ts b/web/apps/photos/src/services/face/db.ts new file mode 100644 index 0000000000..8edaaca6b8 --- /dev/null +++ b/web/apps/photos/src/services/face/db.ts @@ -0,0 +1,27 @@ +/** + * The faces in a file (and an embedding for each of them). + * + * This interface describes the format of both local and remote face data. + * + * - Local face detections and embeddings (collectively called as the face + * index) are generated by the current client when uploading a file (or when + * noticing a file which doesn't yet have a face index), stored in the local + * IndexedDB ("face/db") and also uploaded (E2EE) to remote. + * + * - Remote embeddings are fetched by subsequent clients to avoid them having to + * reindex (indexing faces is a costly operation, esp for mobile clients). + * + * In both these scenarios (whether generated locally or fetched from remote), + * we end up with an face index described by this {@link FaceIndex} interface. + * + * It has a top level envelope with information about the client (in particular + * the primary key {@link fileID}), an inner envelope {@link faceEmbedding} with + * metadata about the indexing, and an array of {@link faces} each containing + * the result of a face detection and an embedding for that detected face. + * + * This last one (faceEmbedding > faces > embedding) is the "actual" embedding, + * but sometimes we colloquially refer to the inner envelope (the + * "faceEmbedding") also an embedding since a file can have other types of + * embedding (envelopes) like a "clipEmbedding". + */ +export interface FaceIndex {} From 3664532f9164bb3dfb6af83c19c8a9df1a47c28d Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 28 May 2024 13:49:03 +0530 Subject: [PATCH 03/21] Document --- web/apps/photos/src/services/face/db.ts | 122 ++++++++++++++++++++++-- 1 file changed, 116 insertions(+), 6 deletions(-) diff --git a/web/apps/photos/src/services/face/db.ts b/web/apps/photos/src/services/face/db.ts index 8edaaca6b8..ff0a1061b9 100644 --- a/web/apps/photos/src/services/face/db.ts +++ b/web/apps/photos/src/services/face/db.ts @@ -1,3 +1,5 @@ +import type { Box, Point } from "./types"; + /** * The faces in a file (and an embedding for each of them). * @@ -14,14 +16,122 @@ * In both these scenarios (whether generated locally or fetched from remote), * we end up with an face index described by this {@link FaceIndex} interface. * - * It has a top level envelope with information about the client (in particular + * It has a top level envelope with information about the file (in particular * the primary key {@link fileID}), an inner envelope {@link faceEmbedding} with * metadata about the indexing, and an array of {@link faces} each containing * the result of a face detection and an embedding for that detected face. * - * This last one (faceEmbedding > faces > embedding) is the "actual" embedding, - * but sometimes we colloquially refer to the inner envelope (the - * "faceEmbedding") also an embedding since a file can have other types of - * embedding (envelopes) like a "clipEmbedding". + * The word embedding is used to refer two things: The last one (faceEmbedding > + * faces > embedding) is the "actual" embedding, but sometimes we colloquially + * refer to the inner envelope (the "faceEmbedding") also an embedding since a + * file can have other types of embedding (envelopes), e.g. a "clipEmbedding". */ -export interface FaceIndex {} +export interface FaceIndex { + /** + * The ID of the {@link EnteFile} whose index this is. + * + * This is used as the primary key when storing the index locally (An + * {@link EnteFile} is guaranteed to have its fileID be unique in the + * namespace of the user. Even if someone shares a file with the user the + * user will get a file entry with a fileID unique to them). + */ + fileID: number; + /** + * The width (in px) of the image (file). + */ + width: number; + /** + * The height (in px) of the image (file). + */ + height: number; + /** + * The "face embedding" for the file. + * + * This is an envelope that contains a list of indexed faces and metadata + * about the indexing. + */ + faceEmbedding: { + /** + * An integral version number of the indexing algorithm / pipeline. + * + * Clients agree out of band what a particular version means. The + * guarantee is that an embedding with a particular version will be the + * same (to negligible floating point epsilons) irrespective of the + * client that indexed the file. + */ + version: number; + /** The UA for the client which generated this embedding. */ + client: string; + /** The list of faces (and their embeddings) detected in the file. */ + faces: Face[]; + }; +} + +/** + * A face detected in a file, and an embedding for this detected face. + * + * During face indexing, we first detect all the faces in a particular file. + * Then for each such detected region, we compute an embedding of that part of + * the file. Together, this detection region and the emedding travel together in + * this {@link Face} interface. + */ +export interface Face { + /** + * A unique identifier for the face. + * + * This ID is guaranteed to be unique for all the faces detected in all the + * files for the user. In particular, each file can have multiple faces but + * they all will get their own unique {@link faceID}. + */ + faceID: string; + /** + * The face detection. Describes the region within the image that was + * detected to be a face, and a set of landmarks (e.g. "eyes") of the + * detection. + * + * All coordinates are relative within the image's dimension, i.e. they have + * been normalized to lie between 0 and 1, with 0 being the left (or top) + * and 1 being the width (or height) of the image. + */ + detection: { + /** + * The region within the image that contains the face. + * + * All coordinates and sizes are between 0 and 1, normalized by the + * dimensions of the image. + * */ + box: Box; + /** + * Face "landmarks", e.g. eyes. + * + * The exact landmarks and their order depends on the face detection + * algorithm being used. + * + * The coordinatesare between 0 and 1, normalized by the dimensions of + * the image. + */ + landmarks: Point[]; + }; + /** + * An correctness probability (0 to 1) that the face detection algorithm + * gave to the detection. Higher values are better. + */ + score: number; + /** + * The computed blur for the detected face. + * + * The exact semantics and range for these (floating point) values depend on + * the face indexing algorithm / pipeline version being used. + * */ + blur: number; + /** + * An embedding for the face. + * + * This is an opaque numeric (signed floating point) vector whose semantics + * and length depend on the version of the face indexing algorithm / + * pipeline that we are using. However, within a set of embeddings with the + * same version, the property is that two such embedding vectors will be + * "cosine similar" to each other if they are both faces of the same person. + */ + embedding: number[]; +} From 5e49b8a528c362d7faeb59c8a84246b1a4049fcf Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 28 May 2024 13:52:40 +0530 Subject: [PATCH 04/21] Move --- web/apps/photos/src/services/face/crop.ts | 2 +- web/apps/photos/src/services/face/db-old.ts | 2 +- web/apps/photos/src/services/face/db.ts | 137 ------------- web/apps/photos/src/services/face/f-index.ts | 15 +- web/apps/photos/src/services/face/remote.ts | 2 +- .../photos/src/services/face/types-old.ts | 46 +++++ web/apps/photos/src/services/face/types.ts | 181 +++++++++++++----- 7 files changed, 192 insertions(+), 193 deletions(-) delete mode 100644 web/apps/photos/src/services/face/db.ts create mode 100644 web/apps/photos/src/services/face/types-old.ts diff --git a/web/apps/photos/src/services/face/crop.ts b/web/apps/photos/src/services/face/crop.ts index 369dfc654a..d4d3753825 100644 --- a/web/apps/photos/src/services/face/crop.ts +++ b/web/apps/photos/src/services/face/crop.ts @@ -1,5 +1,5 @@ import { blobCache } from "@/next/blob-cache"; -import type { Box, Face, FaceAlignment } from "./types"; +import type { Box, Face, FaceAlignment } from "./types-old"; export const saveFaceCrop = async (imageBitmap: ImageBitmap, face: Face) => { const faceCrop = extractFaceCrop(imageBitmap, face.alignment); diff --git a/web/apps/photos/src/services/face/db-old.ts b/web/apps/photos/src/services/face/db-old.ts index 4742dd9d73..a70e94bee7 100644 --- a/web/apps/photos/src/services/face/db-old.ts +++ b/web/apps/photos/src/services/face/db-old.ts @@ -10,7 +10,7 @@ import { } from "idb"; import isElectron from "is-electron"; import type { Person } from "services/face/people"; -import type { MlFileData } from "services/face/types"; +import type { MlFileData } from "services/face/types-old"; import { DEFAULT_ML_SEARCH_CONFIG, MAX_ML_SYNC_ERROR_COUNT, diff --git a/web/apps/photos/src/services/face/db.ts b/web/apps/photos/src/services/face/db.ts deleted file mode 100644 index ff0a1061b9..0000000000 --- a/web/apps/photos/src/services/face/db.ts +++ /dev/null @@ -1,137 +0,0 @@ -import type { Box, Point } from "./types"; - -/** - * The faces in a file (and an embedding for each of them). - * - * This interface describes the format of both local and remote face data. - * - * - Local face detections and embeddings (collectively called as the face - * index) are generated by the current client when uploading a file (or when - * noticing a file which doesn't yet have a face index), stored in the local - * IndexedDB ("face/db") and also uploaded (E2EE) to remote. - * - * - Remote embeddings are fetched by subsequent clients to avoid them having to - * reindex (indexing faces is a costly operation, esp for mobile clients). - * - * In both these scenarios (whether generated locally or fetched from remote), - * we end up with an face index described by this {@link FaceIndex} interface. - * - * It has a top level envelope with information about the file (in particular - * the primary key {@link fileID}), an inner envelope {@link faceEmbedding} with - * metadata about the indexing, and an array of {@link faces} each containing - * the result of a face detection and an embedding for that detected face. - * - * The word embedding is used to refer two things: The last one (faceEmbedding > - * faces > embedding) is the "actual" embedding, but sometimes we colloquially - * refer to the inner envelope (the "faceEmbedding") also an embedding since a - * file can have other types of embedding (envelopes), e.g. a "clipEmbedding". - */ -export interface FaceIndex { - /** - * The ID of the {@link EnteFile} whose index this is. - * - * This is used as the primary key when storing the index locally (An - * {@link EnteFile} is guaranteed to have its fileID be unique in the - * namespace of the user. Even if someone shares a file with the user the - * user will get a file entry with a fileID unique to them). - */ - fileID: number; - /** - * The width (in px) of the image (file). - */ - width: number; - /** - * The height (in px) of the image (file). - */ - height: number; - /** - * The "face embedding" for the file. - * - * This is an envelope that contains a list of indexed faces and metadata - * about the indexing. - */ - faceEmbedding: { - /** - * An integral version number of the indexing algorithm / pipeline. - * - * Clients agree out of band what a particular version means. The - * guarantee is that an embedding with a particular version will be the - * same (to negligible floating point epsilons) irrespective of the - * client that indexed the file. - */ - version: number; - /** The UA for the client which generated this embedding. */ - client: string; - /** The list of faces (and their embeddings) detected in the file. */ - faces: Face[]; - }; -} - -/** - * A face detected in a file, and an embedding for this detected face. - * - * During face indexing, we first detect all the faces in a particular file. - * Then for each such detected region, we compute an embedding of that part of - * the file. Together, this detection region and the emedding travel together in - * this {@link Face} interface. - */ -export interface Face { - /** - * A unique identifier for the face. - * - * This ID is guaranteed to be unique for all the faces detected in all the - * files for the user. In particular, each file can have multiple faces but - * they all will get their own unique {@link faceID}. - */ - faceID: string; - /** - * The face detection. Describes the region within the image that was - * detected to be a face, and a set of landmarks (e.g. "eyes") of the - * detection. - * - * All coordinates are relative within the image's dimension, i.e. they have - * been normalized to lie between 0 and 1, with 0 being the left (or top) - * and 1 being the width (or height) of the image. - */ - detection: { - /** - * The region within the image that contains the face. - * - * All coordinates and sizes are between 0 and 1, normalized by the - * dimensions of the image. - * */ - box: Box; - /** - * Face "landmarks", e.g. eyes. - * - * The exact landmarks and their order depends on the face detection - * algorithm being used. - * - * The coordinatesare between 0 and 1, normalized by the dimensions of - * the image. - */ - landmarks: Point[]; - }; - /** - * An correctness probability (0 to 1) that the face detection algorithm - * gave to the detection. Higher values are better. - */ - score: number; - /** - * The computed blur for the detected face. - * - * The exact semantics and range for these (floating point) values depend on - * the face indexing algorithm / pipeline version being used. - * */ - blur: number; - /** - * An embedding for the face. - * - * This is an opaque numeric (signed floating point) vector whose semantics - * and length depend on the version of the face indexing algorithm / - * pipeline that we are using. However, within a set of embeddings with the - * same version, the property is that two such embedding vectors will be - * "cosine similar" to each other if they are both faces of the same person. - */ - embedding: number[]; -} diff --git a/web/apps/photos/src/services/face/f-index.ts b/web/apps/photos/src/services/face/f-index.ts index 5197214b24..5e93f60bd6 100644 --- a/web/apps/photos/src/services/face/f-index.ts +++ b/web/apps/photos/src/services/face/f-index.ts @@ -2,14 +2,6 @@ import { FILE_TYPE } from "@/media/file-type"; import log from "@/next/log"; import { workerBridge } from "@/next/worker/worker-bridge"; import { Matrix } from "ml-matrix"; -import type { - Box, - Dimensions, - Face, - FaceAlignment, - FaceDetection, - MlFileData, -} from "services/face/types"; import { defaultMLVersion } from "services/machineLearning/machineLearningService"; import { getSimilarityTransformation } from "similarity-transformation"; import { @@ -28,6 +20,13 @@ import { pixelRGBBilinear, warpAffineFloat32List, } from "./image"; +import type { Box, Dimensions } from "./types"; +import type { + Face, + FaceAlignment, + FaceDetection, + MlFileData, +} from "./types-old"; /** * Index faces in the given file. diff --git a/web/apps/photos/src/services/face/remote.ts b/web/apps/photos/src/services/face/remote.ts index 3c64ca30cc..32d0fddad8 100644 --- a/web/apps/photos/src/services/face/remote.ts +++ b/web/apps/photos/src/services/face/remote.ts @@ -2,7 +2,7 @@ import log from "@/next/log"; import ComlinkCryptoWorker from "@ente/shared/crypto"; import { putEmbedding } from "services/embeddingService"; import type { EnteFile } from "types/file"; -import type { Face, FaceDetection, MlFileData, Point } from "./types"; +import type { Face, FaceDetection, MlFileData, Point } from "./types-old"; export const putFaceEmbedding = async ( enteFile: EnteFile, diff --git a/web/apps/photos/src/services/face/types-old.ts b/web/apps/photos/src/services/face/types-old.ts new file mode 100644 index 0000000000..66eec9cf55 --- /dev/null +++ b/web/apps/photos/src/services/face/types-old.ts @@ -0,0 +1,46 @@ +import type { Box, Dimensions, Point } from "./types"; + +export interface FaceDetection { + // box and landmarks is relative to image dimentions stored at mlFileData + box: Box; + landmarks?: Point[]; + probability?: number; +} + +export interface FaceAlignment { + /** + * An affine transformation matrix (rotation, translation, scaling) to align + * the face extracted from the image. + */ + affineMatrix: number[][]; + /** + * The bounding box of the transformed box. + * + * The affine transformation shifts the original detection box a new, + * transformed, box (possibily rotated). This property is the bounding box + * of that transformed box. It is in the coordinate system of the original, + * full, image on which the detection occurred. + */ + boundingBox: Box; +} + +export interface Face { + fileId: number; + detection: FaceDetection; + id: string; + + alignment?: FaceAlignment; + blurValue?: number; + + embedding?: Float32Array; + + personId?: number; +} + +export interface MlFileData { + fileId: number; + faces?: Face[]; + imageDimensions?: Dimensions; + mlVersion: number; + errorCount: number; +} diff --git a/web/apps/photos/src/services/face/types.ts b/web/apps/photos/src/services/face/types.ts index 0b1b2f9757..a1db97a9af 100644 --- a/web/apps/photos/src/services/face/types.ts +++ b/web/apps/photos/src/services/face/types.ts @@ -1,3 +1,139 @@ +/** + * The faces in a file (and an embedding for each of them). + * + * This interface describes the format of both local and remote face data. + * + * - Local face detections and embeddings (collectively called as the face + * index) are generated by the current client when uploading a file (or when + * noticing a file which doesn't yet have a face index), stored in the local + * IndexedDB ("face/db") and also uploaded (E2EE) to remote. + * + * - Remote embeddings are fetched by subsequent clients to avoid them having to + * reindex (indexing faces is a costly operation, esp for mobile clients). + * + * In both these scenarios (whether generated locally or fetched from remote), + * we end up with an face index described by this {@link FaceIndex} interface. + * + * It has a top level envelope with information about the file (in particular + * the primary key {@link fileID}), an inner envelope {@link faceEmbedding} with + * metadata about the indexing, and an array of {@link faces} each containing + * the result of a face detection and an embedding for that detected face. + * + * The word embedding is used to refer two things: The last one (faceEmbedding > + * faces > embedding) is the "actual" embedding, but sometimes we colloquially + * refer to the inner envelope (the "faceEmbedding") also an embedding since a + * file can have other types of embedding (envelopes), e.g. a "clipEmbedding". + */ +export interface FaceIndex { + /** + * The ID of the {@link EnteFile} whose index this is. + * + * This is used as the primary key when storing the index locally (An + * {@link EnteFile} is guaranteed to have its fileID be unique in the + * namespace of the user. Even if someone shares a file with the user the + * user will get a file entry with a fileID unique to them). + */ + fileID: number; + /** + * The width (in px) of the image (file). + */ + width: number; + /** + * The height (in px) of the image (file). + */ + height: number; + /** + * The "face embedding" for the file. + * + * This is an envelope that contains a list of indexed faces and metadata + * about the indexing. + */ + faceEmbedding: { + /** + * An integral version number of the indexing algorithm / pipeline. + * + * Clients agree out of band what a particular version means. The + * guarantee is that an embedding with a particular version will be the + * same (to negligible floating point epsilons) irrespective of the + * client that indexed the file. + */ + version: number; + /** The UA for the client which generated this embedding. */ + client: string; + /** The list of faces (and their embeddings) detected in the file. */ + faces: Face[]; + }; +} + +/** + * A face detected in a file, and an embedding for this detected face. + * + * During face indexing, we first detect all the faces in a particular file. + * Then for each such detected region, we compute an embedding of that part of + * the file. Together, this detection region and the emedding travel together in + * this {@link Face} interface. + */ +export interface Face { + /** + * A unique identifier for the face. + * + * This ID is guaranteed to be unique for all the faces detected in all the + * files for the user. In particular, each file can have multiple faces but + * they all will get their own unique {@link faceID}. + */ + faceID: string; + /** + * The face detection. Describes the region within the image that was + * detected to be a face, and a set of landmarks (e.g. "eyes") of the + * detection. + * + * All coordinates are relative to and normalized by the image's dimension, + * i.e. they have been normalized to lie between 0 and 1, with 0 being the + * left (or top) and 1 being the width (or height) of the image. + */ + detection: { + /** + * The region within the image that contains the face. + * + * All coordinates and sizes are between 0 and 1, normalized by the + * dimensions of the image. + * */ + box: Box; + /** + * Face "landmarks", e.g. eyes. + * + * The exact landmarks and their order depends on the face detection + * algorithm being used. + * + * The coordinatesare between 0 and 1, normalized by the dimensions of + * the image. + */ + landmarks: Point[]; + }; + /** + * An correctness probability (0 to 1) that the face detection algorithm + * gave to the detection. Higher values are better. + */ + score: number; + /** + * The computed blur for the detected face. + * + * The exact semantics and range for these (floating point) values depend on + * the face indexing algorithm / pipeline version being used. + * */ + blur: number; + /** + * An embedding for the face. + * + * This is an opaque numeric (signed floating point) vector whose semantics + * and length depend on the version of the face indexing algorithm / + * pipeline that we are using. However, within a set of embeddings with the + * same version, the property is that two such embedding vectors will be + * "cosine similar" to each other if they are both faces of the same person. + */ + embedding: number[]; +} + /** The x and y coordinates of a point. */ export interface Point { x: number; @@ -21,48 +157,3 @@ export interface Box { /** The height of the box. */ height: number; } - -export interface FaceDetection { - // box and landmarks is relative to image dimentions stored at mlFileData - box: Box; - landmarks?: Point[]; - probability?: number; -} - -export interface FaceAlignment { - /** - * An affine transformation matrix (rotation, translation, scaling) to align - * the face extracted from the image. - */ - affineMatrix: number[][]; - /** - * The bounding box of the transformed box. - * - * The affine transformation shifts the original detection box a new, - * transformed, box (possibily rotated). This property is the bounding box - * of that transformed box. It is in the coordinate system of the original, - * full, image on which the detection occurred. - */ - boundingBox: Box; -} - -export interface Face { - fileId: number; - detection: FaceDetection; - id: string; - - alignment?: FaceAlignment; - blurValue?: number; - - embedding?: Float32Array; - - personId?: number; -} - -export interface MlFileData { - fileId: number; - faces?: Face[]; - imageDimensions?: Dimensions; - mlVersion: number; - errorCount: number; -} From 126727a9cc64f909df1d96ca54a31d0a3bd82119 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 28 May 2024 14:13:46 +0530 Subject: [PATCH 05/21] Document --- web/apps/photos/src/services/face/db.ts | 60 +++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 web/apps/photos/src/services/face/db.ts diff --git a/web/apps/photos/src/services/face/db.ts b/web/apps/photos/src/services/face/db.ts new file mode 100644 index 0000000000..1710f4382e --- /dev/null +++ b/web/apps/photos/src/services/face/db.ts @@ -0,0 +1,60 @@ +import type { FaceIndex } from "./types"; + +/** + * [Note: Face DB schema] + * + * There "face" database is made of two object stores: + * + * - "face-index": Contains {@link FaceIndex} objects, either indexed locally or + * fetched from remote storage. + * + * - "file-status": Contains {@link FileStatus} objects, one for each + * {@link EnteFile} that the current client knows about. + * + * Both the stores are keyed by {@link fileID}, and are expected to contain the + * exact same set of {@link fileID}s. The face-index can be thought of as the + * "original" indexing result, whilst file-status bookkeeps information about + * the indexing process (whether or not a file needs indexing, or if there were + * errors doing so). + * + * In tandem, these serve as the underlying storage for the functions exposed by + * this file. + */ + +/** + * Save the given {@link faceIndex} locally. + * + * @param faceIndex A {@link FaceIndex} representing the faces that we detected + * (and their corresponding embeddings) in some file. + * + * This function adds a new entry, overwriting any existing ones (No merging is + * performed, the existing entry is unconditionally overwritten). + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const saveFaceIndex = (faceIndex: FaceIndex) => {}; + +/** + * Record the existence of a fileID so that entities in the face indexing + * universe know about it. + * + * @param fileID The ID of an {@link EnteFile}. + * + * This function does not overwrite existing entries. If an entry already exists + * for the given {@link fileID} (e.g. if it was indexed and + * {@link saveFaceIndex} called with the result), its existing status remains + * unperturbed. + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const addFileEntry = (fileID: string) => {}; + +/** + * Increment the failure count associated with the given {@link fileID}. + * + * @param fileID The ID of an {@link EnteFile}. + * + * If an entry does not exist yet for the given file, then a new one is created + * and its failure count is set to 1. Otherwise the failure count of the + * existing entry is incremented. + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export const markIndexingFailed = (fileID: string) => {}; From f5947a0c4a72b604852780b6cc7dbb62ab3b3577 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 28 May 2024 15:04:33 +0530 Subject: [PATCH 06/21] Introduce idb --- web/apps/photos/src/services/face/db.ts | 9 ++++++++ web/docs/dependencies.md | 7 +++++- web/docs/storage.md | 30 ++++++++++++++++--------- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/web/apps/photos/src/services/face/db.ts b/web/apps/photos/src/services/face/db.ts index 1710f4382e..e099e823e3 100644 --- a/web/apps/photos/src/services/face/db.ts +++ b/web/apps/photos/src/services/face/db.ts @@ -20,6 +20,8 @@ import type { FaceIndex } from "./types"; * In tandem, these serve as the underlying storage for the functions exposed by * this file. */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const openFaceDB = () => {}; /** * Save the given {@link faceIndex} locally. @@ -58,3 +60,10 @@ export const addFileEntry = (fileID: string) => {}; */ // eslint-disable-next-line @typescript-eslint/no-unused-vars export const markIndexingFailed = (fileID: string) => {}; + +/** + * Clear any data stored by the face module. + * + * Meant to be called during logout. + */ +export const clearFaceData = () => {}; diff --git a/web/docs/dependencies.md b/web/docs/dependencies.md index 3ea8fb2409..6f959e61f5 100644 --- a/web/docs/dependencies.md +++ b/web/docs/dependencies.md @@ -161,11 +161,16 @@ some cases. - [heic-convert](https://github.com/catdad-experiments/heic-convert) is used for converting HEIC files (which browsers don't natively support) into JPEG. -## Processing +## General - [comlink](https://github.com/GoogleChromeLabs/comlink) provides a minimal layer on top of Web Workers to make them more easier to use. +- [idb](https://github.com/jakearchibald/idb) provides a promise API over the + browser-native IndexedDB APIs. + + > For more details about IDB and its role, see [storage.md](storage.md). + ## Photos app specific - [react-dropzone](https://github.com/react-dropzone/react-dropzone/) is a diff --git a/web/docs/storage.md b/web/docs/storage.md index 9f19a6a46d..ae6d01eec5 100644 --- a/web/docs/storage.md +++ b/web/docs/storage.md @@ -1,9 +1,15 @@ # Storage +## Session Storage + +Data tied to the browser tab's lifetime. + +We store the user's encryption key here. + ## Local Storage -Data in the local storage is persisted even after the user closes the tab (or -the browser itself). This is in contrast with session storage, where the data is +Data in the local storage is persisted even after the user closes the tab, or +the browser itself. This is in contrast with session storage, where the data is cleared when the browser tab is closed. The data in local storage is tied to the Document's origin (scheme + host). @@ -15,19 +21,23 @@ Some things that get stored here are: - Various user preferences -## Session Storage +## IndexedDB -Data tied to the browser tab's lifetime. +IndexedDB is a transactional NoSQL store provided by browsers. It has quite +large storage limits, and data is stored per origin (and remains persistent +across tab restarts). -We store the user's encryption key here. +Older code used the LocalForage library for storing things in Indexed DB. This +library falls back to localStorage in case Indexed DB storage is not available. -## Indexed DB +Newer code uses the idb library which provides a promise API over the IndexedDB, +but otherwise does not introduce any new abstractions. -We use the LocalForage library for storing things in Indexed DB. This library -falls back to localStorage in case Indexed DB storage is not available. +For more details, see: + +- https://web.dev/articles/indexeddb +- https://github.com/jakearchibald/idb -Indexed DB allows for larger sizes than local/session storage, and is generally -meant for larger, tabular data. ## OPFS From f1b2e2bec2b6790bfc4097379d184894c37af1df Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 28 May 2024 15:16:19 +0530 Subject: [PATCH 07/21] Update to idb 8 No breaking changes that impact us https://github.com/jakearchibald/idb/blob/main/CHANGELOG.md --- web/apps/photos/package.json | 2 +- web/apps/photos/src/services/face/db.ts | 1 + web/yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/web/apps/photos/package.json b/web/apps/photos/package.json index 0ec924b29b..001fc1036a 100644 --- a/web/apps/photos/package.json +++ b/web/apps/photos/package.json @@ -22,7 +22,7 @@ "ffmpeg-wasm": "file:./thirdparty/ffmpeg-wasm", "formik": "^2.1.5", "hdbscan": "0.0.1-alpha.5", - "idb": "^7.1.1", + "idb": "^8", "leaflet": "^1.9.4", "leaflet-defaulticon-compatibility": "^0.1.1", "localforage": "^1.9.0", diff --git a/web/apps/photos/src/services/face/db.ts b/web/apps/photos/src/services/face/db.ts index e099e823e3..a5fa854bcd 100644 --- a/web/apps/photos/src/services/face/db.ts +++ b/web/apps/photos/src/services/face/db.ts @@ -1,4 +1,5 @@ import type { FaceIndex } from "./types"; +// import { openDB } from "idb"; /** * [Note: Face DB schema] diff --git a/web/yarn.lock b/web/yarn.lock index aaa0d517a8..226b3add6b 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -2922,10 +2922,10 @@ i18next@^23.10: dependencies: "@babel/runtime" "^7.23.2" -idb@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" - integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== +idb@^8: + version "8.0.0" + resolved "https://registry.yarnpkg.com/idb/-/idb-8.0.0.tgz#33d7ed894ed36e23bcb542fb701ad579bfaad41f" + integrity sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw== ieee754@^1.2.1: version "1.2.1" From ca7b60921786bba64c493b96fd0190b8f8a851e4 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 28 May 2024 15:40:08 +0530 Subject: [PATCH 08/21] Schema --- web/apps/photos/src/services/face/db.ts | 53 ++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/web/apps/photos/src/services/face/db.ts b/web/apps/photos/src/services/face/db.ts index a5fa854bcd..4cbf6fda24 100644 --- a/web/apps/photos/src/services/face/db.ts +++ b/web/apps/photos/src/services/face/db.ts @@ -1,5 +1,6 @@ +import log from "@/next/log"; +import { openDB, type DBSchema } from "idb"; import type { FaceIndex } from "./types"; -// import { openDB } from "idb"; /** * [Note: Face DB schema] @@ -21,8 +22,56 @@ import type { FaceIndex } from "./types"; * In tandem, these serve as the underlying storage for the functions exposed by * this file. */ +interface FaceDBSchema extends DBSchema { + "face-index": { + key: string; + value: FaceIndex; + }; + "file-status": { + key: string; + value: FileStatus; + }; +} + +interface FileStatus { + /** The ID of the {@link EnteFile} whose indexing status we represent. */ + fileID: string; + /** + * `1` if we have indexed a file with this {@link fileID}, `0` otherwise. + * + * It is guaranteed that "face-index" will have an entry for the same + * {@link fileID} if and only if {@link isIndexed} is `1`. + * + * [Note: Boolean IndexedDB indexes]. + * + * IndexedDB does not (currently) supported indexes on boolean fields. + * https://github.com/w3c/IndexedDB/issues/76 + * + * As a workaround, we use numeric fields where `0` denotes `false` and `1` + * denotes `true`. + */ + isIndexed: number; + /** + * The number of times attempts to index this file failed. + * + * This is guaranteed to be `0` for files which have already been + * sucessfully indexed (i.e. files for which `isIndexed` is true). + */ + failureCount: number; +} + // eslint-disable-next-line @typescript-eslint/no-unused-vars -const openFaceDB = () => {}; +const openFaceDB = () => + openDB("face", 1, { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + upgrade(db, oldVersion, newVersion, tx) { + log.info(`Upgrading face DB ${oldVersion} => ${newVersion}`); + if (oldVersion < 1) { + // db.createObjectStore(); + } + }, + // TODO: FDB + }); /** * Save the given {@link faceIndex} locally. From 9887d44416b96dee33ad8b3f8e7fb9d4a302c90b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 28 May 2024 15:52:02 +0530 Subject: [PATCH 09/21] index --- web/apps/photos/src/services/face/db.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/web/apps/photos/src/services/face/db.ts b/web/apps/photos/src/services/face/db.ts index 4cbf6fda24..8f2ad681e2 100644 --- a/web/apps/photos/src/services/face/db.ts +++ b/web/apps/photos/src/services/face/db.ts @@ -30,6 +30,7 @@ interface FaceDBSchema extends DBSchema { "file-status": { key: string; value: FileStatus; + indexes: { isIndexed: number }; }; } @@ -42,6 +43,11 @@ interface FileStatus { * It is guaranteed that "face-index" will have an entry for the same * {@link fileID} if and only if {@link isIndexed} is `1`. * + * > Somewhat confusingly, we also have a (IndexedDB) "index" on this field. + * That (IDB) index allows us to effectively select {@link fileIDs} that + * still need indexing (where {@link isIndexed} is not `1`), so there is + * utility, it is just that if I say the word "index" one more time... + * * [Note: Boolean IndexedDB indexes]. * * IndexedDB does not (currently) supported indexes on boolean fields. @@ -64,10 +70,15 @@ interface FileStatus { const openFaceDB = () => openDB("face", 1, { // eslint-disable-next-line @typescript-eslint/no-unused-vars - upgrade(db, oldVersion, newVersion, tx) { + upgrade(db, oldVersion, newVersion) { log.info(`Upgrading face DB ${oldVersion} => ${newVersion}`); if (oldVersion < 1) { - // db.createObjectStore(); + db.createObjectStore("face-index", { keyPath: "fileID" }); + + const statusStore = db.createObjectStore("file-status", { + keyPath: "fileID", + }); + statusStore.createIndex("isIndexed", "isIndexed"); } }, // TODO: FDB @@ -86,8 +97,8 @@ const openFaceDB = () => export const saveFaceIndex = (faceIndex: FaceIndex) => {}; /** - * Record the existence of a fileID so that entities in the face indexing - * universe know about it. + * Record the existence of a file so that entities in the face indexing universe + * know about it (e.g. can index it if it is new and it needs indexing). * * @param fileID The ID of an {@link EnteFile}. * From f34a4d4a219f83c2ef17bad256b077339edff682 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 28 May 2024 18:18:41 +0530 Subject: [PATCH 10/21] lf --- web/apps/photos/src/services/face/db.ts | 1 - web/docs/storage.md | 1 - 2 files changed, 2 deletions(-) diff --git a/web/apps/photos/src/services/face/db.ts b/web/apps/photos/src/services/face/db.ts index 8f2ad681e2..886309ffbd 100644 --- a/web/apps/photos/src/services/face/db.ts +++ b/web/apps/photos/src/services/face/db.ts @@ -69,7 +69,6 @@ interface FileStatus { // eslint-disable-next-line @typescript-eslint/no-unused-vars const openFaceDB = () => openDB("face", 1, { - // eslint-disable-next-line @typescript-eslint/no-unused-vars upgrade(db, oldVersion, newVersion) { log.info(`Upgrading face DB ${oldVersion} => ${newVersion}`); if (oldVersion < 1) { diff --git a/web/docs/storage.md b/web/docs/storage.md index ae6d01eec5..f4b28bda16 100644 --- a/web/docs/storage.md +++ b/web/docs/storage.md @@ -38,7 +38,6 @@ For more details, see: - https://web.dev/articles/indexeddb - https://github.com/jakearchibald/idb - ## OPFS OPFS is used for caching entire files when we're running under Electron (the Web From b1e64cadf68975dca8a3839253e4a10b90b8bc48 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 29 May 2024 11:07:11 +0530 Subject: [PATCH 11/21] Lifecycle --- web/apps/photos/src/services/face/db.ts | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/web/apps/photos/src/services/face/db.ts b/web/apps/photos/src/services/face/db.ts index 886309ffbd..9f11425b7d 100644 --- a/web/apps/photos/src/services/face/db.ts +++ b/web/apps/photos/src/services/face/db.ts @@ -45,8 +45,8 @@ interface FileStatus { * * > Somewhat confusingly, we also have a (IndexedDB) "index" on this field. * That (IDB) index allows us to effectively select {@link fileIDs} that - * still need indexing (where {@link isIndexed} is not `1`), so there is - * utility, it is just that if I say the word "index" one more time... + * still need indexing (where {@link isIndexed} is not `1`), so it is all + * sensible, just that if I say the word "index" one more time... * * [Note: Boolean IndexedDB indexes]. * @@ -80,7 +80,22 @@ const openFaceDB = () => statusStore.createIndex("isIndexed", "isIndexed"); } }, - // TODO: FDB + blocking() { + log.info( + "Another client is attempting to open a new version of face DB", + ); + // TODO: FBD: close via our promise + // TODO: FBD: clear our promise + }, + blocked() { + log.warn( + "Waiting for an existing client to close their connection so that we can update the face DB version", + ); + }, + terminated() { + log.warn("Our connection to face DB was unexpectedly terminated"); + // TODO: FBD: clear our promise + }, }); /** From 2f7d1401cd81b5a1840db9056668365b4d52fcf1 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 29 May 2024 11:26:38 +0530 Subject: [PATCH 12/21] Promise --- web/apps/photos/src/services/face/db.ts | 37 +++++++++++++++++++++---- 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/web/apps/photos/src/services/face/db.ts b/web/apps/photos/src/services/face/db.ts index 9f11425b7d..14900638cd 100644 --- a/web/apps/photos/src/services/face/db.ts +++ b/web/apps/photos/src/services/face/db.ts @@ -66,9 +66,26 @@ interface FileStatus { failureCount: number; } -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const openFaceDB = () => - openDB("face", 1, { +/** + * A promise to the face DB. + * + * We open the database once (lazily), and thereafter save and reuse the promise + * each time something wants to connect to it. + * + * This promise can subsequently get cleared if we need to relinquish our + * connection (e.g. if another client wants to open the face DB with a newer + * version of the schema). + * + * Note that this is module specific state, so the main thread and each worker + * thread that calls the functions in this module will get their own independent + * connection. When we logout, the workers will presumably get resurrected and + * close their connections, while the connection kept by the main thread will be + * used to delete the database in {@link clearFaceData}. + */ +let _faceDB: ReturnType | undefined; + +const openFaceDB = async () => { + const db = await openDB("face", 1, { upgrade(db, oldVersion, newVersion) { log.info(`Upgrading face DB ${oldVersion} => ${newVersion}`); if (oldVersion < 1) { @@ -84,8 +101,8 @@ const openFaceDB = () => log.info( "Another client is attempting to open a new version of face DB", ); - // TODO: FBD: close via our promise - // TODO: FBD: clear our promise + db.close(); + _faceDB = undefined; }, blocked() { log.warn( @@ -94,9 +111,17 @@ const openFaceDB = () => }, terminated() { log.warn("Our connection to face DB was unexpectedly terminated"); - // TODO: FBD: clear our promise + _faceDB = undefined; }, }); + return db; +}; + +/** + * @returns a lazily created, cached connection to the face DB. + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const faceDB = () => (_faceDB ??= openFaceDB()); /** * Save the given {@link faceIndex} locally. From 0cae667b44f0071cac8a4c7ccdaf06e478e9736c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 29 May 2024 12:14:18 +0530 Subject: [PATCH 13/21] Add a close Ref: https://www.w3.org/TR/IndexedDB-2/ --- web/apps/photos/src/services/face/db.ts | 44 ++++++++++++++++++------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/web/apps/photos/src/services/face/db.ts b/web/apps/photos/src/services/face/db.ts index 14900638cd..adaf097cee 100644 --- a/web/apps/photos/src/services/face/db.ts +++ b/web/apps/photos/src/services/face/db.ts @@ -1,5 +1,5 @@ import log from "@/next/log"; -import { openDB, type DBSchema } from "idb"; +import { deleteDB, openDB, type DBSchema } from "idb"; import type { FaceIndex } from "./types"; /** @@ -78,8 +78,8 @@ interface FileStatus { * * Note that this is module specific state, so the main thread and each worker * thread that calls the functions in this module will get their own independent - * connection. When we logout, the workers will presumably get resurrected and - * close their connections, while the connection kept by the main thread will be + * connection. To ensure that all connections get torn down correctly, we need + * to call closeFaceDBConnection * used to delete the database in {@link clearFaceData}. */ let _faceDB: ReturnType | undefined; @@ -123,6 +123,35 @@ const openFaceDB = async () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const faceDB = () => (_faceDB ??= openFaceDB()); +/** + * Close the face DB connection (if any) opened by this module. + * + * To ensure proper teardown of the DB connections, this function must be called + * at least once by any execution context that has called any of the other + * functions in this module. + */ +export const closeFaceDBConnectionsIfNeeded = async () => { + try { + if (_faceDB) (await _faceDB).close(); + } finally { + _faceDB = undefined; + } +}; + +/** + * Clear any data stored by the face module. + * + * Meant to be called during logout. + */ +export const clearFaceData = () => + deleteDB("face", { + blocked() { + log.warn( + "Waiting for an existing client to close their connection so that we can delete the face DB", + ); + }, + }); + /** * Save the given {@link faceIndex} locally. * @@ -133,7 +162,7 @@ const faceDB = () => (_faceDB ??= openFaceDB()); * performed, the existing entry is unconditionally overwritten). */ // eslint-disable-next-line @typescript-eslint/no-unused-vars -export const saveFaceIndex = (faceIndex: FaceIndex) => {}; +export const saveFaceIndex = async (faceIndex: FaceIndex) => {}; /** * Record the existence of a file so that entities in the face indexing universe @@ -160,10 +189,3 @@ export const addFileEntry = (fileID: string) => {}; */ // eslint-disable-next-line @typescript-eslint/no-unused-vars export const markIndexingFailed = (fileID: string) => {}; - -/** - * Clear any data stored by the face module. - * - * Meant to be called during logout. - */ -export const clearFaceData = () => {}; From 9c60fe6f3fd258d385cdc0f3abee4e29086fed10 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 29 May 2024 12:22:58 +0530 Subject: [PATCH 14/21] logout --- web/apps/photos/src/services/face/db.ts | 10 ++++++---- web/apps/photos/src/services/logout.ts | 7 +++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/web/apps/photos/src/services/face/db.ts b/web/apps/photos/src/services/face/db.ts index adaf097cee..5c23bb19f6 100644 --- a/web/apps/photos/src/services/face/db.ts +++ b/web/apps/photos/src/services/face/db.ts @@ -79,8 +79,8 @@ interface FileStatus { * Note that this is module specific state, so the main thread and each worker * thread that calls the functions in this module will get their own independent * connection. To ensure that all connections get torn down correctly, we need - * to call closeFaceDBConnection - * used to delete the database in {@link clearFaceData}. + * to call {@link closeFaceDBConnectionsIfNeeded} from both the main thread and + * all the worker threads that use this module. */ let _faceDB: ReturnType | undefined; @@ -143,14 +143,16 @@ export const closeFaceDBConnectionsIfNeeded = async () => { * * Meant to be called during logout. */ -export const clearFaceData = () => - deleteDB("face", { +export const clearFaceData = async () => { + await closeFaceDBConnectionsIfNeeded(); + return deleteDB("face", { blocked() { log.warn( "Waiting for an existing client to close their connection so that we can delete the face DB", ); }, }); +}; /** * Save the given {@link faceIndex} locally. diff --git a/web/apps/photos/src/services/logout.ts b/web/apps/photos/src/services/logout.ts index a6b155c8c2..9b664bf579 100644 --- a/web/apps/photos/src/services/logout.ts +++ b/web/apps/photos/src/services/logout.ts @@ -3,6 +3,7 @@ import { accountLogout } from "@ente/accounts/services/logout"; import { clipService } from "services/clip-service"; import DownloadManager from "./download"; import exportService from "./export"; +import { clearFaceData } from "./face/db"; import mlWorkManager from "./machineLearning/mlWorkManager"; /** @@ -35,6 +36,12 @@ export const photosLogout = async () => { log.error("Ignoring error during logout (ML)", e); } + try { + await clearFaceData(); + } catch (e) { + log.error("Ignoring error during logout (face)", e); + } + try { exportService.disableContinuousExport(); } catch (e) { From f0f3af96d1bcf227cc55221ceff95d31da063d78 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 29 May 2024 12:25:28 +0530 Subject: [PATCH 15/21] dedup --- web/apps/photos/src/services/logout.ts | 15 +++++++++------ web/packages/accounts/services/logout.ts | 15 +++++++++------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/web/apps/photos/src/services/logout.ts b/web/apps/photos/src/services/logout.ts index 9b664bf579..33854f5578 100644 --- a/web/apps/photos/src/services/logout.ts +++ b/web/apps/photos/src/services/logout.ts @@ -14,18 +14,21 @@ import mlWorkManager from "./machineLearning/mlWorkManager"; * See: [Note: Do not throw during logout]. */ export const photosLogout = async () => { + const ignoreError = (e: unknown, label: string) => + log.error(`Ignoring error during logout (${label})`, e); + await accountLogout(); try { await DownloadManager.logout(); } catch (e) { - log.error("Ignoring error during logout (download)", e); + ignoreError("download", e); } try { await clipService.logout(); } catch (e) { - log.error("Ignoring error during logout (CLIP)", e); + ignoreError("CLIP", e); } const electron = globalThis.electron; @@ -33,25 +36,25 @@ export const photosLogout = async () => { try { await mlWorkManager.logout(); } catch (e) { - log.error("Ignoring error during logout (ML)", e); + ignoreError("ML", e); } try { await clearFaceData(); } catch (e) { - log.error("Ignoring error during logout (face)", e); + ignoreError("face", e); } try { exportService.disableContinuousExport(); } catch (e) { - log.error("Ignoring error during logout (export)", e); + ignoreError("export", e); } try { await electron?.logout(); } catch (e) { - log.error("Ignoring error during logout (electron)", e); + ignoreError("electron", e); } } }; diff --git a/web/packages/accounts/services/logout.ts b/web/packages/accounts/services/logout.ts index 1858ec7cc3..d70b1c36b3 100644 --- a/web/packages/accounts/services/logout.ts +++ b/web/packages/accounts/services/logout.ts @@ -17,34 +17,37 @@ import { logout as remoteLogout } from "../api/user"; * gets in an unexpected state. */ export const accountLogout = async () => { + const ignoreError = (e: unknown, label: string) => + log.error(`Ignoring error during logout (${label})`, e); + try { await remoteLogout(); } catch (e) { - log.error("Ignoring error during logout (remote)", e); + ignoreError("remote", e); } try { InMemoryStore.clear(); } catch (e) { - log.error("Ignoring error during logout (in-memory store)", e); + ignoreError("in-memory store", e); } try { clearKeys(); } catch (e) { - log.error("Ignoring error during logout (session store)", e); + ignoreError("session store", e); } try { clearData(); } catch (e) { - log.error("Ignoring error during logout (local storage)", e); + ignoreError("local storage", e); } try { await localForage.clear(); } catch (e) { - log.error("Ignoring error during logout (local forage)", e); + ignoreError("local forage", e); } try { await clearBlobCaches(); } catch (e) { - log.error("Ignoring error during logout (cache)", e); + ignoreError("cache", e); } }; From bb46e98e857e554e660aba379a0285740367be8f Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 29 May 2024 12:28:56 +0530 Subject: [PATCH 16/21] Desktop --- desktop/src/main/services/logout.ts | 9 ++++++--- web/apps/photos/src/services/logout.ts | 2 +- web/packages/accounts/services/logout.ts | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/desktop/src/main/services/logout.ts b/desktop/src/main/services/logout.ts index e6cb7666ca..37e73309a4 100644 --- a/desktop/src/main/services/logout.ts +++ b/desktop/src/main/services/logout.ts @@ -12,19 +12,22 @@ import { watchReset } from "./watch"; * See: [Note: Do not throw during logout]. */ export const logout = (watcher: FSWatcher) => { + const ignoreError = (label: string, e: unknown) => + log.error(`Ignoring error during logout (${label})`, e); + try { watchReset(watcher); } catch (e) { - log.error("Ignoring error during logout (FS watch)", e); + ignoreError("FS watch", e); } try { clearConvertToMP4Results(); } catch (e) { - log.error("Ignoring error during logout (convert-to-mp4)", e); + ignoreError("convert-to-mp4", e); } try { clearStores(); } catch (e) { - log.error("Ignoring error during logout (native stores)", e); + ignoreError("native stores", e); } }; diff --git a/web/apps/photos/src/services/logout.ts b/web/apps/photos/src/services/logout.ts index 33854f5578..4e09516dee 100644 --- a/web/apps/photos/src/services/logout.ts +++ b/web/apps/photos/src/services/logout.ts @@ -14,7 +14,7 @@ import mlWorkManager from "./machineLearning/mlWorkManager"; * See: [Note: Do not throw during logout]. */ export const photosLogout = async () => { - const ignoreError = (e: unknown, label: string) => + const ignoreError = (label: string, e: unknown) => log.error(`Ignoring error during logout (${label})`, e); await accountLogout(); diff --git a/web/packages/accounts/services/logout.ts b/web/packages/accounts/services/logout.ts index d70b1c36b3..7a150384db 100644 --- a/web/packages/accounts/services/logout.ts +++ b/web/packages/accounts/services/logout.ts @@ -17,7 +17,7 @@ import { logout as remoteLogout } from "../api/user"; * gets in an unexpected state. */ export const accountLogout = async () => { - const ignoreError = (e: unknown, label: string) => + const ignoreError = (label: string, e: unknown) => log.error(`Ignoring error during logout (${label})`, e); try { From 431cd3935886b05b0a841be080890e8f5adfe743 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 29 May 2024 13:00:02 +0530 Subject: [PATCH 17/21] Save --- web/apps/photos/src/services/face/db.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/web/apps/photos/src/services/face/db.ts b/web/apps/photos/src/services/face/db.ts index 5c23bb19f6..0dda336481 100644 --- a/web/apps/photos/src/services/face/db.ts +++ b/web/apps/photos/src/services/face/db.ts @@ -36,7 +36,7 @@ interface FaceDBSchema extends DBSchema { interface FileStatus { /** The ID of the {@link EnteFile} whose indexing status we represent. */ - fileID: string; + fileID: number; /** * `1` if we have indexed a file with this {@link fileID}, `0` otherwise. * @@ -163,8 +163,21 @@ export const clearFaceData = async () => { * This function adds a new entry, overwriting any existing ones (No merging is * performed, the existing entry is unconditionally overwritten). */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const saveFaceIndex = async (faceIndex: FaceIndex) => {}; +export const saveFaceIndex = async (faceIndex: FaceIndex) => { + const db = await faceDB(); + const tx = db.transaction(["face-index", "file-status"], "readwrite"); + const indexStore = tx.objectStore("face-index"); + const statusStore = tx.objectStore("file-status"); + return Promise.all([ + indexStore.put(faceIndex), + statusStore.put({ + fileID: faceIndex.fileID, + isIndexed: 1, + failureCount: 0 + }), + tx.done + ]) +}; /** * Record the existence of a file so that entities in the face indexing universe From 34d4aeaf56aa1ef60b670060dc44279eed3bc777 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 29 May 2024 13:04:09 +0530 Subject: [PATCH 18/21] file entry --- web/apps/photos/src/services/face/db.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/web/apps/photos/src/services/face/db.ts b/web/apps/photos/src/services/face/db.ts index 0dda336481..d3d83902cf 100644 --- a/web/apps/photos/src/services/face/db.ts +++ b/web/apps/photos/src/services/face/db.ts @@ -24,11 +24,11 @@ import type { FaceIndex } from "./types"; */ interface FaceDBSchema extends DBSchema { "face-index": { - key: string; + key: number; value: FaceIndex; }; "file-status": { - key: string; + key: number; value: FileStatus; indexes: { isIndexed: number }; }; @@ -173,10 +173,10 @@ export const saveFaceIndex = async (faceIndex: FaceIndex) => { statusStore.put({ fileID: faceIndex.fileID, isIndexed: 1, - failureCount: 0 + failureCount: 0, }), - tx.done - ]) + tx.done, + ]); }; /** @@ -190,8 +190,18 @@ export const saveFaceIndex = async (faceIndex: FaceIndex) => { * {@link saveFaceIndex} called with the result), its existing status remains * unperturbed. */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const addFileEntry = (fileID: string) => {}; +export const addFileEntry = async (fileID: number) => { + const db = await faceDB(); + const tx = db.transaction("file-status", "readwrite"); + if ((await tx.store.getKey(fileID)) === undefined) { + await tx.store.put({ + fileID, + isIndexed: 0, + failureCount: 0, + }); + } + return tx.done; +}; /** * Increment the failure count associated with the given {@link fileID}. From 8dd0d58319043173b14038eae4e2242d0b03d05f Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 29 May 2024 13:06:52 +0530 Subject: [PATCH 19/21] tick --- web/apps/photos/src/services/face/db.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/web/apps/photos/src/services/face/db.ts b/web/apps/photos/src/services/face/db.ts index d3d83902cf..56ab5d8368 100644 --- a/web/apps/photos/src/services/face/db.ts +++ b/web/apps/photos/src/services/face/db.ts @@ -212,5 +212,15 @@ export const addFileEntry = async (fileID: number) => { * and its failure count is set to 1. Otherwise the failure count of the * existing entry is incremented. */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export const markIndexingFailed = (fileID: string) => {}; +export const markIndexingFailed = async (fileID: number) => { + const db = await faceDB(); + const tx = db.transaction("file-status", "readwrite"); + const status = (await tx.store.get(fileID)) ?? { + fileID, + isIndexed: 0, + failureCount: 0, + }; + status.failureCount = status.failureCount + 1; + await tx.store.put(status); + return tx.done; +}; From cee093c214dc2eaa36c2e7991da8fd2ee01e5cb7 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 29 May 2024 13:22:40 +0530 Subject: [PATCH 20/21] query --- web/apps/photos/src/services/face/db.ts | 60 ++++++++++++++----------- 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/web/apps/photos/src/services/face/db.ts b/web/apps/photos/src/services/face/db.ts index 56ab5d8368..fa4f8dd63f 100644 --- a/web/apps/photos/src/services/face/db.ts +++ b/web/apps/photos/src/services/face/db.ts @@ -30,7 +30,7 @@ interface FaceDBSchema extends DBSchema { "file-status": { key: number; value: FileStatus; - indexes: { isIndexed: number }; + indexes: { isIndexable: number }; }; } @@ -38,15 +38,11 @@ interface FileStatus { /** The ID of the {@link EnteFile} whose indexing status we represent. */ fileID: number; /** - * `1` if we have indexed a file with this {@link fileID}, `0` otherwise. - * - * It is guaranteed that "face-index" will have an entry for the same - * {@link fileID} if and only if {@link isIndexed} is `1`. + * `1` if this file needs to be indexed, `0` otherwise. * * > Somewhat confusingly, we also have a (IndexedDB) "index" on this field. - * That (IDB) index allows us to effectively select {@link fileIDs} that - * still need indexing (where {@link isIndexed} is not `1`), so it is all - * sensible, just that if I say the word "index" one more time... + * That (IDB) index allows us to efficiently select {@link fileIDs} that + * still need indexing (i.e. entries where {@link isIndexed} is `1`). * * [Note: Boolean IndexedDB indexes]. * @@ -56,12 +52,13 @@ interface FileStatus { * As a workaround, we use numeric fields where `0` denotes `false` and `1` * denotes `true`. */ - isIndexed: number; + isIndexable: number; /** * The number of times attempts to index this file failed. * * This is guaranteed to be `0` for files which have already been - * sucessfully indexed (i.e. files for which `isIndexed` is true). + * sucessfully indexed (i.e. files for which `isIndexable` is 0 and which + * have a corresponding entry in the "face-index" object store). */ failureCount: number; } @@ -77,10 +74,10 @@ interface FileStatus { * version of the schema). * * Note that this is module specific state, so the main thread and each worker - * thread that calls the functions in this module will get their own independent - * connection. To ensure that all connections get torn down correctly, we need - * to call {@link closeFaceDBConnectionsIfNeeded} from both the main thread and - * all the worker threads that use this module. + * thread that calls the functions in this module will have their own promises. + * To ensure that all connections get torn down correctly, we need to call + * {@link closeFaceDBConnectionsIfNeeded} from both the main thread and all the + * worker threads that use this module. */ let _faceDB: ReturnType | undefined; @@ -90,11 +87,9 @@ const openFaceDB = async () => { log.info(`Upgrading face DB ${oldVersion} => ${newVersion}`); if (oldVersion < 1) { db.createObjectStore("face-index", { keyPath: "fileID" }); - - const statusStore = db.createObjectStore("file-status", { + db.createObjectStore("file-status", { keyPath: "fileID", - }); - statusStore.createIndex("isIndexed", "isIndexed"); + }).createIndex("isIndexable", "isIndexable"); } }, blocking() { @@ -172,7 +167,7 @@ export const saveFaceIndex = async (faceIndex: FaceIndex) => { indexStore.put(faceIndex), statusStore.put({ fileID: faceIndex.fileID, - isIndexed: 1, + isIndexable: 0, failureCount: 0, }), tx.done, @@ -196,13 +191,27 @@ export const addFileEntry = async (fileID: number) => { if ((await tx.store.getKey(fileID)) === undefined) { await tx.store.put({ fileID, - isIndexed: 0, + isIndexable: 1, failureCount: 0, }); } return tx.done; }; +/** + * Return a list of fileIDs that need to be indexed. + * + * This list is from the universe of the file IDs that the face DB knows about + * (can use {@link addFileEntry} to inform it about new files). From this + * universe, we filter out fileIDs the files corresponding to which have already + * been indexed, or for which we attempted indexing but failed. + */ +export const unindexedFileIDs = async () => { + const db = await faceDB(); + const tx = db.transaction("file-status", "readonly"); + return tx.store.index("isIndexable").getAllKeys(IDBKeyRange.only(1)); +}; + /** * Increment the failure count associated with the given {@link fileID}. * @@ -215,12 +224,11 @@ export const addFileEntry = async (fileID: number) => { export const markIndexingFailed = async (fileID: number) => { const db = await faceDB(); const tx = db.transaction("file-status", "readwrite"); - const status = (await tx.store.get(fileID)) ?? { + const failureCount = ((await tx.store.get(fileID)).failureCount ?? 0) + 1; + await tx.store.put({ fileID, - isIndexed: 0, - failureCount: 0, - }; - status.failureCount = status.failureCount + 1; - await tx.store.put(status); + isIndexable: 0, + failureCount, + }); return tx.done; }; From b3a0bc624bf8b6cfb4457f5df58957060c533258 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 29 May 2024 13:25:41 +0530 Subject: [PATCH 21/21] lf --- web/apps/photos/src/services/face/crop.ts | 3 ++- web/apps/photos/src/services/face/db.ts | 1 - web/apps/photos/src/services/face/remote.ts | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/web/apps/photos/src/services/face/crop.ts b/web/apps/photos/src/services/face/crop.ts index d4d3753825..faf7f0ac9b 100644 --- a/web/apps/photos/src/services/face/crop.ts +++ b/web/apps/photos/src/services/face/crop.ts @@ -1,5 +1,6 @@ import { blobCache } from "@/next/blob-cache"; -import type { Box, Face, FaceAlignment } from "./types-old"; +import type { Box } from "./types"; +import type { Face, FaceAlignment } from "./types-old"; export const saveFaceCrop = async (imageBitmap: ImageBitmap, face: Face) => { const faceCrop = extractFaceCrop(imageBitmap, face.alignment); diff --git a/web/apps/photos/src/services/face/db.ts b/web/apps/photos/src/services/face/db.ts index fa4f8dd63f..1128395237 100644 --- a/web/apps/photos/src/services/face/db.ts +++ b/web/apps/photos/src/services/face/db.ts @@ -115,7 +115,6 @@ const openFaceDB = async () => { /** * @returns a lazily created, cached connection to the face DB. */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars const faceDB = () => (_faceDB ??= openFaceDB()); /** diff --git a/web/apps/photos/src/services/face/remote.ts b/web/apps/photos/src/services/face/remote.ts index 32d0fddad8..36ef724bf3 100644 --- a/web/apps/photos/src/services/face/remote.ts +++ b/web/apps/photos/src/services/face/remote.ts @@ -2,7 +2,8 @@ import log from "@/next/log"; import ComlinkCryptoWorker from "@ente/shared/crypto"; import { putEmbedding } from "services/embeddingService"; import type { EnteFile } from "types/file"; -import type { Face, FaceDetection, MlFileData, Point } from "./types-old"; +import type { Point } from "./types"; +import type { Face, FaceDetection, MlFileData } from "./types-old"; export const putFaceEmbedding = async ( enteFile: EnteFile,