Remove circular dependency between ml/index and ml/people
This commit is contained in:
@@ -13,16 +13,17 @@ import { useIsSmallWidth } from "@/base/hooks";
|
||||
import { pt } from "@/base/i18n";
|
||||
import log from "@/base/log";
|
||||
import {
|
||||
applyPersonSuggestionUpdates,
|
||||
deleteCGroup,
|
||||
renameCGroup,
|
||||
suggestionsAndChoicesForPerson,
|
||||
} from "@/new/photos/services/ml";
|
||||
import {
|
||||
updateChoices,
|
||||
type CGroupPerson,
|
||||
type ClusterPerson,
|
||||
type Person,
|
||||
type PersonSuggestionsAndChoices,
|
||||
type PersonSuggestionUpdates,
|
||||
type PreviewableCluster,
|
||||
} from "@/new/photos/services/ml/people";
|
||||
import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
|
||||
@@ -290,7 +291,7 @@ interface SuggestionsDialogState {
|
||||
* - saved choice for which the user has changed their mind.
|
||||
* - suggestion that the user has either explicitly assigned or rejected.
|
||||
*/
|
||||
marks: Map<string, boolean | undefined>;
|
||||
updates: PersonSuggestionUpdates;
|
||||
}
|
||||
|
||||
type SCItem = PreviewableCluster & { fixed?: boolean; assigned?: boolean };
|
||||
@@ -303,7 +304,7 @@ type SuggestionsDialogAction =
|
||||
personID: string;
|
||||
suggestionsAndChoices: PersonSuggestionsAndChoices;
|
||||
}
|
||||
| { type: "mark"; item: SCItem; value: boolean | undefined }
|
||||
| { type: "updateItem"; item: SCItem; value: boolean | undefined }
|
||||
| { type: "save" }
|
||||
| { type: "toggleHistory" }
|
||||
| { type: "close" };
|
||||
@@ -315,7 +316,7 @@ const initialSuggestionsDialogState: SuggestionsDialogState = {
|
||||
showChoices: false,
|
||||
choices: [],
|
||||
suggestions: [],
|
||||
marks: new Map(),
|
||||
updates: new Map(),
|
||||
};
|
||||
|
||||
const suggestionsDialogReducer = (
|
||||
@@ -328,7 +329,7 @@ const suggestionsDialogReducer = (
|
||||
...initialSuggestionsDialogState,
|
||||
choices: [],
|
||||
suggestions: [],
|
||||
marks: new Map(),
|
||||
updates: new Map(),
|
||||
activity: "fetching",
|
||||
personID: action.personID,
|
||||
};
|
||||
@@ -343,21 +344,21 @@ const suggestionsDialogReducer = (
|
||||
choices: action.suggestionsAndChoices.choices,
|
||||
suggestions: action.suggestionsAndChoices.suggestions,
|
||||
};
|
||||
case "mark": {
|
||||
const marks = new Map(state.marks);
|
||||
case "updateItem": {
|
||||
const updates = new Map(state.updates);
|
||||
const { item, value } = action;
|
||||
if (item.assigned === undefined && value === undefined) {
|
||||
// If this was a suggestion, prune marks created as a result of
|
||||
// the user toggling the item back to its original unset state.
|
||||
marks.delete(item.id);
|
||||
// If this was a suggestion, prune previous updates since the
|
||||
// use has toggled the item back to its original unset state.
|
||||
updates.delete(item.id);
|
||||
} else if (item.assigned !== undefined && value === item.assigned) {
|
||||
// If this is a choice, prune marks which match the choice's
|
||||
// If this is a choice, prune updates which match the choice's
|
||||
// original assigned state.
|
||||
marks.delete(item.id);
|
||||
updates.delete(item.id);
|
||||
} else {
|
||||
marks.set(item.id, value);
|
||||
updates.set(item.id, value);
|
||||
}
|
||||
return { ...state, marks };
|
||||
return { ...state, updates: updates };
|
||||
}
|
||||
case "toggleHistory":
|
||||
return { ...state, showChoices: !state.showChoices };
|
||||
@@ -388,7 +389,7 @@ const SuggestionsDialog: React.FC<SuggestionsDialogProps> = ({
|
||||
|
||||
const isSmallWidth = useIsSmallWidth();
|
||||
|
||||
const hasUnsavedChanges = state.marks.size > 0;
|
||||
const hasUnsavedChanges = state.updates.size > 0;
|
||||
|
||||
const resetPersonAndClose = () => {
|
||||
dispatch({ type: "close" });
|
||||
@@ -439,13 +440,13 @@ const SuggestionsDialog: React.FC<SuggestionsDialogProps> = ({
|
||||
resetPersonAndClose();
|
||||
};
|
||||
|
||||
const handleMark = (item: SCItem, value: boolean | undefined) =>
|
||||
dispatch({ type: "mark", item, value });
|
||||
const handleUpdateItem = (item: SCItem, value: boolean | undefined) =>
|
||||
dispatch({ type: "updateItem", item, value });
|
||||
|
||||
const handleSave = async () => {
|
||||
dispatch({ type: "save" });
|
||||
try {
|
||||
await updateChoices(person.cgroup, [...state.marks.entries()]);
|
||||
await applyPersonSuggestionUpdates(person.cgroup, state.updates);
|
||||
resetPersonAndClose();
|
||||
} catch (e) {
|
||||
log.error("Failed to save suggestion review", e);
|
||||
@@ -517,8 +518,8 @@ const SuggestionsDialog: React.FC<SuggestionsDialogProps> = ({
|
||||
) : state.showChoices ? (
|
||||
<SuggestionOrChoiceList
|
||||
items={state.choices}
|
||||
marks={state.marks}
|
||||
onMarkItem={handleMark}
|
||||
updates={state.updates}
|
||||
onUpdateItem={handleUpdateItem}
|
||||
/>
|
||||
) : state.suggestions.length == 0 ? (
|
||||
<CenteredBox>
|
||||
@@ -532,8 +533,8 @@ const SuggestionsDialog: React.FC<SuggestionsDialogProps> = ({
|
||||
) : (
|
||||
<SuggestionOrChoiceList
|
||||
items={state.suggestions}
|
||||
marks={state.marks}
|
||||
onMarkItem={handleMark}
|
||||
updates={state.updates}
|
||||
onUpdateItem={handleUpdateItem}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
@@ -561,18 +562,18 @@ const SuggestionsDialog: React.FC<SuggestionsDialogProps> = ({
|
||||
|
||||
interface SuggestionOrChoiceListProps {
|
||||
items: SCItem[];
|
||||
marks: Map<string, boolean | undefined>;
|
||||
updates: PersonSuggestionUpdates;
|
||||
/**
|
||||
* Callback invoked when the user changes the value associated with the
|
||||
* given suggestion or choice.
|
||||
*/
|
||||
onMarkItem: (item: SCItem, value: boolean | undefined) => void;
|
||||
onUpdateItem: (item: SCItem, value: boolean | undefined) => void;
|
||||
}
|
||||
|
||||
const SuggestionOrChoiceList: React.FC<SuggestionOrChoiceListProps> = ({
|
||||
items,
|
||||
marks,
|
||||
onMarkItem,
|
||||
updates,
|
||||
onUpdateItem,
|
||||
}) => (
|
||||
<List dense sx={{ width: "100%" }}>
|
||||
{items.map((item) => (
|
||||
@@ -593,9 +594,9 @@ const SuggestionOrChoiceList: React.FC<SuggestionOrChoiceListProps> = ({
|
||||
</Stack>
|
||||
{!item.fixed && (
|
||||
<ToggleButtonGroup
|
||||
value={fromItemValue(item, marks)}
|
||||
value={fromItemValue(item, updates)}
|
||||
exclusive
|
||||
onChange={(_, v) => onMarkItem(item, toItemValue(v))}
|
||||
onChange={(_, v) => onUpdateItem(item, toItemValue(v))}
|
||||
>
|
||||
<ToggleButton value="no" aria-label={t("no")}>
|
||||
<ClearIcon />
|
||||
@@ -610,13 +611,12 @@ const SuggestionOrChoiceList: React.FC<SuggestionOrChoiceListProps> = ({
|
||||
</List>
|
||||
);
|
||||
|
||||
const fromItemValue = (
|
||||
item: SCItem,
|
||||
marks: Map<string, boolean | undefined>,
|
||||
) => {
|
||||
const fromItemValue = (item: SCItem, updates: PersonSuggestionUpdates) => {
|
||||
// Use the in-memory state if available. For choices, fallback to their
|
||||
// original state.
|
||||
const resolved = marks.has(item.id) ? marks.get(item.id) : item.assigned;
|
||||
const resolved = updates.has(item.id)
|
||||
? updates.get(item.id)
|
||||
: item.assigned;
|
||||
return resolved ? "yes" : resolved === false ? "no" : undefined;
|
||||
};
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ export const _clusterFaces = async (
|
||||
}
|
||||
|
||||
const t = `(${Date.now() - startTime} ms)`;
|
||||
log.info(`Generated ${clusters.length} clusters from ${total} faces ${t}`);
|
||||
log.info(`Refreshed ${clusters.length} clusters from ${total} faces ${t}`);
|
||||
|
||||
return clusters;
|
||||
};
|
||||
|
||||
@@ -28,10 +28,12 @@ import type { FaceCluster } from "./cluster";
|
||||
import { regenerateFaceCrops } from "./crop";
|
||||
import { clearMLDB, getIndexableAndIndexedCounts, savedFaceIndex } from "./db";
|
||||
import {
|
||||
_applyPersonSuggestionUpdates,
|
||||
filterNamedPeople,
|
||||
reconstructPeople,
|
||||
type CGroupPerson,
|
||||
type Person,
|
||||
type PersonSuggestionUpdates,
|
||||
} from "./people";
|
||||
import { MLWorker } from "./worker";
|
||||
import type { CLIPMatches } from "./worker-types";
|
||||
@@ -812,3 +814,17 @@ export const deleteCGroup = async ({ id }: CGroup) => {
|
||||
*/
|
||||
export const suggestionsAndChoicesForPerson = async (person: CGroupPerson) =>
|
||||
worker().then((w) => w.suggestionsAndChoicesForPerson(person));
|
||||
|
||||
/**
|
||||
* Implementation for the "save" action on the SuggestionsDialog.
|
||||
*
|
||||
* See {@link _applyPersonSuggestionUpdates} for more details.
|
||||
*/
|
||||
export const applyPersonSuggestionUpdates = async (
|
||||
cgroup: CGroup,
|
||||
updates: PersonSuggestionUpdates,
|
||||
) => {
|
||||
const masterKey = await masterKeyFromSession();
|
||||
await _applyPersonSuggestionUpdates(cgroup, updates, masterKey);
|
||||
return mlSync();
|
||||
};
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { assertionFailed } from "@/base/assert";
|
||||
import log from "@/base/log";
|
||||
import type { EnteFile } from "@/media/file";
|
||||
import { updateAssignedClustersForCGroup } from "@/new/photos/services/ml";
|
||||
import { shuffled } from "@/utils/array";
|
||||
import { ensure } from "@/utils/ensure";
|
||||
import { saveRejectedClustersForCGroup } from "../../services/ml/kvdb";
|
||||
import { getLocalFiles } from "../files";
|
||||
import { savedCGroups, type CGroup } from "../user-entity";
|
||||
import {
|
||||
savedCGroups,
|
||||
updateOrCreateUserEntities,
|
||||
type CGroup,
|
||||
} from "../user-entity";
|
||||
import type { FaceCluster } from "./cluster";
|
||||
import { savedFaceClusters, savedFaceIndexes } from "./db";
|
||||
import { fileIDFromFaceID } from "./face";
|
||||
import { savedRejectedClustersForCGroup } from "./kvdb";
|
||||
import {
|
||||
savedRejectedClustersForCGroup,
|
||||
saveRejectedClustersForCGroup,
|
||||
} from "./kvdb";
|
||||
import { dotProduct } from "./math";
|
||||
|
||||
/**
|
||||
@@ -534,6 +539,20 @@ const randomSample = <T>(items: T[], n: number) => {
|
||||
return [...ix].map((i) => items[i]!);
|
||||
};
|
||||
|
||||
/**
|
||||
* A map specifying the changes to make when the user presses the save button on
|
||||
* the people suggestions dialog.
|
||||
*
|
||||
* Each entry is a (clusterID, assigned) pair.
|
||||
*
|
||||
* * Entries with assigned `true` should be assigned to the cgroup,
|
||||
* * Entries with assigned `false` should be rejected from the cgroup.
|
||||
* * Entries with assigned `undefined` should be reset - i.e. they should be
|
||||
* removed from both the assigned and rejected choices associated with the
|
||||
* cgroup (if needed).
|
||||
*/
|
||||
export type PersonSuggestionUpdates = Map<string, boolean | undefined>;
|
||||
|
||||
/**
|
||||
* Implementation for the "save" action on the SuggestionsDialog.
|
||||
*
|
||||
@@ -542,17 +561,15 @@ const randomSample = <T>(items: T[], n: number) => {
|
||||
*
|
||||
* @param cgroup The cgroup that we want to update.
|
||||
*
|
||||
* @param updates The changes to make. Each entry is a (clusterID, assigned)
|
||||
* pair.
|
||||
* @param updates The changes to make. See {@link PersonSuggestionUpdates}.
|
||||
*
|
||||
* * Entries with assigned `true` should be assigned to the cgroup,
|
||||
* * Entries with assigned `false` should be rejected from the cgroup.
|
||||
* * Entries with assigned `undefined` should be removed from both the assigned
|
||||
* and rejected choices associated with the cgroup.
|
||||
* @param masterKey The user's masterKey, which is is used to encrypt and
|
||||
* decrypt the entity key associated with cgroups.
|
||||
*/
|
||||
export const updateChoices = async (
|
||||
export const _applyPersonSuggestionUpdates = async (
|
||||
cgroup: CGroup,
|
||||
updates: [string, boolean | undefined][],
|
||||
updates: PersonSuggestionUpdates,
|
||||
masterKey: Uint8Array,
|
||||
) => {
|
||||
const clusters = await savedFaceClusters();
|
||||
const clustersByID = new Map(clusters.map((c) => [c.id, c]));
|
||||
@@ -597,7 +614,7 @@ export const updateChoices = async (
|
||||
}
|
||||
};
|
||||
|
||||
for (const [clusterID, assigned] of updates) {
|
||||
for (const [clusterID, assigned] of updates.entries()) {
|
||||
switch (assigned) {
|
||||
case true /* assign */:
|
||||
// TODO-Cluster sanity check, remove after wrapping up dev
|
||||
@@ -627,7 +644,12 @@ export const updateChoices = async (
|
||||
}
|
||||
|
||||
if (assignUpdateCount > 0) {
|
||||
await updateAssignedClustersForCGroup(cgroup, assignedClusters);
|
||||
const assigned = assignedClusters;
|
||||
await updateOrCreateUserEntities(
|
||||
"cgroup",
|
||||
[{ ...cgroup, data: { ...cgroup.data, assigned } }],
|
||||
masterKey,
|
||||
);
|
||||
}
|
||||
|
||||
if (rejectUpdateCount > 0) {
|
||||
|
||||
Reference in New Issue
Block a user