From d1e9205a542ef1ad7de9d0ce7e01f578ea8eada0 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 7 Oct 2022 12:28:39 -0700 Subject: [PATCH] Implement people tab for recognize 3 (fix #43) --- appinfo/routes.php | 22 +++--- lib/Controller/ApiController.php | 57 ++++++++++++++++ lib/Controller/PageController.php | 9 +++ lib/Db/TimelineQuery.php | 1 + lib/Db/TimelineQueryFaces.php | 86 ++++++++++++++++++++++++ src/App.vue | 6 ++ src/components/Tag.vue | 108 +++++++++++++++++++++++++++--- src/components/Timeline.vue | 19 ++++-- src/mixins/UserConfig.ts | 1 + src/router.ts | 9 +++ src/services/DavRequests.ts | 34 +++++++++- src/services/Utils.ts | 12 ++-- src/types.ts | 4 ++ 13 files changed, 338 insertions(+), 30 deletions(-) create mode 100644 lib/Db/TimelineQueryFaces.php diff --git a/appinfo/routes.php b/appinfo/routes.php index e11adb3c..1bb59e98 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -4,24 +4,20 @@ return [ // Days and folder API ['name' => 'page#main', 'url' => '/', 'verb' => 'GET'], ['name' => 'page#folder', 'url' => '/folders/{path}', 'verb' => 'GET', - 'requirements' => [ - 'path' => '.*', - ], - 'defaults' => [ - 'path' => '', - ] + 'requirements' => [ 'path' => '.*' ], + 'defaults' => [ 'path' => '' ] ], ['name' => 'page#favorites', 'url' => '/favorites', 'verb' => 'GET'], ['name' => 'page#videos', 'url' => '/videos', 'verb' => 'GET'], ['name' => 'page#archive', 'url' => '/archive', 'verb' => 'GET'], ['name' => 'page#thisday', 'url' => '/thisday', 'verb' => 'GET'], + ['name' => 'page#people', 'url' => '/people/{name}', 'verb' => 'GET', + 'requirements' => [ 'name' => '.*' ], + 'defaults' => [ 'name' => '' ] + ], ['name' => 'page#tags', 'url' => '/tags/{name}', 'verb' => 'GET', - 'requirements' => [ - 'name' => '.*', - ], - 'defaults' => [ - 'name' => '', - ] + 'requirements' => [ 'name' => '.*' ], + 'defaults' => [ 'name' => '' ] ], // API @@ -29,6 +25,8 @@ return [ ['name' => 'api#dayPost', 'url' => '/api/days', 'verb' => 'POST'], ['name' => 'api#day', 'url' => '/api/days/{id}', 'verb' => 'GET'], ['name' => 'api#tags', 'url' => '/api/tags', 'verb' => 'GET'], + ['name' => 'api#faces', 'url' => '/api/faces', 'verb' => 'GET'], + ['name' => 'api#facePreviews', 'url' => '/api/face-previews/{id}', 'verb' => 'GET'], ['name' => 'api#imageInfo', 'url' => '/api/info/{id}', 'verb' => 'GET'], ['name' => 'api#imageEdit', 'url' => '/api/edit/{id}', 'verb' => 'PATCH'], ['name' => 'api#archive', 'url' => '/api/archive/{id}', 'verb' => 'PATCH'], diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php index e1fcceec..25c0712c 100644 --- a/lib/Controller/ApiController.php +++ b/lib/Controller/ApiController.php @@ -81,6 +81,12 @@ class ApiController extends Controller { $transforms[] = array($this->timelineQuery, 'transformVideoFilter'); } + // Filter only for one face + $faceId = $this->request->getParam('face'); + if ($faceId) { + $transforms[] = array($this->timelineQuery, 'transformFaceFilter', intval($faceId)); + } + // Filter only for one tag $tagName = $this->request->getParam('tag'); if ($tagName) { @@ -303,6 +309,57 @@ class ApiController extends Controller { return new JSONResponse($list, Http::STATUS_OK); } + /** + * @NoAdminRequired + * + * Get list of faces with counts of images + * @return JSONResponse + */ + public function faces(): JSONResponse { + $user = $this->userSession->getUser(); + if (is_null($user)) { + return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED); + } + + // If this isn't the timeline folder then things aren't going to work + $folder = $this->getRequestFolder(); + if (is_null($folder)) { + return new JSONResponse([], Http::STATUS_NOT_FOUND); + } + + // Run actual query + $list = $this->timelineQuery->getFaces( + $folder, + ); + return new JSONResponse($list, Http::STATUS_OK); + } + + /** + * @NoAdminRequired + * @NoCSRFRequired + * + * Get preview objects for a face ID + * @return JSONResponse + */ + public function facePreviews(string $id): JSONResponse { + $user = $this->userSession->getUser(); + if (is_null($user)) { + return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED); + } + + // If this isn't the timeline folder then things aren't going to work + $folder = $this->getRequestFolder(); + if (is_null($folder)) { + return new JSONResponse([], Http::STATUS_NOT_FOUND); + } + + // Run actual query + $list = $this->timelineQuery->getFacePreviews( + $folder, intval($id), + ); + return new JSONResponse($list, Http::STATUS_OK); + } + /** * @NoAdminRequired * diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php index a73025fc..3ef05f18 100644 --- a/lib/Controller/PageController.php +++ b/lib/Controller/PageController.php @@ -68,6 +68,7 @@ class PageController extends Controller { // Apps enabled $this->initialState->provideInitialState('systemtags', $this->appManager->isEnabledForUser('systemtags') === true); + $this->initialState->provideInitialState('recognize', $this->appManager->isEnabledForUser('recognize') === true); $response = new TemplateResponse($this->appName, 'main'); return $response; @@ -113,6 +114,14 @@ class PageController extends Controller { return $this->main(); } + /** + * @NoAdminRequired + * @NoCSRFRequired + */ + public function people() { + return $this->main(); + } + /** * @NoAdminRequired * @NoCSRFRequired diff --git a/lib/Db/TimelineQuery.php b/lib/Db/TimelineQuery.php index 13092d31..c31895d1 100644 --- a/lib/Db/TimelineQuery.php +++ b/lib/Db/TimelineQuery.php @@ -9,6 +9,7 @@ class TimelineQuery { use TimelineQueryDays; use TimelineQueryFilters; use TimelineQueryTags; + use TimelineQueryFaces; protected IDBConnection $connection; diff --git a/lib/Db/TimelineQueryFaces.php b/lib/Db/TimelineQueryFaces.php new file mode 100644 index 00000000..cd959854 --- /dev/null +++ b/lib/Db/TimelineQueryFaces.php @@ -0,0 +1,86 @@ +innerJoin('m', 'recognize_face_detections', 'rfd', $query->expr()->andX( + $query->expr()->eq('rfd.file_id', 'm.fileid'), + $query->expr()->eq('rfd.cluster_id', $query->createNamedParameter($faceId)), + )); + } + + public function getFaces(Folder $folder) { + $query = $this->connection->getQueryBuilder(); + + // SELECT all face clusters + $count = $query->func()->count($query->createFunction('DISTINCT m.fileid'), 'count'); + $query->select('rfc.id', 'rfc.title', $count)->from('recognize_face_clusters', 'rfc'); + + // WHERE there are faces with this cluster + $query->innerJoin('rfc', 'recognize_face_detections', 'rfd', $query->expr()->eq('rfc.id', 'rfd.cluster_id')); + + // WHERE these items are memories indexed photos + $query->innerJoin('rfd', 'memories', 'm', $query->expr()->eq('m.fileid', 'rfd.file_id')); + + // WHERE these photos are in the user's requested folder recursively + $query->innerJoin('m', 'filecache', 'f', $this->getFilecacheJoinQuery($query, $folder, true, false)); + + // GROUP by ID of face cluster + $query->groupBy('rfc.id'); + + // ORDER by number of faces in cluster + $query->orderBy('count', 'DESC'); + + // FETCH all faces + $faces = $query->executeQuery()->fetchAll(); + + // Post process + foreach($faces as &$row) { + $row["name"] = $row["title"]; + unset($row["title"]); + $row["count"] = intval($row["count"]); + } + + return $faces; + } + + public function getFacePreviews(Folder $folder, int $faceId) { + $query = $this->connection->getQueryBuilder(); + + // SELECT face detections for ID + $query->select('rfd.file_id', 'rfd.x', 'rfd.y', 'rfd.width', 'rfd.height', 'f.etag')->from('recognize_face_detections', 'rfd'); + $query->where($query->expr()->eq('rfd.cluster_id', $query->createNamedParameter($faceId))); + + // WHERE these photos are memories indexed + $query->innerJoin('rfd', 'memories', 'm', $query->expr()->eq('m.fileid', 'rfd.file_id')); + + // WHERE these photos are in the user's requested folder recursively + $query->innerJoin('m', 'filecache', 'f', $this->getFilecacheJoinQuery($query, $folder, true, false)); + + // MAX 4 results + $query->setMaxResults(4); + + // FETCH all face detections + $previews = $query->executeQuery()->fetchAll(); + + // Post-process, everthing is a number + foreach($previews as &$row) { + $row["fileid"] = intval($row["file_id"]); + unset($row["file_id"]); + $row["x"] = floatval($row["x"]); + $row["y"] = floatval($row["y"]); + $row["width"] = floatval($row["width"]); + $row["height"] = floatval($row["height"]); + } + + return $previews; + } +} \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index 872f1168..32e6376b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -19,6 +19,10 @@ :title="t('memories', 'Videos')">