mirror of
https://github.com/immich-app/immich.git
synced 2025-06-02 21:24:28 -04:00
fix(server): skip invisible assets for thumbnail generation and ml (#8891)
* skip invisible assets for thumbnail generation and ml * no need to update job status * fix thumbhash check order * linting
This commit is contained in:
parent
112d6d60ec
commit
596c35dc00
@ -225,6 +225,15 @@ describe(MediaService.name, () => {
|
|||||||
expect(assetMock.update).not.toHaveBeenCalledWith();
|
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) => {
|
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 }]);
|
configMock.load.mockResolvedValue([{ key: SystemConfigKey.IMAGE_PREVIEW_FORMAT, value: format }]);
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
@ -353,6 +362,15 @@ describe(MediaService.name, () => {
|
|||||||
expect(assetMock.update).not.toHaveBeenCalledWith();
|
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))(
|
it.each(Object.values(ImageFormat))(
|
||||||
'should generate a %s thumbnail for an image when specified',
|
'should generate a %s thumbnail for an image when specified',
|
||||||
async (format) => {
|
async (format) => {
|
||||||
@ -410,6 +428,15 @@ describe(MediaService.name, () => {
|
|||||||
expect(mediaMock.generateThumbhash).not.toHaveBeenCalled();
|
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 () => {
|
it('should generate a thumbhash', async () => {
|
||||||
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8');
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
|
@ -77,7 +77,7 @@ export class MediaService {
|
|||||||
async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise<JobStatus> {
|
async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise<JobStatus> {
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
||||||
return force
|
return force
|
||||||
? this.assetRepository.getAll(pagination)
|
? this.assetRepository.getAll(pagination, { isVisible: true })
|
||||||
: this.assetRepository.getWithout(pagination, WithoutProperty.THUMBNAIL);
|
: this.assetRepository.getWithout(pagination, WithoutProperty.THUMBNAIL);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -178,6 +178,10 @@ export class MediaService {
|
|||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!asset.isVisible) {
|
||||||
|
return JobStatus.SKIPPED;
|
||||||
|
}
|
||||||
|
|
||||||
const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, image.previewFormat);
|
const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, image.previewFormat);
|
||||||
await this.assetRepository.update({ id: asset.id, previewPath });
|
await this.assetRepository.update({ id: asset.id, previewPath });
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
@ -230,6 +234,10 @@ export class MediaService {
|
|||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!asset.isVisible) {
|
||||||
|
return JobStatus.SKIPPED;
|
||||||
|
}
|
||||||
|
|
||||||
const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat);
|
const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat);
|
||||||
await this.assetRepository.update({ id: asset.id, thumbnailPath });
|
await this.assetRepository.update({ id: asset.id, thumbnailPath });
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
@ -237,7 +245,15 @@ export class MediaService {
|
|||||||
|
|
||||||
async handleGenerateThumbhash({ id }: IEntityJob): Promise<JobStatus> {
|
async handleGenerateThumbhash({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const [asset] = await this.assetRepository.getByIds([id]);
|
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;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,7 +292,12 @@ export class PersonService {
|
|||||||
|
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
||||||
return force
|
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);
|
: this.assetRepository.getWithout(pagination, WithoutProperty.FACES);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -322,6 +327,10 @@ export class PersonService {
|
|||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!asset.isVisible) {
|
||||||
|
return JobStatus.SKIPPED;
|
||||||
|
}
|
||||||
|
|
||||||
const faces = await this.machineLearningRepository.detectFaces(
|
const faces = await this.machineLearningRepository.detectFaces(
|
||||||
machineLearning.url,
|
machineLearning.url,
|
||||||
{ imagePath: asset.previewPath },
|
{ imagePath: asset.previewPath },
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
|
||||||
import { SystemConfigKey } from 'src/entities/system-config.entity';
|
import { SystemConfigKey } from 'src/entities/system-config.entity';
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
import { IDatabaseRepository } from 'src/interfaces/database.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 { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
|
||||||
import { ISearchRepository } from 'src/interfaces/search.interface';
|
import { ISearchRepository } from 'src/interfaces/search.interface';
|
||||||
@ -19,11 +18,6 @@ import { newSearchRepositoryMock } from 'test/repositories/search.repository.moc
|
|||||||
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
|
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
|
||||||
import { Mocked } from 'vitest';
|
import { Mocked } from 'vitest';
|
||||||
|
|
||||||
const asset = {
|
|
||||||
id: 'asset-1',
|
|
||||||
previewPath: 'path/to/resize.ext',
|
|
||||||
} as AssetEntity;
|
|
||||||
|
|
||||||
describe(SmartInfoService.name, () => {
|
describe(SmartInfoService.name, () => {
|
||||||
let sut: SmartInfoService;
|
let sut: SmartInfoService;
|
||||||
let assetMock: Mocked<IAssetRepository>;
|
let assetMock: Mocked<IAssetRepository>;
|
||||||
@ -44,7 +38,7 @@ describe(SmartInfoService.name, () => {
|
|||||||
loggerMock = newLoggerRepositoryMock();
|
loggerMock = newLoggerRepositoryMock();
|
||||||
sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, searchMock, configMock, loggerMock);
|
sut = new SmartInfoService(assetMock, databaseMock, jobMock, machineMock, searchMock, configMock, loggerMock);
|
||||||
|
|
||||||
assetMock.getByIds.mockResolvedValue([asset]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
@ -92,17 +86,16 @@ describe(SmartInfoService.name, () => {
|
|||||||
it('should do nothing if machine learning is disabled', async () => {
|
it('should do nothing if machine learning is disabled', async () => {
|
||||||
configMock.load.mockResolvedValue([{ key: SystemConfigKey.MACHINE_LEARNING_ENABLED, value: false }]);
|
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(assetMock.getByIds).not.toHaveBeenCalled();
|
||||||
expect(machineMock.encodeImage).not.toHaveBeenCalled();
|
expect(machineMock.encodeImage).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip assets without a resize path', async () => {
|
it('should skip assets without a resize path', async () => {
|
||||||
const asset = { previewPath: '' } as AssetEntity;
|
assetMock.getByIds.mockResolvedValue([assetStub.noResizePath]);
|
||||||
assetMock.getByIds.mockResolvedValue([asset]);
|
|
||||||
|
|
||||||
await sut.handleEncodeClip({ id: asset.id });
|
expect(await sut.handleEncodeClip({ id: assetStub.noResizePath.id })).toEqual(JobStatus.FAILED);
|
||||||
|
|
||||||
expect(searchMock.upsert).not.toHaveBeenCalled();
|
expect(searchMock.upsert).not.toHaveBeenCalled();
|
||||||
expect(machineMock.encodeImage).not.toHaveBeenCalled();
|
expect(machineMock.encodeImage).not.toHaveBeenCalled();
|
||||||
@ -111,14 +104,23 @@ describe(SmartInfoService.name, () => {
|
|||||||
it('should save the returned objects', async () => {
|
it('should save the returned objects', async () => {
|
||||||
machineMock.encodeImage.mockResolvedValue([0.01, 0.02, 0.03]);
|
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(
|
expect(machineMock.encodeImage).toHaveBeenCalledWith(
|
||||||
'http://immich-machine-learning:3003',
|
'http://immich-machine-learning:3003',
|
||||||
{ imagePath: 'path/to/resize.ext' },
|
{ imagePath: assetStub.image.previewPath },
|
||||||
{ enabled: true, modelName: 'ViT-B-32__openai' },
|
{ 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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ export class SmartInfoService {
|
|||||||
|
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
||||||
return force
|
return force
|
||||||
? this.assetRepository.getAll(pagination)
|
? this.assetRepository.getAll(pagination, { isVisible: true })
|
||||||
: this.assetRepository.getWithout(pagination, WithoutProperty.SMART_SEARCH);
|
: this.assetRepository.getWithout(pagination, WithoutProperty.SMART_SEARCH);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -84,6 +84,10 @@ export class SmartInfoService {
|
|||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!asset.isVisible) {
|
||||||
|
return JobStatus.SKIPPED;
|
||||||
|
}
|
||||||
|
|
||||||
if (!asset.previewPath) {
|
if (!asset.previewPath) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user