Skip to content

Commit

Permalink
enh: Add MaintenanceJob to trigger stuck clustering
Browse files Browse the repository at this point in the history
Signed-off-by: Marcel Klehr <[email protected]>
  • Loading branch information
marcelklehr committed Sep 30, 2024
1 parent 5488116 commit 16be71f
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 59 deletions.
3 changes: 3 additions & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ The app does not send any sensitive data to cloud providers or similar services.
<dependencies>
<nextcloud min-version="30" max-version="31" />
</dependencies>
<background-jobs>
<job>OCA\Recognize\BackgroundJobs\MaintenanceJob</job>
</background-jobs>

<repair-steps>
<post-migration>
Expand Down
45 changes: 45 additions & 0 deletions lib/BackgroundJobs/MaintenanceJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php
/*
* Copyright (c) 2024 The Recognize contributors.
* This file is licensed under the Affero General Public License version 3 or later. See the COPYING file.
*/
declare(strict_types=1);
namespace OCA\Recognize\BackgroundJobs;

use OCA\Recognize\Db\FaceDetectionMapper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJobList;
use OCP\BackgroundJob\TimedJob;
use OCP\DB\Exception;
use Psr\Log\LoggerInterface;

class MaintenanceJob extends TimedJob {

public function __construct(
ITimeFactory $time,
private LoggerInterface $logger,
private IJobList $jobList,
private FaceDetectionMapper $faceDetectionMapper,
) {
parent::__construct($time);
$this->setInterval(60 * 60 * 12);
$this->setTimeSensitivity(self::TIME_INSENSITIVE);
}

/**
* @param mixed $argument
* @return void
*/
protected function run($argument) {
// Trigger clustering in case it's stuck
try {
$users = $this->faceDetectionMapper->getUsersForUnclustered();
} catch (Exception $e) {
$this->logger->error($e->getMessage(), ['exception' => $e]);
return;
}
foreach ($users as $userId) {
$this->jobList->add(ClusterFacesJob::class, ['userId' => $userId]);
}
}
}
118 changes: 59 additions & 59 deletions lib/Classifiers/Images/ClusteringFaceClassifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,12 @@ private function getUsersWithFileAccess(Node $node): array {
return array_values(array_unique($userIds));
}

