mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Merge branch 'main' of https://github.com/immich-app/immich into feat/crawl-wrapper
This commit is contained in:
@@ -18,14 +18,14 @@ export class AssetFaceFactory {
|
||||
static from(dto: AssetFaceLike = {}) {
|
||||
return new AssetFaceFactory({
|
||||
assetId: newUuid(),
|
||||
boundingBoxX1: 11,
|
||||
boundingBoxX2: 12,
|
||||
boundingBoxY1: 21,
|
||||
boundingBoxY2: 22,
|
||||
boundingBoxX1: 100,
|
||||
boundingBoxX2: 200,
|
||||
boundingBoxY1: 100,
|
||||
boundingBoxY2: 200,
|
||||
deletedAt: null,
|
||||
id: newUuid(),
|
||||
imageHeight: 42,
|
||||
imageWidth: 420,
|
||||
imageHeight: 500,
|
||||
imageWidth: 400,
|
||||
isVisible: true,
|
||||
personId: null,
|
||||
sourceType: SourceType.MachineLearning,
|
||||
|
||||
@@ -96,7 +96,7 @@ export class AssetFactory {
|
||||
}
|
||||
|
||||
face(dto: AssetFaceLike = {}, builder?: FactoryBuilder<AssetFaceFactory>) {
|
||||
this.#faces.push(build(AssetFaceFactory.from(dto), builder));
|
||||
this.#faces.push(build(AssetFaceFactory.from({ assetId: this.value?.id, ...dto }), builder));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
Vendored
-160
@@ -1,160 +0,0 @@
|
||||
import { SourceType } from 'src/enum';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
import { personStub } from 'test/fixtures/person.stub';
|
||||
|
||||
export const faceStub = {
|
||||
face1: Object.freeze({
|
||||
id: 'assetFaceId1',
|
||||
assetId: 'asset-id',
|
||||
asset: {
|
||||
...AssetFactory.create({ id: 'asset-id' }),
|
||||
libraryId: null,
|
||||
updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125',
|
||||
stackId: null,
|
||||
},
|
||||
personId: personStub.withName.id,
|
||||
person: personStub.withName,
|
||||
boundingBoxX1: 0,
|
||||
boundingBoxY1: 0,
|
||||
boundingBoxX2: 1,
|
||||
boundingBoxY2: 1,
|
||||
imageHeight: 1024,
|
||||
imageWidth: 1024,
|
||||
sourceType: SourceType.MachineLearning,
|
||||
faceSearch: { faceId: 'assetFaceId1', embedding: '[1, 2, 3, 4]' },
|
||||
deletedAt: new Date(),
|
||||
updatedAt: new Date('2023-01-01T00:00:00Z'),
|
||||
updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125',
|
||||
isVisible: true,
|
||||
}),
|
||||
primaryFace1: Object.freeze({
|
||||
id: 'assetFaceId2',
|
||||
assetId: 'asset-id',
|
||||
asset: AssetFactory.create({ id: 'asset-id' }),
|
||||
personId: personStub.primaryPerson.id,
|
||||
person: personStub.primaryPerson,
|
||||
boundingBoxX1: 0,
|
||||
boundingBoxY1: 0,
|
||||
boundingBoxX2: 1,
|
||||
boundingBoxY2: 1,
|
||||
imageHeight: 1024,
|
||||
imageWidth: 1024,
|
||||
sourceType: SourceType.MachineLearning,
|
||||
faceSearch: { faceId: 'assetFaceId2', embedding: '[1, 2, 3, 4]' },
|
||||
deletedAt: null,
|
||||
updatedAt: new Date('2023-01-01T00:00:00Z'),
|
||||
updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125',
|
||||
isVisible: true,
|
||||
}),
|
||||
mergeFace1: Object.freeze({
|
||||
id: 'assetFaceId3',
|
||||
assetId: 'asset-id',
|
||||
asset: AssetFactory.create({ id: 'asset-id' }),
|
||||
personId: personStub.mergePerson.id,
|
||||
person: personStub.mergePerson,
|
||||
boundingBoxX1: 0,
|
||||
boundingBoxY1: 0,
|
||||
boundingBoxX2: 1,
|
||||
boundingBoxY2: 1,
|
||||
imageHeight: 1024,
|
||||
imageWidth: 1024,
|
||||
sourceType: SourceType.MachineLearning,
|
||||
faceSearch: { faceId: 'assetFaceId3', embedding: '[1, 2, 3, 4]' },
|
||||
deletedAt: null,
|
||||
updatedAt: new Date('2023-01-01T00:00:00Z'),
|
||||
updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125',
|
||||
isVisible: true,
|
||||
}),
|
||||
noPerson1: Object.freeze({
|
||||
id: 'assetFaceId8',
|
||||
assetId: 'asset-id',
|
||||
asset: AssetFactory.create({ id: 'asset-id' }),
|
||||
personId: null,
|
||||
person: null,
|
||||
boundingBoxX1: 0,
|
||||
boundingBoxY1: 0,
|
||||
boundingBoxX2: 1,
|
||||
boundingBoxY2: 1,
|
||||
imageHeight: 1024,
|
||||
imageWidth: 1024,
|
||||
sourceType: SourceType.MachineLearning,
|
||||
faceSearch: { faceId: 'assetFaceId8', embedding: '[1, 2, 3, 4]' },
|
||||
deletedAt: null,
|
||||
updatedAt: new Date('2023-01-01T00:00:00Z'),
|
||||
updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125',
|
||||
isVisible: true,
|
||||
}),
|
||||
noPerson2: Object.freeze({
|
||||
id: 'assetFaceId9',
|
||||
assetId: 'asset-id',
|
||||
asset: AssetFactory.create({ id: 'asset-id' }),
|
||||
personId: null,
|
||||
person: null,
|
||||
boundingBoxX1: 0,
|
||||
boundingBoxY1: 0,
|
||||
boundingBoxX2: 1,
|
||||
boundingBoxY2: 1,
|
||||
imageHeight: 1024,
|
||||
imageWidth: 1024,
|
||||
sourceType: SourceType.MachineLearning,
|
||||
faceSearch: { faceId: 'assetFaceId9', embedding: '[1, 2, 3, 4]' },
|
||||
deletedAt: null,
|
||||
updatedAt: new Date('2023-01-01T00:00:00Z'),
|
||||
updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125',
|
||||
isVisible: true,
|
||||
}),
|
||||
fromExif1: Object.freeze({
|
||||
id: 'assetFaceId9',
|
||||
assetId: 'asset-id',
|
||||
asset: AssetFactory.create({ id: 'asset-id' }),
|
||||
personId: personStub.randomPerson.id,
|
||||
person: personStub.randomPerson,
|
||||
boundingBoxX1: 100,
|
||||
boundingBoxY1: 100,
|
||||
boundingBoxX2: 200,
|
||||
boundingBoxY2: 200,
|
||||
imageHeight: 500,
|
||||
imageWidth: 400,
|
||||
sourceType: SourceType.Exif,
|
||||
deletedAt: null,
|
||||
updatedAt: new Date('2023-01-01T00:00:00Z'),
|
||||
updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125',
|
||||
isVisible: true,
|
||||
}),
|
||||
fromExif2: Object.freeze({
|
||||
id: 'assetFaceId9',
|
||||
assetId: 'asset-id',
|
||||
asset: AssetFactory.create({ id: 'asset-id' }),
|
||||
personId: personStub.randomPerson.id,
|
||||
person: personStub.randomPerson,
|
||||
boundingBoxX1: 0,
|
||||
boundingBoxY1: 0,
|
||||
boundingBoxX2: 1,
|
||||
boundingBoxY2: 1,
|
||||
imageHeight: 1024,
|
||||
imageWidth: 1024,
|
||||
sourceType: SourceType.Exif,
|
||||
deletedAt: null,
|
||||
updatedAt: new Date('2023-01-01T00:00:00Z'),
|
||||
updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125',
|
||||
isVisible: true,
|
||||
}),
|
||||
withBirthDate: Object.freeze({
|
||||
id: 'assetFaceId10',
|
||||
assetId: 'asset-id',
|
||||
asset: AssetFactory.create({ id: 'asset-id' }),
|
||||
personId: personStub.withBirthDate.id,
|
||||
person: personStub.withBirthDate,
|
||||
boundingBoxX1: 0,
|
||||
boundingBoxY1: 0,
|
||||
boundingBoxX2: 1,
|
||||
boundingBoxY2: 1,
|
||||
imageHeight: 1024,
|
||||
imageWidth: 1024,
|
||||
sourceType: SourceType.MachineLearning,
|
||||
deletedAt: null,
|
||||
updatedAt: new Date('2023-01-01T00:00:00Z'),
|
||||
updateId: '0d1173e3-4d80-4d76-b41e-57d56de21125',
|
||||
isVisible: true,
|
||||
}),
|
||||
};
|
||||
@@ -1,3 +1,6 @@
|
||||
import { Selectable } from 'kysely';
|
||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||
import { AssetFaceFactory } from 'test/factories/asset-face.factory';
|
||||
import { AssetFactory } from 'test/factories/asset.factory';
|
||||
|
||||
export const getForStorageTemplate = (asset: ReturnType<AssetFactory['build']>) => {
|
||||
@@ -20,3 +23,29 @@ export const getForStorageTemplate = (asset: ReturnType<AssetFactory['build']>)
|
||||
isEdited: asset.isEdited,
|
||||
};
|
||||
};
|
||||
|
||||
export const getAsDetectedFace = (face: ReturnType<AssetFaceFactory['build']>) => ({
|
||||
faces: [
|
||||
{
|
||||
boundingBox: {
|
||||
x1: face.boundingBoxX1,
|
||||
y1: face.boundingBoxY1,
|
||||
x2: face.boundingBoxX2,
|
||||
y2: face.boundingBoxY2,
|
||||
},
|
||||
embedding: '[1, 2, 3, 4]',
|
||||
score: 0.2,
|
||||
},
|
||||
],
|
||||
imageHeight: face.imageHeight,
|
||||
imageWidth: face.imageWidth,
|
||||
});
|
||||
|
||||
export const getForFacialRecognitionJob = (
|
||||
face: ReturnType<AssetFaceFactory['build']>,
|
||||
asset: Pick<Selectable<AssetTable>, 'ownerId' | 'visibility' | 'fileCreatedAt'> | null,
|
||||
) => ({
|
||||
...face,
|
||||
asset,
|
||||
faceSearch: { faceId: face.id, embedding: '[1, 2, 3, 4]' },
|
||||
});
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { Kysely } from 'kysely';
|
||||
import { AssetEditAction } from 'src/dtos/editing.dto';
|
||||
import { AssetFileType, AssetMetadataKey, AssetStatus, JobName, SharedLinkType } from 'src/enum';
|
||||
import { AccessRepository } from 'src/repositories/access.repository';
|
||||
import { AlbumRepository } from 'src/repositories/album.repository';
|
||||
import { AssetEditRepository } from 'src/repositories/asset-edit.repository';
|
||||
import { AssetJobRepository } from 'src/repositories/asset-job.repository';
|
||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||
import { EventRepository } from 'src/repositories/event.repository';
|
||||
import { JobRepository } from 'src/repositories/job.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { OcrRepository } from 'src/repositories/ocr.repository';
|
||||
import { SharedLinkAssetRepository } from 'src/repositories/shared-link-asset.repository';
|
||||
import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
|
||||
import { StackRepository } from 'src/repositories/stack.repository';
|
||||
@@ -25,6 +28,7 @@ const setup = (db?: Kysely<DB>) => {
|
||||
database: db || defaultDatabase,
|
||||
real: [
|
||||
AssetRepository,
|
||||
AssetEditRepository,
|
||||
AssetJobRepository,
|
||||
AlbumRepository,
|
||||
AccessRepository,
|
||||
@@ -32,7 +36,7 @@ const setup = (db?: Kysely<DB>) => {
|
||||
StackRepository,
|
||||
UserRepository,
|
||||
],
|
||||
mock: [EventRepository, LoggingRepository, JobRepository, StorageRepository],
|
||||
mock: [EventRepository, LoggingRepository, JobRepository, StorageRepository, OcrRepository],
|
||||
});
|
||||
};
|
||||
|
||||
@@ -586,6 +590,108 @@ describe(AssetService.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOcr', () => {
|
||||
it('should require access', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user2.id });
|
||||
|
||||
await expect(sut.getOcr(auth, asset.id)).rejects.toThrow('Not found or no asset.read access');
|
||||
});
|
||||
|
||||
it('should work', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: asset.id, exifImageHeight: 42, exifImageWidth: 69, orientation: '1' });
|
||||
ctx.getMock(OcrRepository).getByAssetId.mockResolvedValue([factory.assetOcr()]);
|
||||
|
||||
await expect(sut.getOcr(auth, asset.id)).resolves.toEqual([
|
||||
expect.objectContaining({ x1: 0.1, x2: 0.3, x3: 0.3, x4: 0.1, y1: 0.2, y2: 0.2, y3: 0.4, y4: 0.4 }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should apply rotation', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: asset.id, exifImageHeight: 42, exifImageWidth: 69, orientation: '1' });
|
||||
await ctx.database
|
||||
.insertInto('asset_edit')
|
||||
.values({ assetId: asset.id, action: AssetEditAction.Rotate, parameters: { angle: 90 }, sequence: 1 })
|
||||
.execute();
|
||||
ctx.getMock(OcrRepository).getByAssetId.mockResolvedValue([factory.assetOcr()]);
|
||||
|
||||
await expect(sut.getOcr(auth, asset.id)).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
x1: 0.6,
|
||||
x2: 0.8,
|
||||
x3: 0.8,
|
||||
x4: 0.6,
|
||||
y1: expect.any(Number),
|
||||
y2: expect.any(Number),
|
||||
y3: 0.3,
|
||||
y4: 0.3,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOcr', () => {
|
||||
it('should require access', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user2.id });
|
||||
|
||||
await expect(sut.getOcr(auth, asset.id)).rejects.toThrow('Not found or no asset.read access');
|
||||
});
|
||||
|
||||
it('should work', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: asset.id, exifImageHeight: 42, exifImageWidth: 69, orientation: '1' });
|
||||
ctx.getMock(OcrRepository).getByAssetId.mockResolvedValue([factory.assetOcr()]);
|
||||
|
||||
await expect(sut.getOcr(auth, asset.id)).resolves.toEqual([
|
||||
expect.objectContaining({ x1: 0.1, x2: 0.3, x3: 0.3, x4: 0.1, y1: 0.2, y2: 0.2, y3: 0.4, y4: 0.4 }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should apply rotation', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: asset.id, exifImageHeight: 42, exifImageWidth: 69, orientation: '1' });
|
||||
await ctx.database
|
||||
.insertInto('asset_edit')
|
||||
.values({ assetId: asset.id, action: AssetEditAction.Rotate, parameters: { angle: 90 }, sequence: 1 })
|
||||
.execute();
|
||||
ctx.getMock(OcrRepository).getByAssetId.mockResolvedValue([factory.assetOcr()]);
|
||||
|
||||
await expect(sut.getOcr(auth, asset.id)).resolves.toEqual([
|
||||
expect.objectContaining({
|
||||
x1: 0.6,
|
||||
x2: 0.8,
|
||||
x3: 0.8,
|
||||
x4: 0.6,
|
||||
y1: expect.any(Number),
|
||||
y2: expect.any(Number),
|
||||
y3: 0.3,
|
||||
y4: 0.3,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('upsertBulkMetadata', () => {
|
||||
it('should work', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
@@ -758,4 +864,38 @@ describe(AssetService.name, () => {
|
||||
expect(metadata).toEqual([expect.objectContaining({ key: 'some-other-key', value: { foo: 'bar' } })]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('editAsset', () => {
|
||||
it('should require access', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const { user: user2 } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user2.id });
|
||||
|
||||
await expect(
|
||||
sut.editAsset(auth, asset.id, { edits: [{ action: AssetEditAction.Rotate, parameters: { angle: 90 } }] }),
|
||||
).rejects.toThrow('Not found or no asset.edit.create access');
|
||||
});
|
||||
|
||||
it('should work', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
ctx.getMock(JobRepository).queue.mockResolvedValue();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: asset.id, exifImageHeight: 42, exifImageWidth: 69, orientation: '1' });
|
||||
|
||||
const editAction = { action: AssetEditAction.Rotate, parameters: { angle: 90 } } as const;
|
||||
await expect(sut.editAsset(auth, asset.id, { edits: [editAction] })).resolves.toEqual({
|
||||
assetId: asset.id,
|
||||
edits: [editAction],
|
||||
});
|
||||
|
||||
await expect(ctx.get(AssetRepository).getById(asset.id)).resolves.toEqual(
|
||||
expect.objectContaining({ isEdited: true }),
|
||||
);
|
||||
await expect(ctx.get(AssetEditRepository).getAll(asset.id)).resolves.toEqual([editAction]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,7 +51,13 @@ export const newAssetRepositoryMock = (): Mocked<RepositoryInterface<AssetReposi
|
||||
deleteMetadataByKey: vitest.fn(),
|
||||
deleteBulkMetadata: vitest.fn(),
|
||||
getForOriginal: vitest.fn(),
|
||||
getForOriginals: vitest.fn(),
|
||||
getForThumbnail: vitest.fn(),
|
||||
getForVideo: vitest.fn(),
|
||||
getForEdit: vitest.fn(),
|
||||
getForOcr: vitest.fn(),
|
||||
getForMetadataExtractionTags: vitest.fn(),
|
||||
getForFaces: vitest.fn(),
|
||||
getForUpdateTags: vitest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user