Add face merging (fix #65)

old-stable24
Varun Patil 2022-10-11 12:49:05 -07:00
parent dec1f489da
commit 7f8b818056
4 changed files with 184 additions and 5 deletions

View File

@ -0,0 +1,162 @@
<template>
<Modal @close="close" size="large">
<template #title>
{{ t('memories', 'Merge {name} with person', { name }) }}
</template>
<div class="outer">
<div class="photo" v-for="photo of detail" :key="photo.fileid" >
<Tag :data="photo" :rowHeight="115" :noNavigate="true" @open="clickFace" />
</div>
<div v-if="procesingTotal > 0" class="info-pad">
{{ t('memories', 'Processing … {n}/{m}', {
n: processing,
m: procesingTotal,
}) }}
</div>
</div>
<template #buttons>
<NcButton @click="close" class="button" type="error">
{{ t('memories', 'Cancel') }}
</NcButton>
</template>
</Modal>
</template>
<script lang="ts">
import { Component, Mixins, Watch } from 'vue-property-decorator';
import { NcButton, NcTextField } from '@nextcloud/vue';
import { showError } from '@nextcloud/dialogs'
import { IFileInfo, IPhoto, ITag } from '../types';
import Tag from './Tag.vue';
import Modal from './Modal.vue';
import GlobalMixin from '../mixins/GlobalMixin';
import * as dav from '../services/DavRequests';
import client from '../services/DavClient';
@Component({
components: {
NcButton,
NcTextField,
Modal,
Tag,
}
})
export default class FaceMergeModal extends Mixins(GlobalMixin) {
private user: string = "";
private name: string = "";
private detail: IPhoto[] = [];
private processing = 0;
private procesingTotal = 0;
@Watch('$route')
async routeChange(from: any, to: any) {
this.refreshParams();
}
mounted() {
this.refreshParams();
}
public async refreshParams() {
this.user = this.$route.params.user || '';
this.name = this.$route.params.name || '';
this.detail = [];
this.processing = 0;
this.procesingTotal = 0;
const data = await dav.getPeopleData();
let detail = data[0].detail;
detail.forEach((photo: IPhoto) => {
photo.flag = this.c.FLAG_IS_FACE | this.c.FLAG_IS_TAG;
});
detail = detail.filter((photo: ITag) => {
const pname = photo.name || photo.fileid.toString();
return photo.user_id !== this.user || pname !== this.name;
});
this.detail = detail;
}
public async clickFace(face: ITag) {
const newName = face.name || face.fileid.toString();
if (!confirm(this.t('memories', 'Are you sure you want to merge {name} with {newName}?', { name: this.name, newName}))) {
return;
}
try {
// Get all files for current face
let res = await client.getDirectoryContents(
`/recognize/${this.user}/faces/${this.name}`, { details: true }
) as any;
let data: IFileInfo[] = res.data;
this.procesingTotal = data.length;
// Don't try too much
let failures = 0;
// Create move calls
const calls = data.map((p) => async () => {
// Short circuit if we have too many failures
if (failures === 10) {
showError(this.t('memories', 'Too many failures, aborting'));
failures++;
}
if (failures >= 10) return;
// Move to new face with webdav
try {
await client.moveFile(
`/recognize/${this.user}/faces/${this.name}/${p.basename}`,
`/recognize/${face.user_id}/faces/${newName}/${p.basename}`
)
} catch (e) {
console.error(e);
showError(this.t('memories', 'Error while moving {basename}', p));
failures++;
} finally {
this.processing++;
}
});
for await (const _ of dav.runInParallel(calls, 10)) {
// nothing to do
}
// Go to new face
if (failures === 0) {
this.$router.push({ name: 'people', params: { user: face.user_id, name: newName } });
this.close();
}
} catch (error) {
console.error(error);
showError(this.t('photos', 'Failed to move {name}.', {
name: this.name,
}));
}
}
public close() {
this.$emit('close');
}
}
</script>
<style lang="scss" scoped>
.outer {
margin-top: 15px;
}
.photo {
display: inline-block;
position: relative;
cursor: pointer;
vertical-align: top;
font-size: 0.8em;
}
.info-pad {
margin-top: 6px;
margin-bottom: 2px;
}
</style>

View File

@ -11,11 +11,15 @@
<div class="right-actions"> <div class="right-actions">
<NcActions :inline="1"> <NcActions :inline="1">
<NcActionButton :aria-label="t('memories', 'Rename person')" @click="showEditModal=true"> <NcActionButton :aria-label="t('memories', 'Rename person')" @click="showEditModal=true" close-after-click>
{{ t('memories', 'Rename person') }} {{ t('memories', 'Rename person') }}
<template #icon> <EditIcon :size="20" /> </template> <template #icon> <EditIcon :size="20" /> </template>
</NcActionButton> </NcActionButton>
<NcActionButton :aria-label="t('memories', 'Remove person')" @click="showDeleteModal=true"> <NcActionButton :aria-label="t('memories', 'Merge with different person')" @click="showMergeModal=true" close-after-click>
{{ t('memories', 'Merge with different person') }}
<template #icon> <MergeIcon :size="20" /> </template>
</NcActionButton>
<NcActionButton :aria-label="t('memories', 'Remove person')" @click="showDeleteModal=true" close-after-click>
{{ t('memories', 'Remove person') }} {{ t('memories', 'Remove person') }}
<template #icon> <DeleteIcon :size="20" /> </template> <template #icon> <DeleteIcon :size="20" /> </template>
</NcActionButton> </NcActionButton>
@ -24,6 +28,7 @@
<FaceEditModal v-if="showEditModal" @close="showEditModal=false" /> <FaceEditModal v-if="showEditModal" @close="showEditModal=false" />
<FaceDeleteModal v-if="showDeleteModal" @close="showDeleteModal=false" /> <FaceDeleteModal v-if="showDeleteModal" @close="showDeleteModal=false" />
<FaceMergeModal v-if="showMergeModal" @close="showMergeModal=false" />
</div> </div>
</template> </template>
@ -34,9 +39,11 @@ import GlobalMixin from '../mixins/GlobalMixin';
import { NcActions, NcActionButton } from '@nextcloud/vue'; import { NcActions, NcActionButton } from '@nextcloud/vue';
import FaceEditModal from './FaceEditModal.vue'; import FaceEditModal from './FaceEditModal.vue';
import FaceDeleteModal from './FaceDeleteModal.vue'; import FaceDeleteModal from './FaceDeleteModal.vue';
import FaceMergeModal from './FaceMergeModal.vue';
import BackIcon from 'vue-material-design-icons/ArrowLeft.vue'; import BackIcon from 'vue-material-design-icons/ArrowLeft.vue';
import EditIcon from 'vue-material-design-icons/Pencil.vue'; import EditIcon from 'vue-material-design-icons/Pencil.vue';
import DeleteIcon from 'vue-material-design-icons/Close.vue'; import DeleteIcon from 'vue-material-design-icons/Close.vue';
import MergeIcon from 'vue-material-design-icons/Merge.vue';
@Component({ @Component({
components: { components: {
@ -44,15 +51,18 @@ import DeleteIcon from 'vue-material-design-icons/Close.vue';
NcActionButton, NcActionButton,
FaceEditModal, FaceEditModal,
FaceDeleteModal, FaceDeleteModal,
FaceMergeModal,
BackIcon, BackIcon,
EditIcon, EditIcon,
DeleteIcon, DeleteIcon,
MergeIcon,
}, },
}) })
export default class FaceTopMatter extends Mixins(GlobalMixin) { export default class FaceTopMatter extends Mixins(GlobalMixin) {
private name: string = ''; private name: string = '';
private showEditModal: boolean = false; private showEditModal: boolean = false;
private showDeleteModal: boolean = false; private showDeleteModal: boolean = false;
private showMergeModal: boolean = false;
@Watch('$route') @Watch('$route')
async routeChange(from: any, to: any) { async routeChange(from: any, to: any) {

View File

@ -1,6 +1,6 @@
<template> <template>
<NcModal <NcModal
size="small" :size="size"
@close="$emit('close')" @close="$emit('close')"
:outTransition="true"> :outTransition="true">
<div class="container"> <div class="container">
@ -18,7 +18,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from 'vue-property-decorator'; import { Component, Prop, Vue } from 'vue-property-decorator';
import { NcModal } from '@nextcloud/vue'; import { NcModal } from '@nextcloud/vue';
@Component({ @Component({
@ -27,6 +27,7 @@ import { NcModal } from '@nextcloud/vue';
} }
}) })
export default class Modal extends Vue { export default class Modal extends Vue {
@Prop({default: 'small'}) private size?: string;
} }
</script> </script>

View File

@ -57,6 +57,7 @@ interface IFaceDetection extends IPhoto {
export default class Tag extends Mixins(GlobalMixin) { export default class Tag extends Mixins(GlobalMixin) {
@Prop() data: ITag; @Prop() data: ITag;
@Prop() rowHeight: number; @Prop() rowHeight: number;
@Prop() noNavigate: boolean;
// Separate property because the one on data isn't reactive // Separate property because the one on data isn't reactive
private previews: IPhoto[] = []; private previews: IPhoto[] = [];
@ -112,6 +113,11 @@ export default class Tag extends Mixins(GlobalMixin) {
/** Open tag */ /** Open tag */
openTag() { openTag() {
this.$emit('open', this.data);
if (this.noNavigate) {
return;
}
if (this.isFace) { if (this.isFace) {
const name = this.data.name || this.data.fileid.toString(); const name = this.data.name || this.data.fileid.toString();
const user = this.data.user_id; const user = this.data.user_id;
@ -178,7 +184,7 @@ export default class Tag extends Mixins(GlobalMixin) {
top: 50%; width: 100%; top: 50%; width: 100%;
transform: translateY(-50%); transform: translateY(-50%);
color: white; color: white;
width: 100%; width: 90%;
padding: 0 5%; padding: 0 5%;
text-align: center; text-align: center;
font-size: 1.2em; font-size: 1.2em;