mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:39:37 -05:00 
			
		
		
		
	refactor(server): person thumbnail job (#4233)
* refactor(server): person thumbnail job * fix(server): set feature photo
This commit is contained in:
		
							parent
							
								
									ea797c1723
								
							
						
					
					
						commit
						7bc6e9ef64
					
				@ -26,20 +26,7 @@ import { FacialRecognitionService } from './facial-recognition.services';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const croppedFace = Buffer.from('Cropped Face');
 | 
					const croppedFace = Buffer.from('Cropped Face');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const face = {
 | 
					const detectFaceMock = {
 | 
				
			||||||
  start: {
 | 
					 | 
				
			||||||
    assetId: 'asset-1',
 | 
					 | 
				
			||||||
    personId: 'person-1',
 | 
					 | 
				
			||||||
    boundingBox: {
 | 
					 | 
				
			||||||
      x1: 5,
 | 
					 | 
				
			||||||
      y1: 5,
 | 
					 | 
				
			||||||
      x2: 505,
 | 
					 | 
				
			||||||
      y2: 505,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    imageHeight: 1000,
 | 
					 | 
				
			||||||
    imageWidth: 1000,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  middle: {
 | 
					 | 
				
			||||||
  assetId: 'asset-1',
 | 
					  assetId: 'asset-1',
 | 
				
			||||||
  personId: 'person-1',
 | 
					  personId: 'person-1',
 | 
				
			||||||
  boundingBox: {
 | 
					  boundingBox: {
 | 
				
			||||||
@ -52,19 +39,6 @@ const face = {
 | 
				
			|||||||
  imageWidth: 400,
 | 
					  imageWidth: 400,
 | 
				
			||||||
  embedding: [1, 2, 3, 4],
 | 
					  embedding: [1, 2, 3, 4],
 | 
				
			||||||
  score: 0.2,
 | 
					  score: 0.2,
 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  end: {
 | 
					 | 
				
			||||||
    assetId: 'asset-1',
 | 
					 | 
				
			||||||
    personId: 'person-1',
 | 
					 | 
				
			||||||
    boundingBox: {
 | 
					 | 
				
			||||||
      x1: 300,
 | 
					 | 
				
			||||||
      y1: 300,
 | 
					 | 
				
			||||||
      x2: 495,
 | 
					 | 
				
			||||||
      y2: 495,
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    imageHeight: 500,
 | 
					 | 
				
			||||||
    imageWidth: 500,
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const faceSearch = {
 | 
					const faceSearch = {
 | 
				
			||||||
@ -214,7 +188,7 @@ describe(FacialRecognitionService.name, () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should match existing people', async () => {
 | 
					    it('should match existing people', async () => {
 | 
				
			||||||
      machineLearningMock.detectFaces.mockResolvedValue([face.middle]);
 | 
					      machineLearningMock.detectFaces.mockResolvedValue([detectFaceMock]);
 | 
				
			||||||
      searchMock.searchFaces.mockResolvedValue(faceSearch.oneMatch);
 | 
					      searchMock.searchFaces.mockResolvedValue(faceSearch.oneMatch);
 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.image]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.image]);
 | 
				
			||||||
      await sut.handleRecognizeFaces({ id: assetStub.image.id });
 | 
					      await sut.handleRecognizeFaces({ id: assetStub.image.id });
 | 
				
			||||||
@ -233,7 +207,7 @@ describe(FacialRecognitionService.name, () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should create a new person', async () => {
 | 
					    it('should create a new person', async () => {
 | 
				
			||||||
      machineLearningMock.detectFaces.mockResolvedValue([face.middle]);
 | 
					      machineLearningMock.detectFaces.mockResolvedValue([detectFaceMock]);
 | 
				
			||||||
      searchMock.searchFaces.mockResolvedValue(faceSearch.oneRemoteMatch);
 | 
					      searchMock.searchFaces.mockResolvedValue(faceSearch.oneRemoteMatch);
 | 
				
			||||||
      personMock.create.mockResolvedValue(personStub.noName);
 | 
					      personMock.create.mockResolvedValue(personStub.noName);
 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.image]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.image]);
 | 
				
			||||||
@ -253,60 +227,56 @@ describe(FacialRecognitionService.name, () => {
 | 
				
			|||||||
        imageWidth: 400,
 | 
					        imageWidth: 400,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      expect(jobMock.queue.mock.calls).toEqual([
 | 
					      expect(jobMock.queue.mock.calls).toEqual([
 | 
				
			||||||
        [
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            name: JobName.GENERATE_FACE_THUMBNAIL,
 | 
					 | 
				
			||||||
            data: {
 | 
					 | 
				
			||||||
              assetId: 'asset-1',
 | 
					 | 
				
			||||||
              personId: 'person-1',
 | 
					 | 
				
			||||||
              boundingBox: {
 | 
					 | 
				
			||||||
                x1: 100,
 | 
					 | 
				
			||||||
                y1: 100,
 | 
					 | 
				
			||||||
                x2: 200,
 | 
					 | 
				
			||||||
                y2: 200,
 | 
					 | 
				
			||||||
              },
 | 
					 | 
				
			||||||
              imageHeight: 500,
 | 
					 | 
				
			||||||
              imageWidth: 400,
 | 
					 | 
				
			||||||
              score: 0.2,
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
        [{ name: JobName.SEARCH_INDEX_FACE, data: { personId: 'person-1', assetId: 'asset-id' } }],
 | 
					        [{ name: JobName.SEARCH_INDEX_FACE, data: { personId: 'person-1', assetId: 'asset-id' } }],
 | 
				
			||||||
 | 
					        [{ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: 'person-1' } }],
 | 
				
			||||||
      ]);
 | 
					      ]);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('handleGenerateFaceThumbnail', () => {
 | 
					  describe('handleGeneratePersonThumbnail', () => {
 | 
				
			||||||
    it('should return if machine learning is disabled', async () => {
 | 
					    it('should return 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 expect(sut.handleGenerateFaceThumbnail(face.middle)).resolves.toBe(true);
 | 
					      await expect(sut.handleGeneratePersonThumbnail({ id: 'person-1' })).resolves.toBe(true);
 | 
				
			||||||
      expect(assetMock.getByIds).not.toHaveBeenCalled();
 | 
					      expect(assetMock.getByIds).not.toHaveBeenCalled();
 | 
				
			||||||
      expect(configMock.load).toHaveBeenCalled();
 | 
					      expect(configMock.load).toHaveBeenCalled();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should skip an asset not found', async () => {
 | 
					    it('should skip a person not found', async () => {
 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([]);
 | 
					      personMock.getById.mockResolvedValue(null);
 | 
				
			||||||
 | 
					      await sut.handleGeneratePersonThumbnail({ id: 'person-1' });
 | 
				
			||||||
      await sut.handleGenerateFaceThumbnail(face.middle);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      expect(mediaMock.crop).not.toHaveBeenCalled();
 | 
					      expect(mediaMock.crop).not.toHaveBeenCalled();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should skip an asset without a thumbnail', async () => {
 | 
					    it('should skip a person without a face asset id', async () => {
 | 
				
			||||||
 | 
					      personMock.getById.mockResolvedValue(personStub.noThumbnail);
 | 
				
			||||||
 | 
					      await sut.handleGeneratePersonThumbnail({ id: 'person-1' });
 | 
				
			||||||
 | 
					      expect(mediaMock.crop).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should skip an person with a face asset id not found', async () => {
 | 
				
			||||||
 | 
					      personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.middle.assetId });
 | 
				
			||||||
 | 
					      faceMock.getByIds.mockResolvedValue([faceStub.face1]);
 | 
				
			||||||
 | 
					      await sut.handleGeneratePersonThumbnail({ id: 'person-1' });
 | 
				
			||||||
 | 
					      expect(mediaMock.crop).not.toHaveBeenCalled();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    it('should skip a person with a face asset id without a thumbnail', async () => {
 | 
				
			||||||
 | 
					      personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.middle.assetId });
 | 
				
			||||||
 | 
					      faceMock.getByIds.mockResolvedValue([faceStub.face1]);
 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.noResizePath]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.noResizePath]);
 | 
				
			||||||
 | 
					      await sut.handleGeneratePersonThumbnail({ id: 'person-1' });
 | 
				
			||||||
      await sut.handleGenerateFaceThumbnail(face.middle);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      expect(mediaMock.crop).not.toHaveBeenCalled();
 | 
					      expect(mediaMock.crop).not.toHaveBeenCalled();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should generate a thumbnail', async () => {
 | 
					    it('should generate a thumbnail', async () => {
 | 
				
			||||||
 | 
					      personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.middle.assetId });
 | 
				
			||||||
 | 
					      faceMock.getByIds.mockResolvedValue([faceStub.middle]);
 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.image]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.image]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.handleGenerateFaceThumbnail(face.middle);
 | 
					      await sut.handleGeneratePersonThumbnail({ id: 'person-1' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(assetMock.getByIds).toHaveBeenCalledWith(['asset-1']);
 | 
					      expect(assetMock.getByIds).toHaveBeenCalledWith([faceStub.middle.assetId]);
 | 
				
			||||||
      expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/pe/rs');
 | 
					      expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/pe/rs');
 | 
				
			||||||
      expect(mediaMock.crop).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg', {
 | 
					      expect(mediaMock.crop).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg', {
 | 
				
			||||||
        left: 95,
 | 
					        left: 95,
 | 
				
			||||||
@ -321,16 +291,17 @@ describe(FacialRecognitionService.name, () => {
 | 
				
			|||||||
        colorspace: Colorspace.P3,
 | 
					        colorspace: Colorspace.P3,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      expect(personMock.update).toHaveBeenCalledWith({
 | 
					      expect(personMock.update).toHaveBeenCalledWith({
 | 
				
			||||||
        faceAssetId: 'asset-1',
 | 
					 | 
				
			||||||
        id: 'person-1',
 | 
					        id: 'person-1',
 | 
				
			||||||
        thumbnailPath: 'upload/thumbs/user-id/pe/rs/person-1.jpeg',
 | 
					        thumbnailPath: 'upload/thumbs/user-id/pe/rs/person-1.jpeg',
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should generate a thumbnail without going negative', async () => {
 | 
					    it('should generate a thumbnail without going negative', async () => {
 | 
				
			||||||
 | 
					      personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.start.assetId });
 | 
				
			||||||
 | 
					      faceMock.getByIds.mockResolvedValue([faceStub.start]);
 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.image]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.image]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.handleGenerateFaceThumbnail(face.start);
 | 
					      await sut.handleGeneratePersonThumbnail({ id: 'person-1' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(mediaMock.crop).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg', {
 | 
					      expect(mediaMock.crop).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg', {
 | 
				
			||||||
        left: 0,
 | 
					        left: 0,
 | 
				
			||||||
@ -347,9 +318,11 @@ describe(FacialRecognitionService.name, () => {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should generate a thumbnail without overflowing', async () => {
 | 
					    it('should generate a thumbnail without overflowing', async () => {
 | 
				
			||||||
 | 
					      personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.end.assetId });
 | 
				
			||||||
 | 
					      faceMock.getByIds.mockResolvedValue([faceStub.end]);
 | 
				
			||||||
      assetMock.getByIds.mockResolvedValue([assetStub.image]);
 | 
					      assetMock.getByIds.mockResolvedValue([assetStub.image]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.handleGenerateFaceThumbnail(face.end);
 | 
					      await sut.handleGeneratePersonThumbnail({ id: 'person-1' });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(mediaMock.crop).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg', {
 | 
					      expect(mediaMock.crop).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg', {
 | 
				
			||||||
        left: 297,
 | 
					        left: 297,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,8 @@
 | 
				
			|||||||
 | 
					import { PersonEntity } from '@app/infra/entities';
 | 
				
			||||||
import { Inject, Logger } from '@nestjs/common';
 | 
					import { Inject, Logger } from '@nestjs/common';
 | 
				
			||||||
import { IAssetRepository, WithoutProperty } from '../asset';
 | 
					import { IAssetRepository, WithoutProperty } from '../asset';
 | 
				
			||||||
import { usePagination } from '../domain.util';
 | 
					import { usePagination } from '../domain.util';
 | 
				
			||||||
import { IBaseJob, IEntityJob, IFaceThumbnailJob, IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
 | 
					import { IBaseJob, IEntityJob, IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
 | 
				
			||||||
import { CropOptions, FACE_THUMBNAIL_SIZE, IMediaRepository } from '../media';
 | 
					import { CropOptions, FACE_THUMBNAIL_SIZE, IMediaRepository } from '../media';
 | 
				
			||||||
import { IPersonRepository } from '../person/person.repository';
 | 
					import { IPersonRepository } from '../person/person.repository';
 | 
				
			||||||
import { ISearchRepository } from '../search/search.repository';
 | 
					import { ISearchRepository } from '../search/search.repository';
 | 
				
			||||||
@ -89,18 +90,14 @@ export class FacialRecognitionService {
 | 
				
			|||||||
        personId = faceSearchResult.items[0].personId;
 | 
					        personId = faceSearchResult.items[0].personId;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      let newPerson: PersonEntity | null = null;
 | 
				
			||||||
      if (!personId) {
 | 
					      if (!personId) {
 | 
				
			||||||
        this.logger.debug('No matches, creating a new person.');
 | 
					        this.logger.debug('No matches, creating a new person.');
 | 
				
			||||||
        const person = await this.personRepository.create({ ownerId: asset.ownerId });
 | 
					        newPerson = await this.personRepository.create({ ownerId: asset.ownerId });
 | 
				
			||||||
        personId = person.id;
 | 
					        personId = newPerson.id;
 | 
				
			||||||
        await this.jobRepository.queue({
 | 
					 | 
				
			||||||
          name: JobName.GENERATE_FACE_THUMBNAIL,
 | 
					 | 
				
			||||||
          data: { assetId: asset.id, personId, ...rest },
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const faceId: AssetFaceId = { assetId: asset.id, personId };
 | 
					      const faceId: AssetFaceId = { assetId: asset.id, personId };
 | 
				
			||||||
 | 
					 | 
				
			||||||
      await this.faceRepository.create({
 | 
					      await this.faceRepository.create({
 | 
				
			||||||
        ...faceId,
 | 
					        ...faceId,
 | 
				
			||||||
        embedding,
 | 
					        embedding,
 | 
				
			||||||
@ -112,6 +109,11 @@ export class FacialRecognitionService {
 | 
				
			|||||||
        boundingBoxY2: rest.boundingBox.y2,
 | 
					        boundingBoxY2: rest.boundingBox.y2,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_FACE, data: faceId });
 | 
					      await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_FACE, data: faceId });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (newPerson) {
 | 
				
			||||||
 | 
					        await this.personRepository.update({ id: personId, faceAssetId: asset.id });
 | 
				
			||||||
 | 
					        await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: newPerson.id } });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
@ -132,24 +134,41 @@ export class FacialRecognitionService {
 | 
				
			|||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async handleGenerateFaceThumbnail(data: IFaceThumbnailJob) {
 | 
					  async handleGeneratePersonThumbnail(data: IEntityJob) {
 | 
				
			||||||
    const { machineLearning } = await this.configCore.getConfig();
 | 
					    const { machineLearning, thumbnail } = await this.configCore.getConfig();
 | 
				
			||||||
    if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
 | 
					    if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {
 | 
				
			||||||
      return true;
 | 
					      return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { assetId, personId, boundingBox, imageWidth, imageHeight } = data;
 | 
					    const person = await this.personRepository.getById(data.id);
 | 
				
			||||||
 | 
					    if (!person?.faceAssetId) {
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const [face] = await this.faceRepository.getByIds([{ personId: person.id, assetId: person.faceAssetId }]);
 | 
				
			||||||
 | 
					    if (!face) {
 | 
				
			||||||
 | 
					      return false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const {
 | 
				
			||||||
 | 
					      assetId,
 | 
				
			||||||
 | 
					      personId,
 | 
				
			||||||
 | 
					      boundingBoxX1: x1,
 | 
				
			||||||
 | 
					      boundingBoxX2: x2,
 | 
				
			||||||
 | 
					      boundingBoxY1: y1,
 | 
				
			||||||
 | 
					      boundingBoxY2: y2,
 | 
				
			||||||
 | 
					      imageWidth,
 | 
				
			||||||
 | 
					      imageHeight,
 | 
				
			||||||
 | 
					    } = face;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [asset] = await this.assetRepository.getByIds([assetId]);
 | 
					    const [asset] = await this.assetRepository.getByIds([assetId]);
 | 
				
			||||||
    if (!asset || !asset.resizePath) {
 | 
					    if (!asset?.resizePath) {
 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.logger.verbose(`Cropping face for person: ${personId}`);
 | 
					    this.logger.verbose(`Cropping face for person: ${personId}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const output = this.storageCore.ensurePath(StorageFolder.THUMBNAILS, asset.ownerId, `${personId}.jpeg`);
 | 
					    const thumbnailPath = this.storageCore.ensurePath(StorageFolder.THUMBNAILS, asset.ownerId, `${personId}.jpeg`);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    const { x1, y1, x2, y2 } = boundingBox;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const halfWidth = (x2 - x1) / 2;
 | 
					    const halfWidth = (x2 - x1) / 2;
 | 
				
			||||||
    const halfHeight = (y2 - y1) / 2;
 | 
					    const halfHeight = (y2 - y1) / 2;
 | 
				
			||||||
@ -175,7 +194,6 @@ export class FacialRecognitionService {
 | 
				
			|||||||
      height: newHalfSize * 2,
 | 
					      height: newHalfSize * 2,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { thumbnail } = await this.configCore.getConfig();
 | 
					 | 
				
			||||||
    const croppedOutput = await this.mediaRepository.crop(asset.resizePath, cropOptions);
 | 
					    const croppedOutput = await this.mediaRepository.crop(asset.resizePath, cropOptions);
 | 
				
			||||||
    const thumbnailOptions = {
 | 
					    const thumbnailOptions = {
 | 
				
			||||||
      format: 'jpeg',
 | 
					      format: 'jpeg',
 | 
				
			||||||
@ -183,8 +201,9 @@ export class FacialRecognitionService {
 | 
				
			|||||||
      colorspace: thumbnail.colorspace,
 | 
					      colorspace: thumbnail.colorspace,
 | 
				
			||||||
      quality: thumbnail.quality,
 | 
					      quality: thumbnail.quality,
 | 
				
			||||||
    } as const;
 | 
					    } as const;
 | 
				
			||||||
    await this.mediaRepository.resize(croppedOutput, output, thumbnailOptions);
 | 
					
 | 
				
			||||||
    await this.personRepository.update({ id: personId, thumbnailPath: output, faceAssetId: data.assetId });
 | 
					    await this.mediaRepository.resize(croppedOutput, thumbnailPath, thumbnailOptions);
 | 
				
			||||||
 | 
					    await this.personRepository.update({ id: personId, thumbnailPath });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -30,7 +30,7 @@ export enum JobName {
 | 
				
			|||||||
  GENERATE_JPEG_THUMBNAIL = 'generate-jpeg-thumbnail',
 | 
					  GENERATE_JPEG_THUMBNAIL = 'generate-jpeg-thumbnail',
 | 
				
			||||||
  GENERATE_WEBP_THUMBNAIL = 'generate-webp-thumbnail',
 | 
					  GENERATE_WEBP_THUMBNAIL = 'generate-webp-thumbnail',
 | 
				
			||||||
  GENERATE_THUMBHASH_THUMBNAIL = 'generate-thumbhash-thumbnail',
 | 
					  GENERATE_THUMBHASH_THUMBNAIL = 'generate-thumbhash-thumbnail',
 | 
				
			||||||
  GENERATE_FACE_THUMBNAIL = 'generate-face-thumbnail',
 | 
					  GENERATE_PERSON_THUMBNAIL = 'generate-person-thumbnail',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // metadata
 | 
					  // metadata
 | 
				
			||||||
  QUEUE_METADATA_EXTRACTION = 'queue-metadata-extraction',
 | 
					  QUEUE_METADATA_EXTRACTION = 'queue-metadata-extraction',
 | 
				
			||||||
@ -113,7 +113,7 @@ export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
 | 
				
			|||||||
  [JobName.GENERATE_JPEG_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
 | 
					  [JobName.GENERATE_JPEG_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
 | 
				
			||||||
  [JobName.GENERATE_WEBP_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
 | 
					  [JobName.GENERATE_WEBP_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
 | 
				
			||||||
  [JobName.GENERATE_THUMBHASH_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
 | 
					  [JobName.GENERATE_THUMBHASH_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
 | 
				
			||||||
  [JobName.GENERATE_FACE_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
 | 
					  [JobName.GENERATE_PERSON_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // metadata
 | 
					  // metadata
 | 
				
			||||||
  [JobName.QUEUE_METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION,
 | 
					  [JobName.QUEUE_METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,3 @@
 | 
				
			|||||||
import { BoundingBox } from '../smart-info';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface IBaseJob {
 | 
					export interface IBaseJob {
 | 
				
			||||||
  force?: boolean;
 | 
					  force?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -9,14 +7,6 @@ export interface IAssetFaceJob extends IBaseJob {
 | 
				
			|||||||
  personId: string;
 | 
					  personId: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IFaceThumbnailJob extends IAssetFaceJob {
 | 
					 | 
				
			||||||
  imageWidth: number;
 | 
					 | 
				
			||||||
  imageHeight: number;
 | 
					 | 
				
			||||||
  boundingBox: BoundingBox;
 | 
					 | 
				
			||||||
  assetId: string;
 | 
					 | 
				
			||||||
  personId: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface IEntityJob extends IBaseJob {
 | 
					export interface IEntityJob extends IBaseJob {
 | 
				
			||||||
  id: string;
 | 
					  id: string;
 | 
				
			||||||
  source?: 'upload';
 | 
					  source?: 'upload';
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,6 @@ import {
 | 
				
			|||||||
  IBulkEntityJob,
 | 
					  IBulkEntityJob,
 | 
				
			||||||
  IDeleteFilesJob,
 | 
					  IDeleteFilesJob,
 | 
				
			||||||
  IEntityJob,
 | 
					  IEntityJob,
 | 
				
			||||||
  IFaceThumbnailJob,
 | 
					 | 
				
			||||||
  ILibraryFileJob,
 | 
					  ILibraryFileJob,
 | 
				
			||||||
  ILibraryRefreshJob,
 | 
					  ILibraryRefreshJob,
 | 
				
			||||||
  IOfflineLibraryFileJob,
 | 
					  IOfflineLibraryFileJob,
 | 
				
			||||||
@ -68,7 +67,7 @@ export type JobItem =
 | 
				
			|||||||
  // Recognize Faces
 | 
					  // Recognize Faces
 | 
				
			||||||
  | { name: JobName.QUEUE_RECOGNIZE_FACES; data: IBaseJob }
 | 
					  | { name: JobName.QUEUE_RECOGNIZE_FACES; data: IBaseJob }
 | 
				
			||||||
  | { name: JobName.RECOGNIZE_FACES; data: IEntityJob }
 | 
					  | { name: JobName.RECOGNIZE_FACES; data: IEntityJob }
 | 
				
			||||||
  | { name: JobName.GENERATE_FACE_THUMBNAIL; data: IFaceThumbnailJob }
 | 
					  | { name: JobName.GENERATE_PERSON_THUMBNAIL; data: IEntityJob }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Clip Embedding
 | 
					  // Clip Embedding
 | 
				
			||||||
  | { name: JobName.QUEUE_ENCODE_CLIP; data: IBaseJob }
 | 
					  | { name: JobName.QUEUE_ENCODE_CLIP; data: IBaseJob }
 | 
				
			||||||
 | 
				
			|||||||
@ -73,19 +73,8 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
      expect(personMock.getAll).toHaveBeenCalled();
 | 
					      expect(personMock.getAll).toHaveBeenCalled();
 | 
				
			||||||
      expect(personMock.getAllWithoutThumbnail).not.toHaveBeenCalled();
 | 
					      expect(personMock.getAllWithoutThumbnail).not.toHaveBeenCalled();
 | 
				
			||||||
      expect(jobMock.queue).toHaveBeenCalledWith({
 | 
					      expect(jobMock.queue).toHaveBeenCalledWith({
 | 
				
			||||||
        name: JobName.GENERATE_FACE_THUMBNAIL,
 | 
					        name: JobName.GENERATE_PERSON_THUMBNAIL,
 | 
				
			||||||
        data: {
 | 
					        data: { id: personStub.newThumbnail.id },
 | 
				
			||||||
          imageWidth: faceStub.face1.imageWidth,
 | 
					 | 
				
			||||||
          imageHeight: faceStub.face1.imageHeight,
 | 
					 | 
				
			||||||
          boundingBox: {
 | 
					 | 
				
			||||||
            x1: faceStub.face1.boundingBoxX1,
 | 
					 | 
				
			||||||
            x2: faceStub.face1.boundingBoxX2,
 | 
					 | 
				
			||||||
            y1: faceStub.face1.boundingBoxY1,
 | 
					 | 
				
			||||||
            y2: faceStub.face1.boundingBoxY2,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          assetId: faceStub.face1.assetId,
 | 
					 | 
				
			||||||
          personId: personStub.newThumbnail.id,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -106,18 +95,9 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
      expect(personMock.getAllWithoutThumbnail).toHaveBeenCalled();
 | 
					      expect(personMock.getAllWithoutThumbnail).toHaveBeenCalled();
 | 
				
			||||||
      expect(personMock.getRandomFace).toHaveBeenCalled();
 | 
					      expect(personMock.getRandomFace).toHaveBeenCalled();
 | 
				
			||||||
      expect(jobMock.queue).toHaveBeenCalledWith({
 | 
					      expect(jobMock.queue).toHaveBeenCalledWith({
 | 
				
			||||||
        name: JobName.GENERATE_FACE_THUMBNAIL,
 | 
					        name: JobName.GENERATE_PERSON_THUMBNAIL,
 | 
				
			||||||
        data: {
 | 
					        data: {
 | 
				
			||||||
          imageWidth: faceStub.face1.imageWidth,
 | 
					          id: personStub.newThumbnail.id,
 | 
				
			||||||
          imageHeight: faceStub.face1.imageHeight,
 | 
					 | 
				
			||||||
          boundingBox: {
 | 
					 | 
				
			||||||
            x1: faceStub.face1.boundingBoxX1,
 | 
					 | 
				
			||||||
            x2: faceStub.face1.boundingBoxX2,
 | 
					 | 
				
			||||||
            y1: faceStub.face1.boundingBoxY1,
 | 
					 | 
				
			||||||
            y2: faceStub.face1.boundingBoxY2,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          assetId: faceStub.face1.assetId,
 | 
					 | 
				
			||||||
          personId: personStub.newThumbnail.id,
 | 
					 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
				
			|||||||
@ -53,27 +53,16 @@ export class MediaService {
 | 
				
			|||||||
    const people = force ? await this.personRepository.getAll() : await this.personRepository.getAllWithoutThumbnail();
 | 
					    const people = force ? await this.personRepository.getAll() : await this.personRepository.getAllWithoutThumbnail();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const person of people) {
 | 
					    for (const person of people) {
 | 
				
			||||||
      // use stored asset for generating thumbnail or pick a random one if not present
 | 
					      if (!person.faceAssetId) {
 | 
				
			||||||
      const face = person.faceAssetId
 | 
					        const face = await this.personRepository.getRandomFace(person.id);
 | 
				
			||||||
        ? await this.personRepository.getFaceById({ personId: person.id, assetId: person.faceAssetId })
 | 
					        if (!face) {
 | 
				
			||||||
        : await this.personRepository.getRandomFace(person.id);
 | 
					          continue;
 | 
				
			||||||
      if (face) {
 | 
					 | 
				
			||||||
        await this.jobRepository.queue({
 | 
					 | 
				
			||||||
          name: JobName.GENERATE_FACE_THUMBNAIL,
 | 
					 | 
				
			||||||
          data: {
 | 
					 | 
				
			||||||
            imageWidth: face.imageWidth,
 | 
					 | 
				
			||||||
            imageHeight: face.imageHeight,
 | 
					 | 
				
			||||||
            boundingBox: {
 | 
					 | 
				
			||||||
              x1: face.boundingBoxX1,
 | 
					 | 
				
			||||||
              x2: face.boundingBoxX2,
 | 
					 | 
				
			||||||
              y1: face.boundingBoxY1,
 | 
					 | 
				
			||||||
              y2: face.boundingBoxY2,
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            assetId: face.assetId,
 | 
					 | 
				
			||||||
            personId: person.id,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await this.personRepository.update({ id: person.id, faceAssetId: face.assetId });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: person.id } });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
 | 
				
			|||||||
@ -249,7 +249,9 @@ describe(PersonService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it("should update a person's thumbnailPath", async () => {
 | 
					    it("should update a person's thumbnailPath", async () => {
 | 
				
			||||||
      personMock.getById.mockResolvedValue(personStub.withName);
 | 
					      personMock.getById.mockResolvedValue(personStub.withName);
 | 
				
			||||||
 | 
					      personMock.update.mockResolvedValue(personStub.withName);
 | 
				
			||||||
      personMock.getFaceById.mockResolvedValue(faceStub.face1);
 | 
					      personMock.getFaceById.mockResolvedValue(faceStub.face1);
 | 
				
			||||||
 | 
					      accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
 | 
				
			||||||
      accessMock.person.hasOwnerAccess.mockResolvedValue(true);
 | 
					      accessMock.person.hasOwnerAccess.mockResolvedValue(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await expect(
 | 
					      await expect(
 | 
				
			||||||
@ -257,25 +259,12 @@ describe(PersonService.name, () => {
 | 
				
			|||||||
      ).resolves.toEqual(responseDto);
 | 
					      ).resolves.toEqual(responseDto);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(personMock.getById).toHaveBeenCalledWith('person-1');
 | 
					      expect(personMock.getById).toHaveBeenCalledWith('person-1');
 | 
				
			||||||
 | 
					      expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', faceAssetId: faceStub.face1.assetId });
 | 
				
			||||||
      expect(personMock.getFaceById).toHaveBeenCalledWith({
 | 
					      expect(personMock.getFaceById).toHaveBeenCalledWith({
 | 
				
			||||||
        assetId: faceStub.face1.assetId,
 | 
					        assetId: faceStub.face1.assetId,
 | 
				
			||||||
        personId: 'person-1',
 | 
					        personId: 'person-1',
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      expect(jobMock.queue).toHaveBeenCalledWith({
 | 
					      expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: 'person-1' } });
 | 
				
			||||||
        name: JobName.GENERATE_FACE_THUMBNAIL,
 | 
					 | 
				
			||||||
        data: {
 | 
					 | 
				
			||||||
          assetId: faceStub.face1.assetId,
 | 
					 | 
				
			||||||
          personId: 'person-1',
 | 
					 | 
				
			||||||
          boundingBox: {
 | 
					 | 
				
			||||||
            x1: faceStub.face1.boundingBoxX1,
 | 
					 | 
				
			||||||
            x2: faceStub.face1.boundingBoxX2,
 | 
					 | 
				
			||||||
            y1: faceStub.face1.boundingBoxY1,
 | 
					 | 
				
			||||||
            y2: faceStub.face1.boundingBoxY2,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          imageHeight: faceStub.face1.imageHeight,
 | 
					 | 
				
			||||||
          imageWidth: faceStub.face1.imageWidth,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
 | 
					      expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1');
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -77,8 +77,10 @@ export class PersonService {
 | 
				
			|||||||
    await this.access.requirePermission(authUser, Permission.PERSON_WRITE, id);
 | 
					    await this.access.requirePermission(authUser, Permission.PERSON_WRITE, id);
 | 
				
			||||||
    let person = await this.findOrFail(id);
 | 
					    let person = await this.findOrFail(id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (dto.name !== undefined || dto.birthDate !== undefined || dto.isHidden !== undefined) {
 | 
					    const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto;
 | 
				
			||||||
      person = await this.repository.update({ id, name: dto.name, birthDate: dto.birthDate, isHidden: dto.isHidden });
 | 
					
 | 
				
			||||||
 | 
					    if (name !== undefined || birthDate !== undefined || isHidden !== undefined) {
 | 
				
			||||||
 | 
					      person = await this.repository.update({ id, name, birthDate, isHidden });
 | 
				
			||||||
      if (this.needsSearchIndexUpdate(dto)) {
 | 
					      if (this.needsSearchIndexUpdate(dto)) {
 | 
				
			||||||
        const assets = await this.repository.getAssets(id);
 | 
					        const assets = await this.repository.getAssets(id);
 | 
				
			||||||
        const ids = assets.map((asset) => asset.id);
 | 
					        const ids = assets.map((asset) => asset.id);
 | 
				
			||||||
@ -86,28 +88,15 @@ export class PersonService {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (dto.featureFaceAssetId) {
 | 
					    if (assetId) {
 | 
				
			||||||
      const assetId = dto.featureFaceAssetId;
 | 
					      await this.access.requirePermission(authUser, Permission.ASSET_READ, assetId);
 | 
				
			||||||
      const face = await this.repository.getFaceById({ personId: id, assetId });
 | 
					      const face = await this.repository.getFaceById({ personId: id, assetId });
 | 
				
			||||||
      if (!face) {
 | 
					      if (!face) {
 | 
				
			||||||
        throw new BadRequestException('Invalid assetId for feature face');
 | 
					        throw new BadRequestException('Invalid assetId for feature face');
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await this.jobRepository.queue({
 | 
					      person = await this.repository.update({ id, faceAssetId: assetId });
 | 
				
			||||||
        name: JobName.GENERATE_FACE_THUMBNAIL,
 | 
					      await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } });
 | 
				
			||||||
        data: {
 | 
					 | 
				
			||||||
          personId: id,
 | 
					 | 
				
			||||||
          assetId,
 | 
					 | 
				
			||||||
          boundingBox: {
 | 
					 | 
				
			||||||
            x1: face.boundingBoxX1,
 | 
					 | 
				
			||||||
            x2: face.boundingBoxX2,
 | 
					 | 
				
			||||||
            y1: face.boundingBoxY1,
 | 
					 | 
				
			||||||
            y2: face.boundingBoxY2,
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          imageHeight: face.imageHeight,
 | 
					 | 
				
			||||||
          imageWidth: face.imageWidth,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return mapPerson(person);
 | 
					    return mapPerson(person);
 | 
				
			||||||
 | 
				
			|||||||
@ -70,7 +70,7 @@ export class JobRepository implements IJobRepository {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private getJobOptions(item: JobItem): JobsOptions | null {
 | 
					  private getJobOptions(item: JobItem): JobsOptions | null {
 | 
				
			||||||
    switch (item.name) {
 | 
					    switch (item.name) {
 | 
				
			||||||
      case JobName.GENERATE_FACE_THUMBNAIL:
 | 
					      case JobName.GENERATE_PERSON_THUMBNAIL:
 | 
				
			||||||
        return { priority: 1 };
 | 
					        return { priority: 1 };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      default:
 | 
					      default:
 | 
				
			||||||
 | 
				
			|||||||
@ -78,7 +78,7 @@ export class AppService {
 | 
				
			|||||||
      [JobName.LINK_LIVE_PHOTOS]: (data) => this.metadataProcessor.handleLivePhotoLinking(data),
 | 
					      [JobName.LINK_LIVE_PHOTOS]: (data) => this.metadataProcessor.handleLivePhotoLinking(data),
 | 
				
			||||||
      [JobName.QUEUE_RECOGNIZE_FACES]: (data) => this.facialRecognitionService.handleQueueRecognizeFaces(data),
 | 
					      [JobName.QUEUE_RECOGNIZE_FACES]: (data) => this.facialRecognitionService.handleQueueRecognizeFaces(data),
 | 
				
			||||||
      [JobName.RECOGNIZE_FACES]: (data) => this.facialRecognitionService.handleRecognizeFaces(data),
 | 
					      [JobName.RECOGNIZE_FACES]: (data) => this.facialRecognitionService.handleRecognizeFaces(data),
 | 
				
			||||||
      [JobName.GENERATE_FACE_THUMBNAIL]: (data) => this.facialRecognitionService.handleGenerateFaceThumbnail(data),
 | 
					      [JobName.GENERATE_PERSON_THUMBNAIL]: (data) => this.facialRecognitionService.handleGeneratePersonThumbnail(data),
 | 
				
			||||||
      [JobName.PERSON_CLEANUP]: () => this.personService.handlePersonCleanup(),
 | 
					      [JobName.PERSON_CLEANUP]: () => this.personService.handlePersonCleanup(),
 | 
				
			||||||
      [JobName.QUEUE_SIDECAR]: (data) => this.metadataService.handleQueueSidecar(data),
 | 
					      [JobName.QUEUE_SIDECAR]: (data) => this.metadataService.handleQueueSidecar(data),
 | 
				
			||||||
      [JobName.SIDECAR_DISCOVERY]: (data) => this.metadataService.handleSidecarDiscovery(data),
 | 
					      [JobName.SIDECAR_DISCOVERY]: (data) => this.metadataService.handleSidecarDiscovery(data),
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										39
									
								
								server/test/fixtures/face.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										39
									
								
								server/test/fixtures/face.stub.ts
									
									
									
									
										vendored
									
									
								
							@ -55,4 +55,43 @@ export const faceStub = {
 | 
				
			|||||||
    imageHeight: 1024,
 | 
					    imageHeight: 1024,
 | 
				
			||||||
    imageWidth: 1024,
 | 
					    imageWidth: 1024,
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
 | 
					  start: Object.freeze<AssetFaceEntity>({
 | 
				
			||||||
 | 
					    assetId: assetStub.image.id,
 | 
				
			||||||
 | 
					    asset: assetStub.image,
 | 
				
			||||||
 | 
					    personId: personStub.newThumbnail.id,
 | 
				
			||||||
 | 
					    person: personStub.newThumbnail,
 | 
				
			||||||
 | 
					    embedding: [1, 2, 3, 4],
 | 
				
			||||||
 | 
					    boundingBoxX1: 5,
 | 
				
			||||||
 | 
					    boundingBoxY1: 5,
 | 
				
			||||||
 | 
					    boundingBoxX2: 505,
 | 
				
			||||||
 | 
					    boundingBoxY2: 505,
 | 
				
			||||||
 | 
					    imageHeight: 1000,
 | 
				
			||||||
 | 
					    imageWidth: 1000,
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					  middle: Object.freeze<AssetFaceEntity>({
 | 
				
			||||||
 | 
					    assetId: assetStub.image.id,
 | 
				
			||||||
 | 
					    asset: assetStub.image,
 | 
				
			||||||
 | 
					    personId: personStub.newThumbnail.id,
 | 
				
			||||||
 | 
					    person: personStub.newThumbnail,
 | 
				
			||||||
 | 
					    embedding: [1, 2, 3, 4],
 | 
				
			||||||
 | 
					    boundingBoxX1: 100,
 | 
				
			||||||
 | 
					    boundingBoxY1: 100,
 | 
				
			||||||
 | 
					    boundingBoxX2: 200,
 | 
				
			||||||
 | 
					    boundingBoxY2: 200,
 | 
				
			||||||
 | 
					    imageHeight: 500,
 | 
				
			||||||
 | 
					    imageWidth: 400,
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					  end: Object.freeze<AssetFaceEntity>({
 | 
				
			||||||
 | 
					    assetId: assetStub.image.id,
 | 
				
			||||||
 | 
					    asset: assetStub.image,
 | 
				
			||||||
 | 
					    personId: personStub.newThumbnail.id,
 | 
				
			||||||
 | 
					    person: personStub.newThumbnail,
 | 
				
			||||||
 | 
					    embedding: [1, 2, 3, 4],
 | 
				
			||||||
 | 
					    boundingBoxX1: 300,
 | 
				
			||||||
 | 
					    boundingBoxY1: 300,
 | 
				
			||||||
 | 
					    boundingBoxX2: 495,
 | 
				
			||||||
 | 
					    boundingBoxY2: 495,
 | 
				
			||||||
 | 
					    imageHeight: 500,
 | 
				
			||||||
 | 
					    imageWidth: 500,
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user