From 429c821fbf15578ef7153d3fb7e7553dc77b5d40 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 28 Apr 2023 22:29:10 -0700 Subject: [PATCH] Support for recognize v3.8 (fix #618) Signed-off-by: Varun Patil --- lib/ClustersBackend/RecognizeBackend.php | 6 +- lib/Util.php | 2 +- src/components/SelectionManager.vue | 22 ++++-- src/components/modal/FaceDeleteModal.vue | 4 +- src/components/modal/FaceEditModal.vue | 7 +- src/components/modal/FaceMoveModal.vue | 35 +++------ src/services/dav/face.ts | 94 ++++++++++++++++++++---- 7 files changed, 114 insertions(+), 56 deletions(-) diff --git a/lib/ClustersBackend/RecognizeBackend.php b/lib/ClustersBackend/RecognizeBackend.php index e00549ed..5266c8f3 100644 --- a/lib/ClustersBackend/RecognizeBackend.php +++ b/lib/ClustersBackend/RecognizeBackend.php @@ -57,9 +57,13 @@ class RecognizeBackend extends Backend public function transformDayQuery(&$query, bool $aggregate): void { - $faceStr = (string) $this->request->getParam('recognize'); + // Check if Recognize is enabled + if (!$this->isEnabled()) { + throw \OCA\Memories\Exceptions::NotEnabled('Recognize'); + } // Get name and uid of face user + $faceStr = (string) $this->request->getParam('recognize'); $faceNames = explode('/', $faceStr); if (2 !== \count($faceNames)) { throw new \Exception('Invalid face query'); diff --git a/lib/Util.php b/lib/Util.php index 4886f5d9..b62434d3 100644 --- a/lib/Util.php +++ b/lib/Util.php @@ -92,7 +92,7 @@ class Util } $v = $appManager->getAppVersion('recognize'); - if (!version_compare($v, '3.0.0-alpha', '>=')) { + if (!version_compare($v, '3.8.0', '>=')) { return false; } diff --git a/src/components/SelectionManager.vue b/src/components/SelectionManager.vue index 8f73cf0f..087998a3 100644 --- a/src/components/SelectionManager.vue +++ b/src/components/SelectionManager.vue @@ -801,17 +801,23 @@ export default defineComponent({ // Check photo ownership if (this.$route.params.user !== getCurrentUser()?.uid) { - showError( - this.t('memories', 'Only user "{user}" can update this person', { - user, - }) - ); + showError(this.t('memories', 'Only user "{user}" can update this person', { user })); return; } - // Run query - for await (let delIds of dav.removeFaceImages(user, name, Array.from(selection.values()))) { - this.deleteSelectedPhotosById(delIds, selection); + // Make map to get back photo from faceid + const map = new Map(); + for (const photo of selection.values()) { + if (photo.faceid) { + map.set(photo.faceid, photo); + } + } + const photos = Array.from(map.values()); + + // Run WebDAV query + for await (let delIds of dav.recognizeDeleteFaceImages(user, name, photos)) { + const fileIds = delIds.map((id) => map.get(id)?.fileid ?? 0).filter((id) => id); + this.deleteSelectedPhotosById(fileIds, selection); } }, diff --git a/src/components/modal/FaceDeleteModal.vue b/src/components/modal/FaceDeleteModal.vue index 609377ff..79fcb8f2 100644 --- a/src/components/modal/FaceDeleteModal.vue +++ b/src/components/modal/FaceDeleteModal.vue @@ -77,9 +77,9 @@ export default defineComponent({ async save() { try { if (this.$route.name === 'recognize') { - await client.deleteFile(`/recognize/${this.user}/faces/${this.name}`); + await dav.recognizeDeleteFace(this.user, this.name); } else { - await dav.setVisibilityPeopleFaceRecognition(this.name, false); + await dav.faceRecognitionSetPersonVisibility(this.name, false); } this.$router.push({ name: this.$route.name as string }); this.close(); diff --git a/src/components/modal/FaceEditModal.vue b/src/components/modal/FaceEditModal.vue index a44644ce..a1c687af 100644 --- a/src/components/modal/FaceEditModal.vue +++ b/src/components/modal/FaceEditModal.vue @@ -89,12 +89,9 @@ export default defineComponent({ async save() { try { if (this.$route.name === 'recognize') { - await client.moveFile( - `/recognize/${this.user}/faces/${this.oldName}`, - `/recognize/${this.user}/faces/${this.name}` - ); + await dav.recognizeRenameFace(this.user, this.oldName, this.name); } else { - await dav.renamePeopleFaceRecognition(this.oldName, this.name); + await dav.faceRecognitionRenamePerson(this.oldName, this.name); } this.$router.push({ name: this.$route.name as string, diff --git a/src/components/modal/FaceMoveModal.vue b/src/components/modal/FaceMoveModal.vue index 18492079..c13783b0 100644 --- a/src/components/modal/FaceMoveModal.vue +++ b/src/components/modal/FaceMoveModal.vue @@ -89,14 +89,13 @@ export default defineComponent({ async clickFace(face: IFace) { const user = this.$route.params.user || ''; const name = this.$route.params.name || ''; - - const newName = String(face.name || face.cluster_id); + 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 {newName}?', { + this.t('memories', 'Are you sure you want to move the selected photos from {name} to {target}?', { name, - newName, + target, }) ) ) { @@ -108,31 +107,19 @@ export default defineComponent({ this.updateLoading(1); // Create map to return IPhoto later - let photoMap = new Map(); - for (const photo of this.photos) { - photoMap.set(photo.fileid, photo); + const map = new Map(); + for (const photo of this.photos.filter((p) => p.faceid)) { + map.set(photo.faceid!, photo); } - // Create move calls - const calls = this.photos.map((p) => async () => { - try { - await client.moveFile( - `/recognize/${user}/faces/${name}/${p.fileid}-${p.basename}`, - `/recognize/${face.user_id}/faces/${newName}/${p.fileid}-${p.basename}` - ); - return photoMap.get(p.fileid); - } catch (e) { - console.error(e); - showError(this.t('memories', 'Error while moving {basename}', p)); - } - }); - for await (const resp of dav.runInParallel(calls, 10)) { - const valid = resp.filter((r): r is IPhoto => r !== undefined); - this.moved(valid); + // Run WebDAV query + const photos = Array.from(map.values()); + for await (let delIds of dav.recognizeMoveFaceImages(user, name, target, photos)) { + this.moved(delIds.filter((id) => id).map((id) => map.get(id)!)); } } catch (error) { console.error(error); - showError(this.t('photos', 'Failed to move {name}.', { name })); + showError(this.t('photos', 'An error occured while moving photos from {name}.', { name })); } finally { this.updateLoading(-1); this.close(); diff --git a/src/services/dav/face.ts b/src/services/dav/face.ts index f93bab11..de59bbef 100644 --- a/src/services/dav/face.ts +++ b/src/services/dav/face.ts @@ -11,7 +11,12 @@ export async function getFaceList(app: 'recognize' | 'facerecognition') { return (await axios.get(API.FACE_LIST(app))).data; } -export async function updatePeopleFaceRecognition(name: string, params: object) { +/** + * Update a person or cluster in face recognition + * @param name Name of face (or ID) + * @param params Parameters to update + */ +export async function faceRecognitionUpdatePerson(name: string, params: object) { if (Number.isInteger(Number(name))) { return await axios.put(generateUrl(`/apps/facerecognition/api/2.0/cluster/${name}`), params); } else { @@ -19,16 +24,22 @@ export async function updatePeopleFaceRecognition(name: string, params: object) } } -export async function renamePeopleFaceRecognition(name: string, newName: string) { - return await updatePeopleFaceRecognition(name, { - name: newName, - }); +/** + * Rename a face in face recognition + * @param name Name of face (or ID) + * @param target Target name of face + */ +export async function faceRecognitionRenamePerson(name: string, target: string) { + return await faceRecognitionUpdatePerson(name, { name: target }); } -export async function setVisibilityPeopleFaceRecognition(name: string, visibility: boolean) { - return await updatePeopleFaceRecognition(name, { - visible: visibility, - }); +/** + * Set visibility of a face + * @param name Name of face (or ID) + * @param visible Visibility of face + */ +export async function faceRecognitionSetPersonVisibility(name: string, visible: boolean) { + return await faceRecognitionUpdatePerson(name, { visible }); } /** @@ -37,19 +48,19 @@ export async function setVisibilityPeopleFaceRecognition(name: string, visibilit * @param user User ID of face * @param name Name of face (or ID) * @param photos List of photos to remove - * @returns Generator + * @returns Generator for face IDs */ -export async function* removeFaceImages(user: string, name: string, photos: IPhoto[]) { +export async function* recognizeDeleteFaceImages(user: string, name: string, photos: IPhoto[]) { // Remove each file - const calls = photos.map((f) => async () => { + const calls = photos.map((p) => async () => { try { - await client.deleteFile(`/recognize/${user}/faces/${name}/${f.fileid}-${f.basename}`); - return f.fileid; + await client.deleteFile(`/recognize/${user}/faces/${name}/${p.faceid}-${p.basename}`); + return p.faceid!; } catch (e) { console.error(e); showError( t('memories', 'Failed to remove {filename} from face.', { - filename: f.basename ?? f.fileid, + filename: p.basename ?? p.fileid, }) ); return 0; @@ -58,3 +69,56 @@ export async function* removeFaceImages(user: string, name: string, photos: IPho yield* base.runInParallel(calls, 10); } + +/** + * Move faces from one face to another + * + * @param user User ID of face + * @param face Name of face (or ID) + * @param target Name of target face (or ID) + * @param photos List of photos to move + * @returns Generator for face IDs + */ +export async function* recognizeMoveFaceImages(user: string, face: string, target: string, photos: IPhoto[]) { + // 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}` + ); + return p.faceid!; + } catch (e) { + console.error(e); + showError( + t('memories', 'Failed to move {filename} from face.', { + filename: p.basename ?? p.fileid, + }) + ); + return 0; + } + }); + + yield* base.runInParallel(calls, 10); +} + +/** + * Remove a face entirely + * + * @param user User ID of face + * @param name Name of face (or ID) + */ +export async function recognizeDeleteFace(user: string, name: string) { + return await client.deleteFile(`/recognize/${user}/faces/${name}`); +} + +/** + * Rename a face in recognize + * + * @param user User ID of face + * @param name Name of face (or ID) + * @param target Target name of face + */ +export async function recognizeRenameFace(user: string, name: string, target: string) { + await client.moveFile(`/recognize/${user}/faces/${name}`, `/recognize/${user}/faces/${target}`); +}