Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(server): skip invisible assets for thumbnail generation and ml #8891

Merged
merged 4 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions server/src/services/media.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,15 @@ describe(MediaService.name, () => {
expect(assetMock.update).not.toHaveBeenCalledWith();
});

it('should skip invisible assets', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);

expect(await sut.handleGeneratePreview({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED);

expect(mediaMock.resize).not.toHaveBeenCalled();
expect(assetMock.update).not.toHaveBeenCalledWith();
});

it.each(Object.values(ImageFormat))('should generate a %s preview for an image when specified', async (format) => {
configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_PREVIEW_FORMAT, value: format }]);
assetMock.getByIds.mockResolvedValue([assetStub.image]);
Expand Down Expand Up @@ -353,6 +362,15 @@ describe(MediaService.name, () => {
expect(assetMock.update).not.toHaveBeenCalledWith();
});

it('should skip invisible assets', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);

expect(await sut.handleGenerateThumbnail({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED);

expect(mediaMock.resize).not.toHaveBeenCalled();
expect(assetMock.update).not.toHaveBeenCalledWith();
});

it.each(Object.values(ImageFormat))(
'should generate a %s thumbnail for an image when specified',
async (format) => {
Expand Down Expand Up @@ -410,6 +428,15 @@ describe(MediaService.name, () => {
expect(mediaMock.generateThumbhash).not.toHaveBeenCalled();
});

it('should skip invisible assets', async () => {
assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]);

expect(await sut.handleGenerateThumbhash({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED);

expect(mediaMock.generateThumbhash).not.toHaveBeenCalled();
expect(assetMock.update).not.toHaveBeenCalledWith();
});

it('should generate a thumbhash', async () => {
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
assetMock.getByIds.mockResolvedValue([assetStub.image]);
Expand Down
20 changes: 18 additions & 2 deletions server/src/services/media.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class MediaService {
async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise<JobStatus> {
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
return force
? this.assetRepository.getAll(pagination)
? this.assetRepository.getAll(pagination, { isVisible: true })
: this.assetRepository.getWithout(pagination, WithoutProperty.THUMBNAIL);
});

Expand Down Expand Up @@ -178,6 +178,10 @@ export class MediaService {
return JobStatus.FAILED;
}

if (!asset.isVisible) {
return JobStatus.SKIPPED;
}

const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, image.previewFormat);
await this.assetRepository.update({ id: asset.id, previewPath });
return JobStatus.SUCCESS;
Expand Down Expand Up @@ -230,14 +234,26 @@ export class MediaService {
return JobStatus.FAILED;
}

if (!asset.isVisible) {
return JobStatus.SKIPPED;
}

const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat);
await this.assetRepository.update({ id: asset.id, thumbnailPath });
return JobStatus.SUCCESS;
}

async handleGenerateThumbhash({ id }: IEntityJob): Promise<JobStatus> {
const [asset] = await this.assetRepository.getByIds([id]);
if (!asset?.previewPath) {
if (!asset) {
return JobStatus.FAILED;
}

if (!asset.isVisible) {
return JobStatus.SKIPPED;
}

if (!asset.previewPath) {
return JobStatus.FAILED;
}

Expand Down
11 changes: 10 additions & 1 deletion server/src/services/person.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,12 @@ export class PersonService {

const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
return force
? this.assetRepository.getAll(pagination, { orderDirection: 'DESC', withFaces: true, withArchived: true })
? this.assetRepository.getAll(pagination, {
orderDirection: 'DESC',
withFaces: true,
withArchived: true,
isVisible: true,
})
: this.assetRepository.getWithout(pagination, WithoutProperty.FACES);
});

Expand Down Expand Up @@ -322,6 +327,10 @@ export class PersonService {
return JobStatus.FAILED;
}

if (!asset.isVisible) {
return JobStatus.SKIPPED;
}

const faces = await this.machineLearningRepository.detectFaces(
machineLearning.url,
{ imagePath: asset.previewPath },
Expand Down
32 changes: 17 additions & 15 deletions server/src/services/smart-info.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { AssetEntity } from 'src/entities/asset.entity';
import { SystemConfigKey } from 'src/entities/system-config.entity';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
import { ISearchRepository } from 'src/interfaces/search.interface';
Expand All @@ -19,11 +18,6 @@ import { newSearchRepositoryMock } from 'test/repositories/search.repository.moc
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
import { Mocked } from 'vitest';

const asset = {
id: 'asset-1',
previewPath: 'path/to/resize.ext',
} as AssetEntity;

describe(SmartInfoService.name, () => {
let sut: SmartInfoService;
let assetMock: Mocked<IAssetRepository>;
Expand All @@ -44,7 +38,7 @@ describe(SmartInfoService.name, () => {
loggerMock = newLoggerRepositoryMock();
sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, searchMock, configMock, loggerMock);

assetMock.getByIds.mockResolvedValue([asset]);
assetMock.getByIds.mockResolvedValue([assetStub.image]);
});

it('should work', () => {
Expand Down Expand Up @@ -92,17 +86,16 @@ describe(SmartInfoService.name, () => {
it('should do nothing if machine learning is disabled', async () => {
configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]);

await sut.handleEncodeClip({ id: '123' });
expect(await sut.handleEncodeClip({ id: '123' })).toEqual(JobStatus.SKIPPED);

expect(assetMock.getByIds).not.toHaveBeenCalled();
expect(machineMock.encodeImage).not.toHaveBeenCalled();
});

it('should skip assets without a resize path', async () => {
const asset = { previewPath: '' } as AssetEntity;
assetMock.getByIds.mockResolvedValue([asset]);
assetMock.getByIds.mockResolvedValue([assetStub.noResizePath]);

await sut.handleEncodeClip({ id: asset.id });
expect(await sut.handleEncodeClip({ id: assetStub.noResizePath.id })).toEqual(JobStatus.FAILED);

expect(searchMock.upsert).not.toHaveBeenCalled();
expect(machineMock.encodeImage).not.toHaveBeenCalled();
Expand All @@ -111,14 +104,23 @@ describe(SmartInfoService.name, () => {
it('should save the returned objects', async () => {
machineMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]);

await sut.handleEncodeClip({ id: asset.id });
expect(await sut.handleEncodeClip({ id: assetStub.image.id })).toEqual(JobStatus.SUCCESS);

expect(machineMock.encodeImage).toHaveBeenCalledWith(
'http://immich-machine-learning:3003',
{ imagePath: 'path/to/resize.ext' },
{ imagePath: assetStub.image.previewPath },
{ enabled: true, modelName: 'ViT-B-32__openai' },
);
expect(searchMock.upsert).toHaveBeenCalledWith('asset-1', [0.01, 0.02, 0.03]);
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();
});
});

Expand Down
6 changes: 5 additions & 1 deletion server/src/services/smart-info.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class SmartInfoService {

const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
return force
? this.assetRepository.getAll(pagination)
? this.assetRepository.getAll(pagination, { isVisible: true })
: this.assetRepository.getWithout(pagination, WithoutProperty.SMART_SEARCH);
});

Expand All @@ -84,6 +84,10 @@ export class SmartInfoService {
return JobStatus.FAILED;
}

if (!asset.isVisible) {
return JobStatus.SKIPPED;
}

if (!asset.previewPath) {
return JobStatus.FAILED;
}
Expand Down
Loading