/**
* @param string $user
* @param \OCA\Recognize\Db\QueueFile[] $queueFiles
* @return void
* @throws \ErrorException
*/
/**
* @param string $user
* @param \OCA\Recognize\Db\QueueFile[] $queueFiles
* @return void
* @throws \ErrorException
*/
public function classify(array $queueFiles): void {
if ($this->config->getAppValueString('tensorflow.purejs', 'false') === 'true') {
$timeout = self::IMAGE_PUREJS_TIMEOUT;
Expand Down Expand Up @@ -94,61 +94,61 @@ public function classify(array $queueFiles): void {
}

$usersToCluster = [];
try {
$classifierProcess = $this->classifyFiles(self::MODEL_NAME, $filteredQueueFiles, $timeout);
try {
$classifierProcess = $this->classifyFiles(self::MODEL_NAME, $filteredQueueFiles, $timeout);

/**
* @var list<array> $faces
*/
foreach ($classifierProcess as $queueFile => $faces) {
$this->logger->debug('Face results for ' . $queueFile->getFileId() . ' are in');
foreach ($faces as $face) {
if ($face['score'] < self::MIN_FACE_RECOGNITION_SCORE) {
$this->logger->debug('Face score too low. continuing with next face.');
continue;
}
if (abs($face['angle']['roll']) > self::MAX_FACE_ROLL || abs($face['angle']['yaw']) > self::MAX_FACE_YAW) {
$this->logger->debug('Face is not straight. continuing with next face.');
continue;
}
/**
* @var list<array> $faces
*/
foreach ($classifierProcess as $queueFile => $faces) {
$this->logger->debug('Face results for ' . $queueFile->getFileId() . ' are in');
foreach ($faces as $face) {
if ($face['score'] < self::MIN_FACE_RECOGNITION_SCORE) {
$this->logger->debug('Face score too low. continuing with next face.');
continue;
}
if (abs($face['angle']['roll']) > self::MAX_FACE_ROLL || abs($face['angle']['yaw']) > self::MAX_FACE_YAW) {
$this->logger->debug('Face is not straight. continuing with next face.');
continue;
}

try {
$node = $this->rootFolder->getFirstNodeById($queueFile->getFileId());
$userIds = $node !== null ? $this->getUsersWithFileAccess($node) : [];
} catch (InvalidPathException|NotFoundException $e) {
$userIds = [];
}
try {
$node = $this->rootFolder->getFirstNodeById($queueFile->getFileId());
$userIds = $node !== null ? $this->getUsersWithFileAccess($node) : [];
} catch (InvalidPathException|NotFoundException $e) {
$userIds = [];
}

// Insert face detection for all users with access
foreach ($userIds as $userId) {
$this->logger->debug('preparing face detection for user ' . $userId);
$faceDetection = new FaceDetection();
$faceDetection->setX($face['x']);
$faceDetection->setY($face['y']);
$faceDetection->setWidth($face['width']);
$faceDetection->setHeight($face['height']);
$faceDetection->setVector($face['vector']);
$faceDetection->setFileId($queueFile->getFileId());
$faceDetection->setUserId($userId);
try {
$this->faceDetections->insert($faceDetection);
} catch (Exception $e) {
$this->logger->error('Could not store face detection in database', ['exception' => $e]);
continue;
}
$usersToCluster[$userId] = true;
}
$this->config->setAppValueString(self::MODEL_NAME . '.status', 'true');
$this->config->setAppValueString(self::MODEL_NAME . '.lastFile', (string)time());
}
}
} finally {
$usersToCluster = array_keys($usersToCluster);
foreach ($usersToCluster as $userId) {
$this->logger->debug('scheduling ClusterFacesJob for user ' . $userId);
$this->jobList->add(ClusterFacesJob::class, ['userId' => $userId]);
}
$this->logger->debug('face classifier end');
}
// Insert face detection for all users with access
foreach ($userIds as $userId) {
$this->logger->debug('preparing face detection for user ' . $userId);
$faceDetection = new FaceDetection();
$faceDetection->setX($face['x']);
$faceDetection->setY($face['y']);
$faceDetection->setWidth($face['width']);
$faceDetection->setHeight($face['height']);
$faceDetection->setVector($face['vector']);
$faceDetection->setFileId($queueFile->getFileId());
$faceDetection->setUserId($userId);
try {
$this->faceDetections->insert($faceDetection);
} catch (Exception $e) {
$this->logger->error('Could not store face detection in database', ['exception' => $e]);
continue;
}
$usersToCluster[$userId] = true;
}
$this->config->setAppValueString(self::MODEL_NAME . '.status', 'true');
$this->config->setAppValueString(self::MODEL_NAME . '.lastFile', (string)time());
}
}
} finally {
$usersToCluster = array_keys($usersToCluster);
foreach ($usersToCluster as $userId) {
$this->logger->debug('scheduling ClusterFacesJob for user ' . $userId);
$this->jobList->add(ClusterFacesJob::class, ['userId' => $userId]);
}
$this->logger->debug('face classifier end');
}
}
}
18 changes: 18 additions & 0 deletions lib/Db/FaceDetectionMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,24 @@ public function countUnclustered(): int {
return (int) $count;
}

/**
* @return array<string>
* @throws \OCP\DB\Exception
*/
public function getUsersForUnclustered(): array {
$qb = $this->db->getQueryBuilder();
$qb->selectDistinct('user_id')
->from('recognize_face_detections')
->where($qb->expr()->isNull('cluster_id'))
->andWhere($qb->expr()->gte('height', $qb->createPositionalParameter(FaceClusterAnalyzer::MIN_DETECTION_SIZE)))
->andWhere($qb->expr()->gte('width', $qb->createPositionalParameter(FaceClusterAnalyzer::MIN_DETECTION_SIZE)));
$result = $qb->executeQuery();
/** @var array<string> $users */
$users = $result->fetchAll(\PDO::FETCH_COLUMN);
$result->closeCursor();
return $users;
}

protected function mapRowToEntity(array $row): Entity {
try {
return parent::mapRowToEntity($row);
Expand Down

0 comments on commit 16be71f

Please sign in to comment.