Implement unclustered photos for recognize (fix #475)

Signed-off-by: Varun Patil <radialapps@gmail.com>
pull/743/head
Varun Patil 2023-07-03 17:07:50 -07:00
parent a87c29a2fa
commit 590d8272d5
11 changed files with 125 additions and 82 deletions

View File

@ -94,7 +94,7 @@ class RecognizeBackend extends Backend
// Join with cluster
$clusterQuery = null;
if ('NULL' === $faceName) {
$clusterQuery = $query->expr()->isNull('rfd.cluster_id');
$clusterQuery = $query->expr()->eq('rfd.cluster_id', $query->expr()->literal(-1));
} else {
$nameField = is_numeric($faceName) ? 'rfc.id' : 'rfc.title';
$query->innerJoin('m', 'recognize_face_clusters', 'rfc', $query->expr()->andX(

View File

@ -201,18 +201,25 @@ export default defineComponent({
if: (self: any) => self.config.albums_enabled && !self.routeIsAlbums,
},
{
id: 'face-move',
name: t('memories', 'Move to person'),
icon: MoveIcon,
callback: this.moveSelectionToPerson.bind(this),
if: () => this.$route.name === 'recognize',
if: () => this.routeIsRecognize,
},
{
name: t('memories', 'Remove from person'),
icon: CloseIcon,
callback: this.removeSelectionFromPerson.bind(this),
if: () => this.$route.name === 'recognize',
if: () => this.routeIsRecognize && !this.routeIsRecognizeUnassigned,
},
];
// Move face-move to start if unassigned faces
if (this.routeIsRecognizeUnassigned) {
const i = this.defaultActions.findIndex((a) => a.id === 'face-move');
this.defaultActions.unshift(this.defaultActions.splice(i, 1)[0]);
}
},
beforeDestroy() {
@ -800,7 +807,7 @@ export default defineComponent({
* Move selected photos to another person
*/
async moveSelectionToPerson(selection: Selection) {
if (!this.config.show_face_rect) {
if (!this.config.show_face_rect && !this.routeIsRecognizeUnassigned) {
showError(this.t('memories', 'You must enable "Mark person in preview" to use this feature'));
return;
}

View File

@ -593,7 +593,7 @@ export default defineComponent({
API.DAYS_FILTER(query, filter, `${user}/${name}`);
// Face rect
if (this.config.show_face_rect) {
if (this.config.show_face_rect || this.routeIsRecognizeUnassigned) {
API.DAYS_FILTER(query, DaysFilterType.FACE_RECT);
}
}

View File

@ -90,16 +90,7 @@ export default defineComponent({
const name = this.$route.params.name || '';
const target = String(face.name || face.cluster_id);
if (
!confirm(
this.t('memories', 'Are you sure you want to move the selected photos from {name} to {target}?', {
name,
target,
})
)
) {
return;
}
if (!confirm(this.t('memories', 'Move the selected photos to {target}?', { target }))) return;
try {
this.show = false;

View File

@ -1,17 +1,27 @@
<template>
<div v-if="name" class="face-top-matter">
<NcActions>
<div class="face-top-matter">
<NcActions v-if="name">
<NcActionButton :aria-label="t('memories', 'Back')" @click="back()">
{{ t('memories', 'Back') }}
<template #icon> <BackIcon :size="20" /> </template>
</NcActionButton>
</NcActions>
<div class="name">{{ name }}</div>
<div class="name" :class="{ rename: isReal }" @click="rename">{{ displayName }}</div>
<div class="right-actions">
<NcActions :inline="1">
<NcActionButton :aria-label="t('memories', 'Rename person')" @click="$refs.editModal?.open()" close-after-click>
<NcActions :inline="0">
<!-- root view (not cluster or unassigned) -->
<template v-if="!name && routeIsRecognize && !routeIsRecognizeUnassigned">
<NcActionButton :aria-label="t('memories', 'Unassigned faces')" @click="openUnassigned" close-after-click>
{{ t('memories', 'Unassigned faces') }}
<template #icon> <UnassignedIcon :size="20" /> </template>
</NcActionButton>
</template>
<!-- real cluster -->
<template v-if="isReal">
<NcActionButton :aria-label="t('memories', 'Rename person')" @click="rename" close-after-click>
{{ t('memories', 'Rename person') }}
<template #icon> <EditIcon :size="20" /> </template>
</NcActionButton>
@ -38,6 +48,7 @@
{{ t('memories', 'Remove person') }}
<template #icon> <DeleteIcon :size="20" /> </template>
</NcActionButton>
</template>
</NcActions>
</div>
@ -56,6 +67,7 @@ import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton';
import NcActionCheckbox from '@nextcloud/vue/dist/Components/NcActionCheckbox';
import { emit } from '@nextcloud/event-bus';
import { getCurrentUser } from '@nextcloud/auth';
import FaceEditModal from '../modal/FaceEditModal.vue';
import FaceDeleteModal from '../modal/FaceDeleteModal.vue';
@ -64,6 +76,9 @@ import BackIcon from 'vue-material-design-icons/ArrowLeft.vue';
import EditIcon from 'vue-material-design-icons/Pencil.vue';
import DeleteIcon from 'vue-material-design-icons/Close.vue';
import MergeIcon from 'vue-material-design-icons/Merge.vue';
import UnassignedIcon from 'vue-material-design-icons/AccountQuestion.vue';
import * as utils from '../../services/Utils';
export default defineComponent({
name: 'FaceTopMatter',
@ -78,6 +93,7 @@ export default defineComponent({
EditIcon,
DeleteIcon,
MergeIcon,
UnassignedIcon,
},
mixins: [UserConfig],
@ -86,6 +102,19 @@ export default defineComponent({
name() {
return this.$route.params.name || '';
},
isReal() {
return this.name && this.name !== utils.constants.FACE_NULL;
},
displayName() {
if (this.routeIsRecognizeUnassigned) {
return this.t('memories', 'Unassigned faces');
} else if (!this.name) {
return this.t('memories', 'People');
}
return this.name;
},
},
methods: {
@ -93,6 +122,20 @@ export default defineComponent({
this.$router.go(-1);
},
rename() {
if (this.name) (<any>this.$refs.editModal)?.open();
},
openUnassigned() {
this.$router.push({
name: this.$route.name as string,
params: {
user: String(getCurrentUser()?.uid),
name: utils.constants.FACE_NULL,
},
});
},
changeShowFaceRect() {
this.updateSetting('show_face_rect');
emit('memories:timeline:hard-refresh', {});
@ -100,3 +143,14 @@ export default defineComponent({
},
});
</script>
<style scoped lang="scss">
.face-top-matter {
.name.rename:hover {
cursor: text;
text-decoration: underline;
text-decoration-color: var(--color-placeholder-light);
text-underline-offset: 5px;
}
}
</style>

View File

@ -20,8 +20,6 @@ import ClusterTopMatter from './ClusterTopMatter.vue';
import FaceTopMatter from './FaceTopMatter.vue';
import AlbumTopMatter from './AlbumTopMatter.vue';
import { TopMatterType } from '../../types';
export default defineComponent({
name: 'TopMatter',
components: {
@ -44,32 +42,17 @@ export default defineComponent({
},
computed: {
type() {
currentmatter() {
switch (this.$route.name) {
case 'folders':
return TopMatterType.FOLDER;
return FolderTopMatter;
case 'albums':
return TopMatterType.ALBUM;
return AlbumTopMatter;
case 'tags':
case 'places':
return TopMatterType.CLUSTER;
return ClusterTopMatter;
case 'recognize':
case 'facerecognition':
return this.$route.params.name ? TopMatterType.FACE : TopMatterType.CLUSTER;
default:
return TopMatterType.NONE;
}
},
currentmatter() {
switch (this.type) {
case TopMatterType.FOLDER:
return FolderTopMatter;
case TopMatterType.ALBUM:
return AlbumTopMatter;
case TopMatterType.CLUSTER:
return ClusterTopMatter;
case TopMatterType.FACE:
return FaceTopMatter;
default:
return null;

View File

@ -25,6 +25,12 @@ export default defineComponent({
routeIsPeople(): boolean {
return ['recognize', 'facerecognition'].includes(<string>this.$route.name);
},
routeIsRecognize(): boolean {
return this.$route.name === 'recognize';
},
routeIsRecognizeUnassigned(): boolean {
return this.routeIsRecognize && this.$route.params.name === constants.FACE_NULL;
},
routeIsArchive(): boolean {
return this.$route.name === 'archive';
},

View File

@ -5,6 +5,7 @@ import { generateUrl } from '@nextcloud/router';
import { IFace, IPhoto } from '../../types';
import { API } from '../API';
import client from '../DavClient';
import * as utils from '../Utils';
import * as base from './base';
export async function getFaceList(app: 'recognize' | 'facerecognition') {
@ -83,10 +84,16 @@ export async function* recognizeMoveFaceImages(user: string, face: string, targe
// Remove each file
const calls = photos.map((p) => async () => {
try {
await client.moveFile(
`/recognize/${user}/faces/${face}/${p.faceid}-${p.basename}`,
`/recognize/${user}/faces/${target}/${p.faceid}-${p.basename}`
);
const dest = `/recognize/${user}/faces/${target}`;
const name = `${p.faceid}-${p.basename}`;
// NULL source needs special handling
let source = `/recognize/${user}/faces/${face}`;
if (face === utils.constants.FACE_NULL) {
source = `/recognize/${user}/unassigned-faces`;
}
await client.moveFile(`${source}/${name}`, `${dest}/${name}`);
return p.faceid!;
} catch (e) {
console.error(e);

View File

@ -11,6 +11,8 @@ export const constants = {
FLAG_LEAVING: 1 << 5,
FLAG_IS_LOCAL: 1 << 6,
},
FACE_NULL: 'NULL',
};
/**

View File

@ -208,18 +208,9 @@ export type ITick = {
key?: number;
};
export type TopMatter = {
type: TopMatterType;
};
export enum TopMatterType {
NONE = 0,
FOLDER = 1,
CLUSTER = 2,
FACE = 3,
ALBUM = 4,
}
export type ISelectionAction = {
/** Identifier (optional) */
id?: string;
/** Display text */
name: string;
/** Icon component */

View File

@ -15,6 +15,8 @@ declare module 'vue' {
routeIsFolders: boolean;
routeIsAlbums: boolean;
routeIsPeople: boolean;
routeIsRecognize: boolean;
routeIsRecognizeUnassigned: boolean;
routeIsArchive: boolean;
routeIsPlaces: boolean;
routeIsMap: boolean;