diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index aab41ce0df..64a9c7d3e0 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -17,7 +17,7 @@ import { import { wipHasSwitchedOnceCmpAndSet } from "@/new/photos/services/ml"; import { filterSearchableFiles, - setSearchableCollectionsAndFiles, + setSearchCollectionsAndFiles, } from "@/new/photos/services/search"; import type { SearchOption } from "@/new/photos/services/search/types"; import { EnteFile } from "@/new/photos/types/file"; @@ -411,7 +411,7 @@ export default function Gallery() { useEffect( () => - setSearchableCollectionsAndFiles({ + setSearchCollectionsAndFiles({ collections: collections ?? [], files: getUniqueFiles(files ?? []), }), diff --git a/web/packages/new/photos/components/PeopleList.tsx b/web/packages/new/photos/components/PeopleList.tsx index 1b477c4497..bc660578f1 100644 --- a/web/packages/new/photos/components/PeopleList.tsx +++ b/web/packages/new/photos/components/PeopleList.tsx @@ -1,6 +1,6 @@ import { useIsMobileWidth } from "@/base/hooks"; -import type { Person } from "@/new/photos/services/ml"; import { faceCrop, unidentifiedFaceIDs } from "@/new/photos/services/ml"; +import type { Person } from "@/new/photos/services/ml/cgroups"; import type { EnteFile } from "@/new/photos/types/file"; import { Skeleton, Typography, styled } from "@mui/material"; import { t } from "i18next"; diff --git a/web/packages/new/photos/services/ml/cgroups.ts b/web/packages/new/photos/services/ml/cgroups.ts index 982fd655e1..d6c3e0fbaa 100644 --- a/web/packages/new/photos/services/ml/cgroups.ts +++ b/web/packages/new/photos/services/ml/cgroups.ts @@ -2,7 +2,6 @@ import { masterKeyFromSession } from "@/base/session-store"; import { fileIDFromFaceID, wipClusterEnable } from "."; import type { EnteFile } from "../../types/file"; import { getLocalFiles } from "../files"; -import { setCGroups } from "../search"; import { pullCGroups } from "../user-entity"; import type { FaceCluster } from "./cluster"; import { getClusterGroups, getFaceIndexes } from "./db"; @@ -78,6 +77,47 @@ export interface CGroup { avatarFaceID: string | undefined; } +/** + * A massaged version of {@link CGroup} suitable for being shown in the UI. + * + * The cgroups synced with remote do not directly correspond to "people". + * CGroups represent both positive and negative feedback, where the negations + * are specifically feedback meant so that we do not show the corresponding + * cluster in the UI. + * + * So while each person has an underlying cgroups, not all cgroups have a + * corresponding person. + * + * Beyond this semantic difference, there is also data massaging: a + * {@link Person} has data converted into a format that the UI can directly and + * efficiently use, as compared to a {@link CGroup}, which is tailored for + * transmission and storage. + */ +export interface Person { + /** + * Nanoid of the underlying {@link CGroup}. + */ + id: string; + /** + * The name of the person. + */ + name: string; + /** + * IDs of the (unique) files in which this face occurs. + */ + fileIDs: number[]; + /** + * The face that should be used as the "cover" face to represent this + * {@link Person} in the UI. + */ + displayFaceID: string; + /** + * The {@link EnteFile} which contains the display face. + */ + displayFaceFile: EnteFile; +} + +// TODO-Cluster remove me /** * A {@link CGroup} annotated with various in-memory state to make it easier for * the upper layers of our code to directly use it. @@ -110,10 +150,13 @@ export const syncCGroups = async () => { * This function is meant to run after files, cgroups and faces have been synced * with remote. It then uses all the information in the local DBs to construct * an in-memory list of {@link Person}s on which the UI will operate. + * + * @return A list of {@link Person}s, sorted by the number of files that they + * reference. */ -export const updatePeople = async () => { - if (!process.env.NEXT_PUBLIC_ENTE_WIP_CL) return; - if (!(await wipClusterEnable())) return; +export const updatedPeople = async () => { + if (!process.env.NEXT_PUBLIC_ENTE_WIP_CL) return []; + if (!(await wipClusterEnable())) return []; // Ignore faces belonging to deleted (incl Trash) and hidden files. // @@ -144,7 +187,7 @@ export const updatePeople = async () => { // Convert cgroups to people. const cgroups = await getClusterGroups(); - const people = cgroups + return cgroups .map((cgroup) => { // Hidden cgroups are clusters specifically marked so as to not be shown // in the UI. @@ -196,15 +239,6 @@ export const updatePeople = async () => { return { id, name, fileIDs, displayFaceID, displayFaceFile }; }) - .filter((c) => !!c); - - // Read all the latest cluster groups, locally present files, local faces - - // Re-read the cgroups across which we should search from the local ML DB. - // - // This DB read can happen in the search worker too, but that would cause - // another handle to the DB to be opened (from the search worker's context). - // Making the DB call here avoids that (not that we noticed any issues, but - // preemptively trying not to poke IndexedDB too much lest it be flaky). - await setCGroups(); + .filter((c) => !!c) + .sort((a, b) => b.fileIDs.length - a.fileIDs.length); }; diff --git a/web/packages/new/photos/services/ml/index.ts b/web/packages/new/photos/services/ml/index.ts index 7427394a3d..fbb22f7d50 100644 --- a/web/packages/new/photos/services/ml/index.ts +++ b/web/packages/new/photos/services/ml/index.ts @@ -596,46 +596,6 @@ const setInterimScheduledStatus = () => { const workerDidProcessFileOrIdle = throttled(updateMLStatusSnapshot, 2000); -/** - * A massaged version of {@link CGroup} suitable for being shown in the UI. - * - * The cgroups synced with remote do not directly correspond to "people". - * CGroups represent both positive and negative feedback, where the negations - * are specifically feedback meant so that we do not show the corresponding - * cluster in the UI. - * - * So while each person has an underlying cgroups, not all cgroups have a - * corresponding person. - * - * Beyond this semantic difference, there is also data massaging: a - * {@link Person} has data converted into a format that the UI can directly and - * efficiently use, as compared to a {@link CGroup}, which is tailored for - * transmission and storage. - */ -export interface Person { - /** - * Nanoid of the underlying {@link CGroup}. - */ - id: string; - /** - * The name of the person. - */ - name: string; - /** - * IDs of the (unique) files in which this face occurs. - */ - fileIDs: number[]; - /** - * The face that should be used as the "cover" face to represent this - * {@link Person} in the UI. - */ - displayFaceID: string; - /** - * The {@link EnteFile} which contains the display face. - */ - displayFaceFile: EnteFile; -} - /** * A function that can be used to subscribe to updates to {@link Person}s. * diff --git a/web/packages/new/photos/services/search/index.ts b/web/packages/new/photos/services/search/index.ts index df7f67fe30..0d3273484e 100644 --- a/web/packages/new/photos/services/search/index.ts +++ b/web/packages/new/photos/services/search/index.ts @@ -4,12 +4,12 @@ import { ComlinkWorker } from "@/base/worker/comlink-worker"; import { FileType } from "@/media/file-type"; import i18n, { t } from "i18next"; import { clipMatches, isMLEnabled, isMLSupported } from "../ml"; -import type { CGroup } from "../ml/cgroups"; +import type { Person } from "../ml/cgroups"; import type { LabelledFileType, LabelledSearchDateComponents, LocalizedSearchData, - SearchableCollectionsAndFiles, + SearchCollectionsAndFiles, SearchSuggestion, } from "./types"; import type { SearchWorker } from "./worker"; @@ -54,15 +54,14 @@ export const searchDataSync = () => /** * Set the collections and files over which we should search. */ -export const setSearchableCollectionsAndFiles = ( - data: SearchableCollectionsAndFiles, -) => void worker().then((w) => w.setSearchableCollectionsAndFiles(data)); +export const setSearchCollectionsAndFiles = (cf: SearchCollectionsAndFiles) => + void worker().then((w) => w.setCollectionsAndFiles(cf)); /** - * Set the cgroups that we should search across. + * Set the people that we should search across. */ -export const setCGroups = (data: CGroup[]) => - worker().then((w) => w.setCGroups(data)); +export const setPeople = (people: Person[]) => + void worker().then((w) => w.setPeople(people)); /** * Convert a search string into (annotated) suggestions that can be shown in the @@ -121,7 +120,7 @@ const suggestionsToOptions = (suggestions: SearchSuggestion[]) => /** * Return the list of {@link EnteFile}s (from amongst the previously set - * {@link SearchableCollectionsAndFiles}) that match the given search {@link suggestion}. + * {@link SearchCollectionsAndFiles}) that match the given search {@link suggestion}. */ export const filterSearchableFiles = async (suggestion: SearchSuggestion) => worker().then((w) => w.filterSearchableFiles(suggestion)); diff --git a/web/packages/new/photos/services/search/types.ts b/web/packages/new/photos/services/search/types.ts index 3a621a39a0..32639ee07a 100644 --- a/web/packages/new/photos/services/search/types.ts +++ b/web/packages/new/photos/services/search/types.ts @@ -6,7 +6,7 @@ import type { Location } from "@/base/types"; import type { Collection } from "@/media/collection"; import { FileType } from "@/media/file-type"; -import type { Person } from "@/new/photos/services/ml"; +import type { Person } from "@/new/photos/services/ml/cgroups"; import type { EnteFile } from "@/new/photos/types/file"; import type { LocationTag } from "../user-entity"; @@ -45,9 +45,9 @@ export interface SearchOption { } /** - * The collections and files which we should search. + * The collections and files over which we should search. */ -export interface SearchableCollectionsAndFiles { +export interface SearchCollectionsAndFiles { collections: Collection[]; files: EnteFile[]; } diff --git a/web/packages/new/photos/services/search/worker.ts b/web/packages/new/photos/services/search/worker.ts index de8ae76ca5..b7a24a50c5 100644 --- a/web/packages/new/photos/services/search/worker.ts +++ b/web/packages/new/photos/services/search/worker.ts @@ -1,9 +1,8 @@ import { HTTPError } from "@/base/http"; -import log from "@/base/log"; import type { Location } from "@/base/types"; import type { Collection } from "@/media/collection"; import { fileCreationPhotoDate, fileLocation } from "@/media/file-metadata"; -import type { CGroup } from "@/new/photos/services/ml/cgroups"; +import type { Person } from "@/new/photos/services/ml/cgroups"; import type { EnteFile } from "@/new/photos/types/file"; import { ensure } from "@/utils/ensure"; import { nullToUndefined } from "@/utils/transform"; @@ -23,13 +22,11 @@ import type { LabelledSearchDateComponents, LocalizedSearchData, Searchable, - SearchableCollectionsAndFiles, + SearchCollectionsAndFiles, SearchDateComponents, SearchSuggestion, } from "./types"; -type SearchableCGroup = Searchable & { name: string }>; - /** * A web worker that runs the search asynchronously so that the main thread * remains responsive. @@ -37,11 +34,11 @@ type SearchableCGroup = Searchable & { name: string }>; export class SearchWorker { private locationTags: Searchable[] = []; private cities: Searchable[] = []; - private searchableCollectionsAndFiles: SearchableCollectionsAndFiles = { + private collectionsAndFiles: SearchCollectionsAndFiles = { collections: [], files: [], }; - private searchableCGroups: SearchableCGroup[] = []; + private searchablePeople: Searchable[] = []; /** * Fetch any state we might need when the actual search happens. @@ -71,23 +68,17 @@ export class SearchWorker { /** * Set the collections and files that we should search across. */ - setSearchableCollectionsAndFiles(data: SearchableCollectionsAndFiles) { - this.searchableCollectionsAndFiles = data; + setCollectionsAndFiles(cf: SearchCollectionsAndFiles) { + this.collectionsAndFiles = cf; } /** - * Set the cgroups that we should search across. + * Set the people that we should search across. */ - setCGroups(cgroups: CGroup[]) { - this.searchableCGroups = cgroups - .map((cgroup) => { - const name = cgroup.name; - if (!name) return undefined; - if (cgroup.isHidden) return undefined; - return { ...cgroup, name, lowercasedName: name.toLowerCase() }; - }) - .filter((c) => !!c); - log.debug(() => ["searchableCGroups", this.searchableCGroups]); + setPeople(people: Person[]) { + this.searchablePeople = people.map((person) => { + return { ...person, lowercasedName: person.name.toLowerCase() }; + }); } /** @@ -101,8 +92,8 @@ export class SearchWorker { return suggestionsForString( s, searchString, - this.searchableCollectionsAndFiles, - this.searchableCGroups, + this.collectionsAndFiles, + this.searchablePeople, localizedSearchData, this.locationTags, this.cities, @@ -114,7 +105,7 @@ export class SearchWorker { */ filterSearchableFiles(suggestion: SearchSuggestion) { return filterSearchableFiles( - this.searchableCollectionsAndFiles.files, + this.collectionsAndFiles.files, suggestion, ); } @@ -123,7 +114,7 @@ export class SearchWorker { * Batched variant of {@link filterSearchableFiles}. */ filterSearchableFilesMulti(suggestions: SearchSuggestion[]) { - const files = this.searchableCollectionsAndFiles.files; + const files = this.collectionsAndFiles.files; return suggestions .map((sg) => [filterSearchableFiles(files, sg), sg] as const) .filter(([files]) => files.length); @@ -139,15 +130,15 @@ expose(SearchWorker); const suggestionsForString = ( s: string, searchString: string, - { collections, files }: SearchableCollectionsAndFiles, - searchableCGroups: SearchableCGroup[], + { collections, files }: SearchCollectionsAndFiles, + searchablePeople: Searchable[], { locale, holidays, labelledFileTypes }: LocalizedSearchData, locationTags: Searchable[], cities: Searchable[], ): SearchSuggestion[] => [ // . <-- clip suggestions will be inserted here by our caller. - peopleSuggestions(s, searchableCGroups), + peopleSuggestions(s, searchablePeople), fileTypeSuggestions(s, labelledFileTypes), dateSuggestions(s, locale, holidays), locationSuggestions(s, locationTags, cities), @@ -216,21 +207,11 @@ const fileCaptionSuggestion = ( const peopleSuggestions = ( s: string, - searchableCGroups: SearchableCGroup[], -): SearchSuggestion[] => { - // Suppress the unused warning during WIP - if (process.env.NEXT_PUBLIC_ENTE_WIP_CL) { - console.log({ s, searchableCGroups }); - } - return []; - // searchableCGroups - // .filter(({ lowercasedName }) => lowercasedName.startsWith(s)) - // .map((scgroup) => ({ - // type: "person", - // person: scgroup, - // label: scgroup.name, - // })); -}; + searchablePeople: Searchable[], +): SearchSuggestion[] => + searchablePeople + .filter(({ lowercasedName }) => lowercasedName.startsWith(s)) + .map((person) => ({ type: "person", person, label: person.name })); const dateSuggestions = ( s: string,