2024-03-21 12:59:49 +01:00
|
|
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
|
|
|
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
2024-04-19 03:37:55 +02:00
|
|
|
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
2024-04-16 23:30:31 +02:00
|
|
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
2024-03-21 12:59:49 +01:00
|
|
|
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
|
|
|
import { ISearchRepository } from 'src/interfaces/search.interface';
|
2024-05-16 00:58:23 +02:00
|
|
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
2024-03-21 00:07:30 +01:00
|
|
|
import { SmartInfoService } from 'src/services/smart-info.service';
|
2024-03-21 04:15:09 +01:00
|
|
|
import { getCLIPModelInfo } from 'src/utils/misc';
|
2024-03-20 19:32:04 +01:00
|
|
|
import { assetStub } from 'test/fixtures/asset.stub';
|
2024-05-16 00:58:23 +02:00
|
|
|
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
2024-03-20 19:32:04 +01:00
|
|
|
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
|
|
|
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
|
|
|
|
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
2024-04-16 23:30:31 +02:00
|
|
|
import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock';
|
2024-03-20 19:32:04 +01:00
|
|
|
import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock';
|
|
|
|
import { newSearchRepositoryMock } from 'test/repositories/search.repository.mock';
|
2024-05-16 00:58:23 +02:00
|
|
|
import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock';
|
2024-04-16 16:44:45 +02:00
|
|
|
import { Mocked } from 'vitest';
|
2023-02-25 15:12:03 +01:00
|
|
|
|
|
|
|
describe(SmartInfoService.name, () => {
|
|
|
|
let sut: SmartInfoService;
|
2024-04-16 16:44:45 +02:00
|
|
|
let assetMock: Mocked<IAssetRepository>;
|
2024-05-16 00:58:23 +02:00
|
|
|
let systemMock: Mocked<ISystemMetadataRepository>;
|
2024-04-16 16:44:45 +02:00
|
|
|
let jobMock: Mocked<IJobRepository>;
|
|
|
|
let searchMock: Mocked<ISearchRepository>;
|
|
|
|
let machineMock: Mocked<IMachineLearningRepository>;
|
|
|
|
let databaseMock: Mocked<IDatabaseRepository>;
|
2024-04-16 23:30:31 +02:00
|
|
|
let loggerMock: Mocked<ILoggerRepository>;
|
2023-02-25 15:12:03 +01:00
|
|
|
|
2024-03-05 23:23:06 +01:00
|
|
|
beforeEach(() => {
|
2023-03-20 16:55:28 +01:00
|
|
|
assetMock = newAssetRepositoryMock();
|
2024-05-16 00:58:23 +02:00
|
|
|
systemMock = newSystemMetadataRepositoryMock();
|
2024-02-13 02:50:47 +01:00
|
|
|
searchMock = newSearchRepositoryMock();
|
2023-03-18 14:44:42 +01:00
|
|
|
jobMock = newJobRepositoryMock();
|
2023-02-25 15:12:03 +01:00
|
|
|
machineMock = newMachineLearningRepositoryMock();
|
2023-12-28 00:36:51 +01:00
|
|
|
databaseMock = newDatabaseRepositoryMock();
|
2024-04-16 23:30:31 +02:00
|
|
|
loggerMock = newLoggerRepositoryMock();
|
2024-05-16 00:58:23 +02:00
|
|
|
sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, searchMock, systemMock, loggerMock);
|
2023-05-26 21:43:24 +02:00
|
|
|
|
2024-04-19 03:37:55 +02:00
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
2023-02-25 15:12:03 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should work', () => {
|
|
|
|
expect(sut).toBeDefined();
|
|
|
|
});
|
|
|
|
|
2023-03-20 16:55:28 +01:00
|
|
|
describe('handleQueueEncodeClip', () => {
|
2023-09-11 17:56:38 +02:00
|
|
|
it('should do nothing if machine learning is disabled', async () => {
|
2024-05-16 00:58:23 +02:00
|
|
|
systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
|
2023-09-11 17:56:38 +02:00
|
|
|
|
|
|
|
await sut.handleQueueEncodeClip({});
|
|
|
|
|
|
|
|
expect(assetMock.getAll).not.toHaveBeenCalled();
|
|
|
|
expect(assetMock.getWithout).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
2023-03-20 16:55:28 +01:00
|
|
|
it('should queue the assets without clip embeddings', async () => {
|
2023-05-22 20:05:06 +02:00
|
|
|
assetMock.getWithout.mockResolvedValue({
|
2023-08-01 03:28:07 +02:00
|
|
|
items: [assetStub.image],
|
2023-05-22 20:05:06 +02:00
|
|
|
hasNextPage: false,
|
|
|
|
});
|
2023-03-20 16:55:28 +01:00
|
|
|
|
|
|
|
await sut.handleQueueEncodeClip({ force: false });
|
|
|
|
|
2024-01-29 15:51:22 +01:00
|
|
|
expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }]);
|
|
|
|
expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.SMART_SEARCH);
|
2024-02-27 16:24:23 +01:00
|
|
|
expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
|
2023-03-20 16:55:28 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should queue all the assets', async () => {
|
2023-05-22 20:05:06 +02:00
|
|
|
assetMock.getAll.mockResolvedValue({
|
2023-08-01 03:28:07 +02:00
|
|
|
items: [assetStub.image],
|
2023-05-22 20:05:06 +02:00
|
|
|
hasNextPage: false,
|
|
|
|
});
|
2023-03-20 16:55:28 +01:00
|
|
|
|
|
|
|
await sut.handleQueueEncodeClip({ force: true });
|
|
|
|
|
2024-01-29 15:51:22 +01:00
|
|
|
expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }]);
|
2023-03-20 16:55:28 +01:00
|
|
|
expect(assetMock.getAll).toHaveBeenCalled();
|
2024-02-27 16:24:23 +01:00
|
|
|
expect(searchMock.deleteAllSearchEmbeddings).toHaveBeenCalled();
|
2023-03-20 16:55:28 +01:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('handleEncodeClip', () => {
|
2023-09-11 17:56:38 +02:00
|
|
|
it('should do nothing if machine learning is disabled', async () => {
|
2024-05-16 00:58:23 +02:00
|
|
|
systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled);
|
2023-09-11 17:56:38 +02:00
|
|
|
|
2024-04-19 03:37:55 +02:00
|
|
|
expect(await sut.handleEncodeClip({ id: '123' })).toEqual(JobStatus.SKIPPED);
|
2023-09-11 17:56:38 +02:00
|
|
|
|
|
|
|
expect(assetMock.getByIds).not.toHaveBeenCalled();
|
|
|
|
expect(machineMock.encodeImage).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
2023-03-20 16:55:28 +01:00
|
|
|
it('should skip assets without a resize path', async () => {
|
2024-04-19 03:37:55 +02:00
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.noResizePath]);
|
2023-05-26 21:43:24 +02:00
|
|
|
|
2024-04-19 03:37:55 +02:00
|
|
|
expect(await sut.handleEncodeClip({ id: assetStub.noResizePath.id })).toEqual(JobStatus.FAILED);
|
2023-03-20 16:55:28 +01:00
|
|
|
|
2024-02-13 02:50:47 +01:00
|
|
|
expect(searchMock.upsert).not.toHaveBeenCalled();
|
2023-03-20 16:55:28 +01:00
|
|
|
expect(machineMock.encodeImage).not.toHaveBeenCalled();
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should save the returned objects', async () => {
|
|
|
|
machineMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]);
|
|
|
|
|
2024-04-19 03:37:55 +02:00
|
|
|
expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.SUCCESS);
|
2023-03-20 16:55:28 +01:00
|
|
|
|
2023-08-29 15:58:00 +02:00
|
|
|
expect(machineMock.encodeImage).toHaveBeenCalledWith(
|
|
|
|
'http://immich-machine-learning:3003',
|
2024-04-19 03:37:55 +02:00
|
|
|
{ imagePath: assetStub.image.previewPath },
|
2023-10-31 11:02:04 +01:00
|
|
|
{ enabled: true, modelName: 'ViT-B-32__openai' },
|
2023-08-29 15:58:00 +02:00
|
|
|
);
|
2024-04-19 03:37:55 +02:00
|
|
|
expect(searchMock.upsert).toHaveBeenCalledWith(assetStub.image.id, [0.01, 0.02, 0.03]);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should skip invisible assets', async () => {
|
|
|
|
assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);
|
|
|
|
|
|
|
|
expect(await sut.handleEncodeClip({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED);
|
|
|
|
|
|
|
|
expect(machineMock.encodeImage).not.toHaveBeenCalled();
|
|
|
|
expect(searchMock.upsert).not.toHaveBeenCalled();
|
2023-12-08 17:15:46 +01:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('getCLIPModelInfo', () => {
|
|
|
|
it('should return the model info', () => {
|
|
|
|
expect(getCLIPModelInfo('ViT-B-32__openai')).toEqual({ dimSize: 512 });
|
2024-03-21 04:15:09 +01:00
|
|
|
expect(getCLIPModelInfo('M-CLIP/XLM-Roberta-Large-Vit-L-14')).toEqual({ dimSize: 768 });
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should clean the model name', () => {
|
|
|
|
expect(getCLIPModelInfo('ViT-B-32::openai')).toEqual({ dimSize: 512 });
|
2023-12-08 17:15:46 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should throw an error if the model is not present', () => {
|
|
|
|
expect(() => getCLIPModelInfo('test-model')).toThrow('Unknown CLIP model: test-model');
|
2023-03-20 16:55:28 +01:00
|
|
|
});
|
|
|
|
});
|
2023-02-25 15:12:03 +01:00
|
|
|
});
|