diff --git a/web/apps/photos/src/pages/cluster-debug.tsx b/web/apps/photos/src/pages/cluster-debug.tsx new file mode 100644 index 0000000000..d71b300647 --- /dev/null +++ b/web/apps/photos/src/pages/cluster-debug.tsx @@ -0,0 +1,254 @@ +import { SelectionBar } from "@/base/components/Navbar"; +import { pt } from "@/base/i18n"; +import log from "@/base/log"; +import { wipClusterPageContents } from "@/new/photos/services/ml"; +import { EnteFile } from "@/new/photos/types/file"; +import { + FluidContainer, + VerticallyCentered, +} from "@ente/shared/components/Container"; +import EnteSpinner from "@ente/shared/components/EnteSpinner"; +import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages"; +import { CustomError } from "@ente/shared/error"; +import useMemoSingleThreaded from "@ente/shared/hooks/useMemoSingleThreaded"; +import BackButton from "@mui/icons-material/ArrowBackOutlined"; +import { Box, IconButton, styled } from "@mui/material"; +import Typography from "@mui/material/Typography"; +import { DedupePhotoList } from "components/PhotoList/dedupe"; +import PreviewCard from "components/pages/gallery/PreviewCard"; +import { ALL_SECTION } from "constants/collection"; +import { t } from "i18next"; +import { useRouter } from "next/router"; +import { AppContext } from "pages/_app"; +import { createContext, useContext, useEffect, useState } from "react"; +import AutoSizer from "react-virtualized-auto-sizer"; +import { getLocalCollections } from "services/collectionService"; +import { Duplicate } from "services/deduplicationService"; +import { + DeduplicateContextType, + DefaultDeduplicateContext, +} from "types/deduplicate"; +import { updateFileMsrcProps } from "utils/photoFrame"; + +const DeduplicateContext = createContext( + DefaultDeduplicateContext, +); + +const Info = styled("div")` + padding: 24px; + font-size: 18px; +`; + +// TODO-Cluster Temporary component for debugging +export default function Deduplicate() { + const { startLoading, finishLoading, showNavBar } = useContext(AppContext); + const [duplicates, setDuplicates] = useState(null); + const [collectionNameMap, setCollectionNameMap] = useState( + new Map(), + ); + + useEffect(() => { + showNavBar(true); + }, []); + + useEffect(() => { + syncWithRemote(); + }, []); + + const syncWithRemote = async () => { + startLoading(); + const collections = await getLocalCollections(); + const collectionNameMap = new Map(); + for (const collection of collections) { + collectionNameMap.set(collection.id, collection.name); + } + setCollectionNameMap(collectionNameMap); + const faceAndFiles = await wipClusterPageContents(); + // const files = await getLocalFiles(); + // const duplicateFiles = await getDuplicates(files, collectionNameMap); + const duplicateFiles = faceAndFiles.map(({ face, file }) => ({ + files: [file], + size: face.score, + })); + const currFileSizeMap = new Map(); + let toSelectFileIDs: number[] = []; + let count = 0; + for (const dupe of duplicateFiles) { + // select all except first file + toSelectFileIDs = [ + ...toSelectFileIDs, + ...dupe.files.slice(1).map((f) => f.id), + ]; + count += dupe.files.length - 1; + + for (const file of dupe.files) { + currFileSizeMap.set(file.id, dupe.size); + } + } + setDuplicates(duplicateFiles); + const selectedFiles = { + count: count, + ownCount: count, + collectionID: ALL_SECTION, + }; + for (const fileID of toSelectFileIDs) { + selectedFiles[fileID] = true; + } + + finishLoading(); + }; + + const duplicateFiles = useMemoSingleThreaded(() => { + return (duplicates ?? []).reduce((acc, dupe) => { + return [...acc, ...dupe.files]; + }, []); + }, [duplicates]); + + if (!duplicates) { + return ( + + + + ); + } + + return ( + + {duplicateFiles.length > 0 && ( + {t("DEDUPLICATE_BASED_ON_SIZE")} + )} + {duplicateFiles.length === 0 ? ( + + + {t("NO_DUPLICATES_FOUND")} + + + ) : ( + + )} + + + ); +} + +const Options: React.FC = () => { + const router = useRouter(); + + const close = () => { + router.push(PAGES.GALLERY); + }; + + return ( + + + + + + {pt("Faces")} + + + ); +}; + +interface ClusterDebugPhotoFrameProps { + files: EnteFile[]; + duplicates?: Duplicate[]; + activeCollectionID: number; +} + +const ClusterDebugPhotoFrame: React.FC = ({ + duplicates, + files, + activeCollectionID, +}) => { + const displayFiles = useMemoSingleThreaded(() => { + return files.map((item) => { + const filteredItem = { + ...item, + w: window.innerWidth, + h: window.innerHeight, + title: item.pubMagicMetadata?.data.caption, + }; + return filteredItem; + }); + }, [files]); + + const updateURL = + (index: number) => (id: number, url: string, forceUpdate?: boolean) => { + const file = displayFiles[index]; + // this is to prevent outdated updateURL call from updating the wrong file + if (file.id !== id) { + log.info( + `[${id}]PhotoSwipe: updateURL: file id mismatch: ${file.id} !== ${id}`, + ); + throw Error(CustomError.UPDATE_URL_FILE_ID_MISMATCH); + } + if (file.msrc && !forceUpdate) { + throw Error(CustomError.URL_ALREADY_SET); + } + updateFileMsrcProps(file, url); + }; + + const getThumbnail = ( + item: EnteFile, + index: number, + isScrolling: boolean, + ) => ( + {}} + selectable={false} + onSelect={() => {}} + selected={false} + selectOnClick={false} + onHover={() => {}} + onRangeSelect={() => {}} + isRangeSelectActive={false} + isInsSelectRange={false} + activeCollectionID={activeCollectionID} + showPlaceholder={isScrolling} + /> + ); + + return ( + + + {({ height, width }) => ( + + )} + + + ); +}; + +const Container = styled("div")` + display: block; + flex: 1; + width: 100%; + flex-wrap: wrap; + margin: 0 auto; + overflow: hidden; + .pswp-thumbnail { + display: inline-block; + cursor: pointer; + } +`; diff --git a/web/packages/new/photos/components/MLSettings.tsx b/web/packages/new/photos/components/MLSettings.tsx index 43dc1231e3..fb3e6ea008 100644 --- a/web/packages/new/photos/components/MLSettings.tsx +++ b/web/packages/new/photos/components/MLSettings.tsx @@ -1,5 +1,5 @@ import { EnteDrawer } from "@/base/components/EnteDrawer"; -import { MenuItemGroup, MenuSectionTitle } from "@/base/components/Menu"; +import { MenuItemGroup } from "@/base/components/Menu"; import { Titlebar } from "@/base/components/Titlebar"; import { pt, ut } from "@/base/i18n"; import log from "@/base/log"; @@ -8,7 +8,6 @@ import { enableML, mlStatusSnapshot, mlStatusSubscribe, - wipCluster, wipClusterEnable, type MLStatus, } from "@/new/photos/services/ml"; @@ -28,6 +27,7 @@ import { type DialogProps, } from "@mui/material"; import { t } from "i18next"; +import { useRouter } from "next/router"; import React, { useEffect, useState, useSyncExternalStore } from "react"; import { Trans } from "react-i18next"; import type { NewAppContextPhotos } from "../types/context"; @@ -338,7 +338,10 @@ const ManageML: React.FC = ({ }); }; - const wipClusterNow = () => void wipCluster(); + // TODO-Cluster + // const wipClusterNow = () => void wipCluster(); + const router = useRouter(); + const wipClusterNow = () => router.push("/cluster-debug"); return ( @@ -387,15 +390,17 @@ const ManageML: React.FC = ({ - + /> */} )} diff --git a/web/packages/new/photos/services/ml/cluster-new.ts b/web/packages/new/photos/services/ml/cluster-new.ts index ecd520c436..3c6b4e01e3 100644 --- a/web/packages/new/photos/services/ml/cluster-new.ts +++ b/web/packages/new/photos/services/ml/cluster-new.ts @@ -287,7 +287,7 @@ export const clusterFaces = async (faceIndexes: FaceIndex[]) => { `Clustered ${faces.length} faces into ${validClusters.length} clusters (${Date.now() - t} ms)`, ); - return { clusters: validClusters, cgroups }; + return { faces, clusters: validClusters, cgroups }; }; /** diff --git a/web/packages/new/photos/services/ml/index.ts b/web/packages/new/photos/services/ml/index.ts index 846b48c62f..aeb10c5d35 100644 --- a/web/packages/new/photos/services/ml/index.ts +++ b/web/packages/new/photos/services/ml/index.ts @@ -28,6 +28,7 @@ import { faceIndexes, indexableAndIndexedCounts, } from "./db"; +import type { Face } from "./face"; import { MLWorker } from "./worker"; import type { CLIPMatches } from "./worker-types"; @@ -342,6 +343,38 @@ export const wipSearchPersons = async () => { return _wip_searchPersons ?? []; }; +export const wipClusterPageContents = async () => { + if (!(await wipClusterEnable())) return []; + + log.info("clustering"); + _wip_isClustering = true; + _wip_searchPersons = undefined; + triggerStatusUpdate(); + + const { faces } = await clusterFaces(await faceIndexes()); + // const searchPersons = await convertToSearchPersons(clusters, cgroups); + + const localFiles = await getAllLocalFiles(); + const localFileByID = new Map(localFiles.map((f) => [f.id, f])); + + const result1: { file: EnteFile; face: Face }[] = []; + for (const face of faces) { + const file = ensure( + localFileByID.get(ensure(fileIDFromFaceID(face.faceID))), + ); + result1.push({ file, face }); + } + + const result = result1.sort((a, b) => b.face.score - a.face.score); + + _wip_isClustering = false; + // _wip_searchPersons = searchPersons; + triggerStatusUpdate(); + + // return { faces, clusters, cgroups }; + return result; +}; + export const wipCluster = async () => { if (!(await wipClusterEnable())) return;