mirror of
https://github.com/nextcloud/recognize.git
synced 2026-01-13 20:25:35 +00:00
801 lines
31 KiB
PHP
801 lines
31 KiB
PHP
<?php
|
|
|
|
use OCA\Recognize\BackgroundJobs\ClassifyFacesJob;
|
|
use OCA\Recognize\BackgroundJobs\ClassifyImagenetJob;
|
|
use OCA\Recognize\BackgroundJobs\ClassifyLandmarksJob;
|
|
use OCA\Recognize\BackgroundJobs\ClassifyMovinetJob;
|
|
use OCA\Recognize\BackgroundJobs\ClassifyMusicnnJob;
|
|
use OCA\Recognize\BackgroundJobs\ClusterFacesJob;
|
|
use OCA\Recognize\BackgroundJobs\ProcessFsActionsJob;
|
|
use OCA\Recognize\BackgroundJobs\SchedulerJob;
|
|
use OCA\Recognize\BackgroundJobs\StorageCrawlJob;
|
|
use OCA\Recognize\Classifiers\Audio\MusicnnClassifier;
|
|
use OCA\Recognize\Classifiers\Classifier;
|
|
use OCA\Recognize\Classifiers\Images\ClusteringFaceClassifier;
|
|
use OCA\Recognize\Classifiers\Images\ImagenetClassifier;
|
|
use OCA\Recognize\Classifiers\Images\LandmarksClassifier;
|
|
use OCA\Recognize\Classifiers\Video\MovinetClassifier;
|
|
use OCA\Recognize\Db\FaceClusterMapper;
|
|
use OCA\Recognize\Db\FaceDetectionMapper;
|
|
use OCA\Recognize\Db\QueueFile;
|
|
use OCA\Recognize\Service\FaceClusterAnalyzer;
|
|
use OCA\Recognize\Service\Logger;
|
|
use OCA\Recognize\Service\QueueService;
|
|
use OCP\AppFramework\Services\IAppConfig;
|
|
use OCP\BackgroundJob\IJobList;
|
|
use OCP\Files\Folder;
|
|
use OCP\Files\IRootFolder;
|
|
use OCP\SystemTag\ISystemTagObjectMapper;
|
|
use Test\TestCase;
|
|
|
|
/**
|
|
* @group DB
|
|
*/
|
|
class ClassifierTest extends TestCase {
|
|
public const TEST_USER1 = 'test-user1';
|
|
public const TEST_USER2 = 'test-user2';
|
|
|
|
public const TEST_FILES = ['alpine.jpg' ,'eiffeltower.jpg', 'Rock_Rejam.mp3', 'jumpingjack.gif', 'test'];
|
|
public const ALL_MODELS = [
|
|
ClusteringFaceClassifier::MODEL_NAME,
|
|
ImagenetClassifier::MODEL_NAME,
|
|
LandmarksClassifier::MODEL_NAME,
|
|
MovinetClassifier::MODEL_NAME,
|
|
MusicnnClassifier::MODEL_NAME,
|
|
];
|
|
|
|
public const CLASSIFIERS = [
|
|
ClusteringFaceClassifier::MODEL_NAME => ClusteringFaceClassifier::class,
|
|
ImagenetClassifier::MODEL_NAME => ImagenetClassifier::class,
|
|
LandmarksClassifier::MODEL_NAME => LandmarksClassifier::class,
|
|
MovinetClassifier::MODEL_NAME => MovinetClassifier::class,
|
|
MusicnnClassifier::MODEL_NAME => MusicnnClassifier::class,
|
|
];
|
|
|
|
private Classifier $classifier;
|
|
private OCP\Files\File $testFile;
|
|
private IRootFolder $rootFolder;
|
|
private Folder $userFolder;
|
|
private QueueService $queue;
|
|
private FaceDetectionMapper $faceDetectionMapper;
|
|
private IJobList $jobList;
|
|
private IAppConfig $config;
|
|
private \OCP\Share\IManager $shareManager;
|
|
|
|
public static function setUpBeforeClass(): void {
|
|
parent::setUpBeforeClass();
|
|
$backend = new \Test\Util\User\Dummy();
|
|
$backend->createUser(self::TEST_USER1, self::TEST_USER1);
|
|
$backend->createUser(self::TEST_USER2, self::TEST_USER2);
|
|
\OC::$server->get(\OCP\IUserManager::class)->registerBackend($backend);
|
|
}
|
|
|
|
public function setUp(): void {
|
|
parent::setUp();
|
|
$this->rootFolder = \OC::$server->getRootFolder();
|
|
$this->userFolder = $this->loginAndGetUserFolder(self::TEST_USER1);
|
|
$this->faceDetectionMapper = \OC::$server->get(FaceDetectionMapper::class);
|
|
$logger = \OC::$server->get(Logger::class);
|
|
$cliOutput = $this->createMock(Symfony\Component\Console\Output\OutputInterface::class);
|
|
$cliOutput->method('writeln')
|
|
->willReturnCallback(fn ($msg) => print($msg."\n"));
|
|
$logger->setCliOutput($cliOutput);
|
|
$this->jobList = \OC::$server->get(IJobList::class);
|
|
$this->config = \OC::$server->getRegisteredAppContainer('recognize')->get(IAppConfig::class);
|
|
$this->queue = \OC::$server->get(QueueService::class);
|
|
$this->shareManager = \OC::$server->get(\OCP\Share\IManager::class);
|
|
foreach (self::TEST_FILES as $filename) {
|
|
try {
|
|
$this->userFolder->get($filename)->delete();
|
|
} catch (\OCP\Files\NotFoundException $e) {
|
|
// noop
|
|
}
|
|
}
|
|
\OCP\Server::get(\OCA\Files_Trashbin\Trashbin::class)->deleteAll();
|
|
$this->queue->clearQueue(ImagenetClassifier::MODEL_NAME);
|
|
$this->queue->clearQueue(LandmarksClassifier::MODEL_NAME);
|
|
$this->queue->clearQueue(MovinetClassifier::MODEL_NAME);
|
|
$this->queue->clearQueue(MusicnnClassifier::MODEL_NAME);
|
|
$this->config->setAppValueString('imagenet.enabled', 'false');
|
|
$this->config->setAppValueString('faces.enabled', 'false');
|
|
$this->config->setAppValueString('movinet.enabled', 'false');
|
|
$this->config->setAppValueString('musicnn.enabled', 'false');
|
|
|
|
$faceClusterAnalyzer = \OC::$server->get(FaceClusterAnalyzer::class);
|
|
$faceClusterAnalyzer->setMinDatasetSize(30);
|
|
}
|
|
|
|
public function testSchedulerJob() : void {
|
|
$this->testFile = $this->userFolder->newFile('/alpine.jpg', file_get_contents(__DIR__.'/res/alpine.JPG'));
|
|
|
|
/** @var SchedulerJob $scheduler */
|
|
$scheduler = \OC::$server->get(SchedulerJob::class);
|
|
|
|
$scheduler->setId(1);
|
|
$scheduler->setLastRun(0);
|
|
$scheduler->start($this->jobList);
|
|
|
|
$storageId = $this->testFile->getMountPoint()->getNumericStorageId();
|
|
$rootId = $this->testFile->getMountPoint()->getStorageRootId();
|
|
self::assertTrue($this->jobList->has(StorageCrawlJob::class, [
|
|
'storage_id' => $storageId,
|
|
'root_id' => $rootId,
|
|
'override_root' => $this->userFolder->getId(),
|
|
'last_file_id' => 0,
|
|
'models' => self::ALL_MODELS,
|
|
]));
|
|
|
|
// cleanup
|
|
$this->jobList->remove(StorageCrawlJob::class, [
|
|
'storage_id' => $storageId,
|
|
'root_id' => $rootId,
|
|
'override_root' => $this->userFolder->getId(),
|
|
'last_file_id' => 0,
|
|
'models' => self::ALL_MODELS,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider ignoreImageFilesProvider
|
|
* @return void
|
|
* @throws \OCP\DB\Exception
|
|
* @throws \OCP\Files\InvalidPathException
|
|
* @throws \OCP\Files\NotFoundException
|
|
* @throws \OCP\Files\NotPermittedException
|
|
*/
|
|
public function testFileListener(string $ignoreFileName) : void {
|
|
$this->config->setAppValueString('imagenet.enabled', 'true');
|
|
$this->queue->clearQueue(ImagenetClassifier::MODEL_NAME);
|
|
|
|
$this->testFile = $this->userFolder->newFile('/alpine.jpg', file_get_contents(__DIR__.'/res/alpine.JPG'));
|
|
$this->userFolder->newFolder('/test/ignore/');
|
|
$ignoreFile = $this->userFolder->newFile('/test/' . $ignoreFileName, '');
|
|
$this->ignoredFile = $this->userFolder->newFile('/test/ignore/alpine-2.jpg', file_get_contents(__DIR__.'/res/alpine.JPG'));
|
|
|
|
$storageId = $this->testFile->getMountPoint()->getNumericStorageId();
|
|
$rootId = $this->testFile->getMountPoint()->getStorageRootId();
|
|
|
|
var_dump([ 'testFile' => $this->testFile->getId(), 'ignoredFile' => $this->ignoredFile->getId() ]);
|
|
|
|
$this->runFsActionJobs();
|
|
$queue = $this->queue->getFromQueue(ImagenetClassifier::MODEL_NAME, $storageId, $rootId, 100);
|
|
self::assertCount(1, $this->queue->getFromQueue(ImagenetClassifier::MODEL_NAME, $storageId, $rootId, 100), 'one element should have been added to imagenet queue: ' . var_export($queue, true) . var_export(array_map(fn (QueueFile $queueFile) => $this->rootFolder->getFirstNodeById($queueFile->getFileId())?->getPath(), $queue), true));
|
|
|
|
$this->testFile->delete();
|
|
|
|
$this->runFsActionJobs();
|
|
self::assertCount(0, $this->queue->getFromQueue(ImagenetClassifier::MODEL_NAME, $storageId, $rootId, 100), 'entry should have been removed from imagenet queue');
|
|
|
|
$ignoreFile->delete();
|
|
|
|
$this->runFsActionJobs();
|
|
self::assertCount(1, $this->queue->getFromQueue(ImagenetClassifier::MODEL_NAME, $storageId, $rootId, 100), 'one element should have been added to imagenet queue after deleting ignore file');
|
|
|
|
$ignoreFile = $this->userFolder->newFile('/test/' . $ignoreFileName, '');
|
|
|
|
$this->runFsActionJobs();
|
|
self::assertCount(0, $this->queue->getFromQueue(ImagenetClassifier::MODEL_NAME, $storageId, $rootId, 100), 'entry should have been removed from imagenet queue after creating ignore file');
|
|
|
|
$this->ignoredFile->move($this->userFolder->getPath() . '/alpine-2.jpg');
|
|
|
|
$this->runFsActionJobs();
|
|
self::assertCount(1, $this->queue->getFromQueue(ImagenetClassifier::MODEL_NAME, $storageId, $rootId, 100), 'one element should have been added to imagenet queue after moving it out of ignored territory');
|
|
|
|
$ignoreFile->move($this->userFolder->getPath() . '/' . $ignoreFileName);
|
|
|
|
$this->runFsActionJobs();
|
|
self::assertCount(0, $this->queue->getFromQueue(ImagenetClassifier::MODEL_NAME, $storageId, $rootId, 100), 'entry should have been removed from imagenet queue after moving ignore file');
|
|
|
|
$ignoreFile->move($this->userFolder->getPath() . '/test/' . $ignoreFileName);
|
|
|
|
$this->runFsActionJobs();
|
|
self::assertCount(1, $this->queue->getFromQueue(ImagenetClassifier::MODEL_NAME, $storageId, $rootId, 100), 'one element should have been added to imagenet queue after moving ignore file');
|
|
|
|
$this->ignoredFile->move($this->userFolder->getPath() . '/test/ignore/alpine-2.jpg');
|
|
|
|
$this->runFsActionJobs();
|
|
self::assertCount(0, $this->queue->getFromQueue(ImagenetClassifier::MODEL_NAME, $storageId, $rootId, 100), 'entry should have been removed from imagenet queue after moving it into ignored territory');
|
|
}
|
|
|
|
private function runFsActionJobs() {
|
|
while ($job = $this->jobList->getNext(jobClasses:[ProcessFsActionsJob::class])) {
|
|
$this->jobList->resetBackgroundJob($job);
|
|
$job->start($this->jobList);
|
|
$this->jobList->resetBackgroundJob($job);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dataProvider ignoreImageFilesProvider
|
|
* @return void
|
|
* @throws \OCP\DB\Exception
|
|
* @throws \OCP\Files\InvalidPathException
|
|
* @throws \OCP\Files\NotFoundException
|
|
* @throws \OCP\Files\NotPermittedException
|
|
*/
|
|
public function testFileListenerWithFolder(string $ignoreFileName) : void {
|
|
$this->config->setAppValueString('imagenet.enabled', 'true');
|
|
$this->queue->clearQueue(ImagenetClassifier::MODEL_NAME);
|
|
|
|
$folder = $this->userFolder->newFolder('/folder/');
|
|
$this->testFile = $folder->newFile('/alpine.jpg', file_get_contents(__DIR__.'/res/alpine.JPG'));
|
|
$ignoreFolder = $this->userFolder->newFolder('/test/ignore/');
|
|
$ignoredFolder = $ignoreFolder->newFolder('/folder/');
|
|
$ignoreFile = $this->userFolder->newFile('/test/' . $ignoreFileName, '');
|
|
$this->ignoredFile = $ignoredFolder->newFile('/alpine-2.jpg', file_get_contents(__DIR__.'/res/alpine.JPG'));
|
|
|
|
$storageId = $this->testFile->getMountPoint()->getNumericStorageId();
|
|
$rootId = $this->testFile->getMountPoint()->getStorageRootId();
|
|
|
|
$this->runFsActionJobs();
|
|
self::assertCount(1, $this->queue->getFromQueue(ImagenetClassifier::MODEL_NAME, $storageId, $rootId, 100), 'one element should have been added to imagenet queue');
|
|
|
|
$folder->delete();
|
|
|
|
$this->runFsActionJobs();
|
|
self::assertCount(0, $this->queue->getFromQueue(ImagenetClassifier::MODEL_NAME, $storageId, $rootId, 100), 'entry should have been removed from imagenet queue');
|
|
|
|
$ignoreFile->delete();
|
|
|
|
$this->runFsActionJobs();
|
|
self::assertCount(1, $this->queue->getFromQueue(ImagenetClassifier::MODEL_NAME, $storageId, $rootId, 100), 'one element should have been added to imagenet queue after deleting ignore file');
|
|
|
|
$ignoreFile = $this->userFolder->newFile('/test/' . $ignoreFileName, '');
|
|
|
|
$this->runFsActionJobs();
|
|
self::assertCount(0, $this->queue->getFromQueue(ImagenetClassifier::MODEL_NAME, $storageId, $rootId, 100), 'entry should have been removed from imagenet queue after creating ignore file');
|
|
|
|
$ignoredFolder->move($this->userFolder->getPath() . '/folder2');
|
|
|
|
$this->runFsActionJobs();
|
|
self::assertCount(1, $this->queue->getFromQueue(ImagenetClassifier::MODEL_NAME, $storageId, $rootId, 100), 'one element should have been added to imagenet queue after moving it out of ignored territory');
|
|
|
|
$ignoreFile->move($this->userFolder->getPath() . '/' . $ignoreFileName);
|
|
|
|
$this->runFsActionJobs();
|
|
self::assertCount(0, $this->queue->getFromQueue(ImagenetClassifier::MODEL_NAME, $storageId, $rootId, 100), 'entry should have been removed from imagenet queue after moving ignore file');
|
|
|
|
$ignoreFile->move($this->userFolder->getPath() . '/test/' . $ignoreFileName);
|
|
|
|
$this->runFsActionJobs();
|
|
self::assertCount(1, $this->queue->getFromQueue(ImagenetClassifier::MODEL_NAME, $storageId, $rootId, 100), 'one element should have been added to imagenet queue after moving ignore file');
|
|
|
|
$ignoredFolder->move($this->userFolder->getPath() . '/test/ignore/folder');
|
|
|
|
$this->runFsActionJobs();
|
|
self::assertCount(0, $this->queue->getFromQueue(ImagenetClassifier::MODEL_NAME, $storageId, $rootId, 100), 'entry should have been removed from imagenet queue after moving it into ignored territory');
|
|
}
|
|
|
|
/**
|
|
* @dataProvider ignoreImageFilesProvider
|
|
* @return void
|
|
* @throws \OCP\Files\InvalidPathException
|
|
* @throws \OCP\Files\NotFoundException
|
|
*/
|
|
public function testClassifyCommand(string $ignoreFileName) : void {
|
|
$this->queue->clearQueue(ImagenetClassifier::MODEL_NAME);
|
|
|
|
$this->testFile = $this->userFolder->newFile('/alpine.jpg', file_get_contents(__DIR__.'/res/alpine.JPG'));
|
|
$this->userFolder->newFolder('/test/ignore/');
|
|
$this->userFolder->newFile('/test/' . $ignoreFileName, '');
|
|
$this->ignoredFile = $this->userFolder->newFile('/test/ignore/alpine.jpg', file_get_contents(__DIR__.'/res/alpine.JPG'));
|
|
|
|
/** @var \OCA\Recognize\Command\Classify $classifyCommand */
|
|
$classifyCommand = \OC::$server->get(\OCA\Recognize\Command\Classify::class);
|
|
|
|
$cliOutput = $this->createMock(Symfony\Component\Console\Output\OutputInterface::class);
|
|
$cliOutput->method('writeln')
|
|
->willReturnCallback(fn ($msg) => print($msg."\n"));
|
|
$cliInput = $this->createMock(Symfony\Component\Console\Input\InputInterface::class);
|
|
|
|
$this->config->setAppValueString('imagenet.enabled', 'true');
|
|
$classifyCommand->run($cliInput, $cliOutput);
|
|
|
|
/** @var \OCP\SystemTag\ISystemTagObjectMapper $objectMapper */
|
|
$objectMapper = \OC::$server->get(ISystemTagObjectMapper::class);
|
|
/** @var \OCP\SystemTag\ISystemTagManager $tagManager */
|
|
$tagManager = \OC::$server->get(OCP\SystemTag\ISystemTagManager::class);
|
|
|
|
self::assertTrue(
|
|
$objectMapper->haveTag(
|
|
(string)$this->testFile->getId(),
|
|
'files',
|
|
$tagManager->getTag('Alpine', true, true)->getId()
|
|
),
|
|
'Correct tag should have been set on image file'
|
|
);
|
|
|
|
self::assertFalse(
|
|
$objectMapper->haveTag(
|
|
(string)$this->ignoredFile->getId(),
|
|
'files',
|
|
$tagManager->getTag('Alpine', true, true)->getId()
|
|
),
|
|
'Correct tag should not have been set on ignored image file'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider ignoreImageFilesProvider
|
|
* @return void
|
|
* @throws \OCP\DB\Exception
|
|
* @throws \OCP\Files\InvalidPathException
|
|
* @throws \OCP\Files\NotFoundException
|
|
* @throws \OCP\Files\NotPermittedException
|
|
* @throws \Psr\Container\ContainerExceptionInterface
|
|
* @throws \Psr\Container\NotFoundExceptionInterface
|
|
*/
|
|
public function testImagenetPipeline(string $ignoreFileName) : void {
|
|
$this->testFile = $this->userFolder->newFile('/alpine.jpg', file_get_contents(__DIR__.'/res/alpine.JPG'));
|
|
$this->userFolder->newFolder('/test/ignore/');
|
|
$this->ignoredFile = $this->userFolder->newFile('/test/ignore/alpine.jpg', file_get_contents(__DIR__.'/res/alpine.JPG'));
|
|
$this->userFolder->newFile('/test/' . $ignoreFileName, '');
|
|
|
|
$this->config->setAppValueString('imagenet.enabled', 'true');
|
|
|
|
/** @var StorageCrawlJob $scheduler */
|
|
$crawler = \OC::$server->get(StorageCrawlJob::class);
|
|
/** @var IJobList $this->jobList */
|
|
$this->jobList = \OC::$server->get(IJobList::class);
|
|
/** @var QueueService $queue */
|
|
|
|
$storageId = $this->testFile->getMountPoint()->getNumericStorageId();
|
|
$rootId = $this->testFile->getMountPoint()->getStorageRootId();
|
|
|
|
self::assertCount(0, $this->queue->getFromQueue(ImagenetClassifier::MODEL_NAME, $storageId, $rootId, 100), 'imagenet queue should be empty initially');
|
|
|
|
$crawler->setArgument([
|
|
'storage_id' => $storageId,
|
|
'root_id' => $rootId,
|
|
'override_root' => $this->userFolder->getId(),
|
|
'last_file_id' => 0,
|
|
'models' => self::ALL_MODELS,
|
|
]);
|
|
$crawler->setId(1);
|
|
$crawler->setLastRun(0);
|
|
$crawler->start($this->jobList);
|
|
|
|
// clean up
|
|
$this->jobList->remove(StorageCrawlJob::class);
|
|
|
|
self::assertTrue($this->jobList->has(ClassifyImagenetJob::class, [
|
|
'storageId' => $storageId,
|
|
'rootId' => $rootId
|
|
]), 'ClassifyImagenetJob should have been scheduled');
|
|
|
|
self::assertCount(1,
|
|
$this->queue->getFromQueue(ImagenetClassifier::MODEL_NAME, $storageId, $rootId, 100),
|
|
'imagenet queue should contain exactly one image');
|
|
|
|
list($classifier) = $this->jobList->getJobs(ClassifyImagenetJob::class, 1, 0);
|
|
$classifier->start($this->jobList);
|
|
|
|
/** @var \OCP\SystemTag\ISystemTagObjectMapper $objectMapper */
|
|
$objectMapper = \OC::$server->get(ISystemTagObjectMapper::class);
|
|
/** @var \OCP\SystemTag\ISystemTagManager $tagManager */
|
|
$tagManager = \OC::$server->get(OCP\SystemTag\ISystemTagManager::class);
|
|
|
|
self::assertTrue(
|
|
$objectMapper->haveTag(
|
|
(string)$this->testFile->getId(),
|
|
'files',
|
|
$tagManager->getTag('Alpine', true, true)->getId()
|
|
),
|
|
'Correct tag should have been set on image file.'
|
|
);
|
|
|
|
self::assertFalse(
|
|
$objectMapper->haveTag(
|
|
(string)$this->ignoredFile->getId(),
|
|
'files',
|
|
$tagManager->getTag('Alpine', true, true)->getId()
|
|
),
|
|
'Correct tag should not have been set on ignored image file'
|
|
);
|
|
}
|
|
|
|
public function testLandmarksPipeline() : void {
|
|
if ($this->config->getAppValueString('tensorflow.purejs', 'false') === 'true') {
|
|
// landmarks will fail with purejs/WASM mode, sadly, because we use a worse imagenet model in WASM mode
|
|
self::markTestSkipped();
|
|
}
|
|
$this->testFile = $this->userFolder->newFile('/eiffeltower.jpg', file_get_contents(__DIR__.'/res/eiffeltower.jpg'));
|
|
$this->config->setAppValueString('imagenet.enabled', 'true');
|
|
$this->config->setAppValueString('landmarks.enabled', 'true');
|
|
/** @var StorageCrawlJob $scheduler */
|
|
$crawler = \OC::$server->get(StorageCrawlJob::class);
|
|
/** @var IJobList $this->jobList */
|
|
$this->jobList = \OC::$server->get(IJobList::class);
|
|
|
|
$storageId = $this->testFile->getMountPoint()->getNumericStorageId();
|
|
$rootId = $this->testFile->getMountPoint()->getStorageRootId();
|
|
|
|
self::assertCount(0,
|
|
$this->queue->getFromQueue(ImagenetClassifier::MODEL_NAME, $storageId, $rootId, 100),
|
|
'imagenet Queue should be empty initially');
|
|
self::assertCount(0,
|
|
$this->queue->getFromQueue(LandmarksClassifier::MODEL_NAME, $storageId, $rootId, 100),
|
|
'landmarks Queue should be empty initially');
|
|
|
|
$crawler->setArgument([
|
|
'storage_id' => $storageId,
|
|
'root_id' => $rootId,
|
|
'override_root' => $this->userFolder->getId(),
|
|
'last_file_id' => 0,
|
|
'models' => self::ALL_MODELS,
|
|
]);
|
|
$crawler->setId(1);
|
|
$crawler->setLastRun(0);
|
|
$crawler->start($this->jobList);
|
|
|
|
// clean up
|
|
$this->jobList->remove(StorageCrawlJob::class);
|
|
|
|
self::assertTrue($this->jobList->has(ClassifyImagenetJob::class, [
|
|
'storageId' => $storageId,
|
|
'rootId' => $rootId
|
|
]), 'ClassifyImagenetJob should have been created');
|
|
|
|
self::assertCount(0,
|
|
$this->queue->getFromQueue(LandmarksClassifier::MODEL_NAME, $storageId, $rootId, 100),
|
|
'landmarks queue should still be empty after StorageCrawlJob');
|
|
self::assertCount(1,
|
|
$this->queue->getFromQueue(ImagenetClassifier::MODEL_NAME, $storageId, $rootId, 100),
|
|
'imagenet queue should contain image');
|
|
|
|
list($classifier, ) = $this->jobList->getJobs(ClassifyImagenetJob::class, 1, 0);
|
|
$classifier->start($this->jobList);
|
|
|
|
self::assertTrue($this->jobList->has(ClassifyLandmarksJob::class, [
|
|
'storageId' => $storageId,
|
|
'rootId' => $rootId
|
|
]), 'ClassifyLandmarksJob should have been scheduled');
|
|
|
|
self::assertCount(1,
|
|
$this->queue->getFromQueue(LandmarksClassifier::MODEL_NAME, $storageId, $rootId, 100),
|
|
'landmarks queue should contain image');
|
|
|
|
list($classifier, ) = $this->jobList->getJobs(ClassifyLandmarksJob::class, 1, 0);
|
|
$classifier->start($this->jobList);
|
|
|
|
/** @var \OCP\SystemTag\ISystemTagObjectMapper $objectMapper */
|
|
$objectMapper = \OC::$server->get(ISystemTagObjectMapper::class);
|
|
/** @var \OCP\SystemTag\ISystemTagManager $tagManager */
|
|
$tagManager = \OC::$server->get(OCP\SystemTag\ISystemTagManager::class);
|
|
|
|
self::assertTrue(
|
|
$objectMapper->haveTag(
|
|
(string)$this->testFile->getId(),
|
|
'files',
|
|
$tagManager->getTag('Eiffel Tower', true, true)->getId()
|
|
),
|
|
'Correct Tag should have been set on image file'
|
|
);
|
|
}
|
|
|
|
public function testFacesPipeline() : void {
|
|
// Upload FaceID files
|
|
$testFiles = [];
|
|
$personToFiles = [];
|
|
foreach (['Nguyen_Ngoc_Nghia', 'Sao_Mai'] as $person) {
|
|
$personToFiles[$person] = [];
|
|
$it = new RecursiveDirectoryIterator(__DIR__ . '/res/FaceID-550/'.$person, RecursiveDirectoryIterator::SKIP_DOTS);
|
|
$files = new RecursiveIteratorIterator($it,
|
|
RecursiveIteratorIterator::CHILD_FIRST);
|
|
foreach ($files as $file) {
|
|
if (!$file->isDir()) {
|
|
$personToFiles[$person][] = $testFiles[] = $this->userFolder->newFile('/' . $file->getBasename(), file_get_contents($file->getRealPath()));
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->config->setAppValueString('faces.enabled', 'true');
|
|
|
|
/** @var StorageCrawlJob $scheduler */
|
|
$crawler = \OC::$server->get(StorageCrawlJob::class);
|
|
/** @var IJobList $this->jobList */
|
|
$this->jobList = \OC::$server->get(IJobList::class);
|
|
|
|
$storageId = $testFiles[0]->getMountPoint()->getNumericStorageId();
|
|
$rootId = $testFiles[0]->getMountPoint()->getStorageRootId();
|
|
|
|
self::assertCount(0,
|
|
$this->queue->getFromQueue(ClusteringFaceClassifier::MODEL_NAME, $storageId, $rootId, 100),
|
|
'faces Queue should be empty initially');
|
|
|
|
$crawler->setArgument([
|
|
'storage_id' => $storageId,
|
|
'root_id' => $rootId,
|
|
'override_root' => $this->userFolder->getId(),
|
|
'last_file_id' => 0,
|
|
'models' => self::ALL_MODELS,
|
|
]);
|
|
$crawler->setId(1);
|
|
$crawler->setLastRun(0);
|
|
$crawler->start($this->jobList);
|
|
|
|
while (count($jobs = $this->jobList->getJobs(StorageCrawlJob::class, 1, 0)) > 0) {
|
|
list($crawler) = $jobs;
|
|
$crawler->start($this->jobList);
|
|
}
|
|
|
|
self::assertTrue($this->jobList->has(ClassifyFacesJob::class, [
|
|
'storageId' => $storageId,
|
|
'rootId' => $rootId
|
|
]), 'ClassifyFacesJob should have been created');
|
|
|
|
self::assertCount(count($testFiles),
|
|
$this->queue->getFromQueue(ClusteringFaceClassifier::MODEL_NAME, $storageId, $rootId, 500),
|
|
'faces queue should contain images');
|
|
|
|
list($classifier, ) = $this->jobList->getJobs(ClassifyFacesJob::class, 1, 0);
|
|
$classifier->start($this->jobList);
|
|
|
|
self::assertGreaterThan(10,
|
|
count($this->faceDetectionMapper->findByUserId(self::TEST_USER1)),
|
|
'at least 10 face detections should have been created');
|
|
|
|
self::assertTrue($this->jobList->has(ClusterFacesJob::class, [
|
|
'userId' => self::TEST_USER1,
|
|
]), 'ClusterFacesJob should have been scheduled');
|
|
|
|
list($clusterer, ) = $this->jobList->getJobs(ClusterFacesJob::class, 1, 0);
|
|
$clusterer->start($this->jobList);
|
|
|
|
/** @var FaceClusterMapper $clusterMapper */
|
|
$clusterMapper = \OC::$server->get(FaceClusterMapper::class);
|
|
|
|
/** @var \OCA\Recognize\Db\FaceCluster[] $clusters */
|
|
$clusters = $clusterMapper->findByUserId(self::TEST_USER1);
|
|
self::assertGreaterThan(2, $clusters, 'at least 2 clusters should have been created');
|
|
|
|
$personToFileIds = [];
|
|
foreach ($personToFiles as $person => $files) {
|
|
$personToFileIds[$person] = array_map(fn ($file) => $file->getId(), $files);
|
|
}
|
|
|
|
foreach ($clusters as $cluster) {
|
|
/** @var \OCA\Recognize\Db\FaceDetection[] $faces */
|
|
$faces = $this->faceDetectionMapper->findByClusterId($cluster->getId());
|
|
$currentClusterPerson = null;
|
|
foreach ($faces as $face) {
|
|
$persons = array_keys(array_filter($personToFileIds, fn ($fileIds) => in_array($face->getFileId(), $fileIds)));
|
|
if (count($persons) > 0) {
|
|
if ($currentClusterPerson === null) {
|
|
$currentClusterPerson = $persons[0];
|
|
} else {
|
|
self::assertEquals($currentClusterPerson, $persons[0], 'All fotos of the same person should be in the same cluster');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @dataProvider ignoreVideoFilesProvider
|
|
* @return void
|
|
* @throws \OCP\DB\Exception
|
|
* @throws \OCP\Files\InvalidPathException
|
|
* @throws \OCP\Files\NotFoundException
|
|
* @throws \OCP\Files\NotPermittedException
|
|
* @throws \Psr\Container\ContainerExceptionInterface
|
|
* @throws \Psr\Container\NotFoundExceptionInterface
|
|
*/
|
|
public function testMovinetPipeline(string $ignoreFileName) : void {
|
|
if ($this->config->getAppValueString('tensorflow.purejs', 'false') === 'true') {
|
|
// Cannot run musicnn with purejs/WASM mode
|
|
self::markTestSkipped();
|
|
}
|
|
$this->testFile = $this->userFolder->newFile('/jumpingjack.gif', file_get_contents(__DIR__.'/res/jumpingjack.gif'));
|
|
$this->userFolder->newFolder('/test/ignore/');
|
|
$this->ignoredFile = $this->userFolder->newFile('/test/ignore/jumpingjack.gif', file_get_contents(__DIR__.'/res/jumpingjack.gif'));
|
|
$this->userFolder->newFile('/test/' . $ignoreFileName, '');
|
|
$this->config->setAppValueString('movinet.enabled', 'true');
|
|
/** @var StorageCrawlJob $scheduler */
|
|
$crawler = \OC::$server->get(StorageCrawlJob::class);
|
|
|
|
$storageId = $this->testFile->getMountPoint()->getNumericStorageId();
|
|
$rootId = $this->testFile->getMountPoint()->getStorageRootId();
|
|
|
|
self::assertCount(0,
|
|
$this->queue->getFromQueue(MovinetClassifier::MODEL_NAME, $storageId, $rootId, 100),
|
|
'movinet queue should be empty initially');
|
|
|
|
$crawler->setArgument([
|
|
'storage_id' => $storageId,
|
|
'root_id' => $rootId,
|
|
'override_root' => $this->userFolder->getId(),
|
|
'last_file_id' => 0,
|
|
'models' => self::ALL_MODELS,
|
|
]);
|
|
$crawler->setId(1);
|
|
$crawler->setLastRun(0);
|
|
$crawler->start($this->jobList);
|
|
|
|
while (count($jobs = $this->jobList->getJobs(StorageCrawlJob::class, 1, 0)) > 0) {
|
|
list($crawler) = $jobs;
|
|
$crawler->start($this->jobList);
|
|
}
|
|
|
|
self::assertTrue($this->jobList->has(ClassifyMovinetJob::class, [
|
|
'storageId' => $storageId,
|
|
'rootId' => $rootId
|
|
]), 'ClassifyMovinetJob should have been scheduled');
|
|
|
|
self::assertCount(1,
|
|
$this->queue->getFromQueue(MovinetClassifier::MODEL_NAME, $storageId, $rootId, 100),
|
|
'movinet queue should contain exactly one entry');
|
|
|
|
list($classifier) = $this->jobList->getJobs(ClassifyMovinetJob::class, 1, 0);
|
|
$classifier->start($this->jobList);
|
|
|
|
/** @var \OCP\SystemTag\ISystemTagObjectMapper $objectMapper */
|
|
$objectMapper = \OC::$server->get(ISystemTagObjectMapper::class);
|
|
/** @var \OCP\SystemTag\ISystemTagManager $tagManager */
|
|
$tagManager = \OC::$server->get(OCP\SystemTag\ISystemTagManager::class);
|
|
|
|
self::assertTrue(
|
|
$objectMapper->haveTag(
|
|
(string)$this->testFile->getId(),
|
|
'files',
|
|
$tagManager->getTag('jumping jacks', true, true)->getId()
|
|
),
|
|
'Correct tag should have been set on gif file'
|
|
);
|
|
|
|
self::assertFalse(
|
|
$objectMapper->haveTag(
|
|
(string)$this->ignoredFile->getId(),
|
|
'files',
|
|
$tagManager->getTag('jumping jacks', true, true)->getId()
|
|
),
|
|
'Correct tag should not have been set on ignored file'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider ignoreAudioFilesProvider
|
|
* @return void
|
|
* @throws \OCP\DB\Exception
|
|
* @throws \OCP\Files\InvalidPathException
|
|
* @throws \OCP\Files\NotFoundException
|
|
* @throws \OCP\Files\NotPermittedException
|
|
* @throws \Psr\Container\ContainerExceptionInterface
|
|
* @throws \Psr\Container\NotFoundExceptionInterface
|
|
*/
|
|
public function testMusicnnPipeline(string $ignoreFileName) : void {
|
|
if ($this->config->getAppValueString('tensorflow.purejs', 'false') === 'true') {
|
|
// Cannot run musicnn with purejs/WASM mode
|
|
self::markTestSkipped();
|
|
}
|
|
$this->testFile = $this->userFolder->newFile('/Rock_Rejam.mp3', file_get_contents(__DIR__.'/res/Rock_Rejam.mp3'));
|
|
$this->userFolder->newFolder('/test/ignore/');
|
|
$this->ignoredFile = $this->userFolder->newFile('/test/ignore/Rock_Rejam.mp3', file_get_contents(__DIR__.'/res/Rock_Rejam.mp3'));
|
|
$this->userFolder->newFile('/test/' . $ignoreFileName, '');
|
|
$this->config->setAppValueString('musicnn.enabled', 'true');
|
|
/** @var StorageCrawlJob $scheduler */
|
|
$crawler = \OC::$server->get(StorageCrawlJob::class);
|
|
/** @var IJobList $this->jobList */
|
|
$this->jobList = \OC::$server->get(IJobList::class);
|
|
|
|
$storageId = $this->testFile->getMountPoint()->getNumericStorageId();
|
|
$rootId = $this->testFile->getMountPoint()->getStorageRootId();
|
|
|
|
self::assertCount(0,
|
|
$this->queue->getFromQueue(MusicnnClassifier::MODEL_NAME, $storageId, $rootId, 100),
|
|
'musicnn queue should be empty initially');
|
|
|
|
$crawler->setArgument([
|
|
'storage_id' => $storageId,
|
|
'root_id' => $rootId,
|
|
'override_root' => $this->userFolder->getId(),
|
|
'last_file_id' => 0,
|
|
'models' => self::ALL_MODELS,
|
|
]);
|
|
$crawler->setId(1);
|
|
$crawler->setLastRun(0);
|
|
$crawler->start($this->jobList);
|
|
|
|
while (count($jobs = $this->jobList->getJobs(StorageCrawlJob::class, 1, 0)) > 0) {
|
|
list($crawler) = $jobs;
|
|
$crawler->start($this->jobList);
|
|
}
|
|
|
|
self::assertTrue($this->jobList->has(ClassifyMusicnnJob::class, [
|
|
'storageId' => $storageId,
|
|
'rootId' => $rootId
|
|
]), 'ClassifyMovinetJob should have been scheduled');
|
|
|
|
self::assertCount(1,
|
|
$this->queue->getFromQueue(MusicnnClassifier::MODEL_NAME, $storageId, $rootId, 100),
|
|
'movinet queue should contain exactly one entry');
|
|
|
|
list($classifier) = $this->jobList->getJobs(ClassifyMusicnnJob::class, 1, 0);
|
|
$classifier->start($this->jobList);
|
|
|
|
/** @var \OCP\SystemTag\ISystemTagObjectMapper $objectMapper */
|
|
$objectMapper = \OC::$server->get(ISystemTagObjectMapper::class);
|
|
/** @var \OCP\SystemTag\ISystemTagManager $tagManager */
|
|
$tagManager = \OC::$server->get(OCP\SystemTag\ISystemTagManager::class);
|
|
|
|
self::assertTrue(
|
|
$objectMapper->haveTag(
|
|
(string)$this->testFile->getId(),
|
|
'files',
|
|
$tagManager->getTag('electronic', true, true)->getId()
|
|
),
|
|
'Correct tag should have been set on gif file'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider classifierFilesProvider
|
|
* @return void
|
|
* @throws \OCP\Files\InvalidPathException
|
|
* @throws \OCP\Files\NotFoundException
|
|
* @throws \OCP\Files\NotPermittedException
|
|
*/
|
|
public function testClassifier($file, $model, $tag) : void {
|
|
if ($this->config->getAppValueString('tensorflow.purejs', 'false') === 'true' && in_array($model, ['movinet', 'musicnn'])) {
|
|
// Cannot run musicnn/movinet with purejs/WASM mode
|
|
self::markTestSkipped();
|
|
}
|
|
$this->classifier = \OC::$server->get(self::CLASSIFIERS[$model]);
|
|
$this->testFile = $this->userFolder->newFile('/'.$file, file_get_contents(__DIR__.'/res/'.$file));
|
|
$queueFile = new QueueFile();
|
|
$queueFile->setFileId($this->testFile->getId());
|
|
$queueFile->setId(1);
|
|
$generator = $this->classifier->classifyFiles($model, [$queueFile], 200);
|
|
$classifications = iterator_to_array($generator, false);
|
|
self::assertCount(1, $classifications);
|
|
self::assertContains($tag, $classifications[0]);
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function classifierFilesProvider() {
|
|
return [
|
|
['alpine.JPG', ImagenetClassifier::MODEL_NAME, 'Alpine'],
|
|
['eiffeltower.jpg', LandmarksClassifier::MODEL_NAME, 'Eiffel Tower'],
|
|
['Rock_Rejam.mp3', MusicnnClassifier::MODEL_NAME, 'electronic'],
|
|
['jumpingjack.gif', MovinetClassifier::MODEL_NAME, 'jumping jacks']
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function ignoreAudioFilesProvider() {
|
|
return [
|
|
['.nomusic'],
|
|
['.nomedia']
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function ignoreVideoFilesProvider() {
|
|
return [
|
|
['.novideo'],
|
|
['.nomedia']
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function ignoreImageFilesProvider() {
|
|
return [
|
|
['.noimage'],
|
|
['.nomedia']
|
|
];
|
|
}
|
|
|
|
private function loginAndGetUserFolder(string $userId) {
|
|
$this->loginAsUser($userId);
|
|
return $this->rootFolder->getUserFolder($userId);
|
|
}
|
|
}
|