Added machine learning microservice and object detection (#76)

This commit is contained in:
Alex 2022-03-27 14:58:54 -05:00 committed by GitHub
parent fe693db84f
commit dd9c5244fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 11555 additions and 278 deletions

View file

@ -16,13 +16,12 @@ import {
} from '@nestjs/common';
import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
import { AssetService } from './asset.service';
import { FileFieldsInterceptor, FilesInterceptor } from '@nestjs/platform-express';
import { FileFieldsInterceptor } from '@nestjs/platform-express';
import { multerOption } from '../../config/multer-option.config';
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
import { CreateAssetDto } from './dto/create-asset.dto';
import { ServeFileDto } from './dto/serve-file.dto';
import { AssetOptimizeService } from '../../modules/image-optimize/image-optimize.service';
import { AssetEntity, AssetType } from './entities/asset.entity';
import { AssetEntity } from './entities/asset.entity';
import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
import { Response as Res } from 'express';
import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto';
@ -61,6 +60,7 @@ export class AssetController {
if (uploadFiles.thumbnailData != null) {
await this.assetService.updateThumbnailInfo(savedAsset.id, uploadFiles.thumbnailData[0].path);
await this.backgroundTaskService.tagImage(uploadFiles.thumbnailData[0].path, savedAsset);
await this.backgroundTaskService.detectObject(uploadFiles.thumbnailData[0].path, savedAsset);
}
await this.backgroundTaskService.extractExif(savedAsset, file.originalname, file.size);
@ -81,6 +81,11 @@ export class AssetController {
return this.assetService.serveFile(authUser, query, res, headers);
}
@Get('/allObjects')
async getCuratedObject(@GetAuthUser() authUser: AuthUserDto) {
return this.assetService.getCuratedObject(authUser);
}
@Get('/allLocation')
async getCuratedLocation(@GetAuthUser() authUser: AuthUserDto) {
return this.assetService.getCuratedLocation(authUser);

View file

@ -3,9 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm';
import { MoreThan, Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { CreateAssetDto } from './dto/create-asset.dto';
import { UpdateAssetDto } from './dto/update-asset.dto';
import { AssetEntity, AssetType } from './entities/asset.entity';
import _, { result } from 'lodash';
import _ from 'lodash';
import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
import { GetAllAssetReponseDto } from './dto/get-all-asset-response.dto';
import { createReadStream, stat } from 'fs';
@ -44,9 +43,7 @@ export class AssetService {
asset.duration = assetInfo.duration;
try {
const res = await this.assetRepository.save(asset);
return res;
return await this.assetRepository.save(asset);
} catch (e) {
Logger.error(`Error Create New Asset ${e}`, 'createUserAsset');
}
@ -68,13 +65,11 @@ export class AssetService {
public async getAllAssetsNoPagination(authUser: AuthUserDto) {
try {
const assets = await this.assetRepository
return await this.assetRepository
.createQueryBuilder('a')
.where('a."userId" = :userId', { userId: authUser.id })
.orderBy('a."createdAt"::date', 'DESC')
.getMany();
return assets;
} catch (e) {
Logger.error(e, 'getAllAssets');
}
@ -226,10 +221,10 @@ export class AssetService {
}
public async deleteAssetById(authUser: AuthUserDto, assetIds: DeleteAssetDto) {
let result = [];
const result = [];
const target = assetIds.ids;
for (let assetId of target) {
for (const assetId of target) {
const res = await this.assetRepository.delete({
id: assetId,
userId: authUser.id,
@ -251,11 +246,11 @@ export class AssetService {
return result;
}
async getAssetSearchTerm(authUser: AuthUserDto): Promise<String[]> {
const possibleSearchTerm = new Set<String>();
async getAssetSearchTerm(authUser: AuthUserDto): Promise<string[]> {
const possibleSearchTerm = new Set<string>();
const rows = await this.assetRepository.query(
`
select distinct si.tags, e.orientation, e."lensModel", e.make, e.model , a.type, e.city, e.state, e.country
select distinct si.tags, si.objects, e.orientation, e."lensModel", e.make, e.model , a.type, e.city, e.state, e.country
from assets a
left join exif e on a.id = e."assetId"
left join smart_info si on a.id = si."assetId"
@ -268,6 +263,9 @@ export class AssetService {
// tags
row['tags']?.map((tag) => possibleSearchTerm.add(tag?.toLowerCase()));
// objects
row['objects']?.map((object) => possibleSearchTerm.add(object?.toLowerCase()));
// asset's tyoe
possibleSearchTerm.add(row['type']?.toLowerCase());
@ -300,18 +298,17 @@ export class AssetService {
WHERE a."userId" = $1
AND
(
TO_TSVECTOR('english', ARRAY_TO_STRING(si.tags, ',')) @@ PLAINTO_TSQUERY('english', $2) OR
TO_TSVECTOR('english', ARRAY_TO_STRING(si.tags, ',')) @@ PLAINTO_TSQUERY('english', $2) OR
TO_TSVECTOR('english', ARRAY_TO_STRING(si.objects, ',')) @@ PLAINTO_TSQUERY('english', $2) OR
e.exif_text_searchable_column @@ PLAINTO_TSQUERY('english', $2)
);
`;
const rows = await this.assetRepository.query(query, [authUser.id, searchAssetDto.searchTerm]);
return rows;
return await this.assetRepository.query(query, [authUser.id, searchAssetDto.searchTerm]);
}
async getCuratedLocation(authUser: AuthUserDto) {
const rows = await this.assetRepository.query(
return await this.assetRepository.query(
`
select distinct on (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId"
from assets a
@ -322,7 +319,18 @@ export class AssetService {
`,
[authUser.id],
);
}
return rows;
async getCuratedObject(authUser: AuthUserDto) {
return await this.assetRepository.query(
`
select distinct on (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."resizePath", a."deviceAssetId", a."deviceId"
from assets a
left join smart_info si on a.id = si."assetId"
where a."userId" = $1
and si.objects is not null
`,
[authUser.id],
);
}
}

View file

@ -13,6 +13,9 @@ export class SmartInfoEntity {
@Column({ type: 'text', array: true, nullable: true })
tags: string[];
@Column({ type: 'text', array: true, nullable: true })
objects: string[];
@OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'assetId', referencedColumnName: 'id' })
asset: SmartInfoEntity;

View file

@ -5,7 +5,6 @@ import { UserModule } from './api-v1/user/user.module';
import { AssetModule } from './api-v1/asset/asset.module';
import { AuthModule } from './api-v1/auth/auth.module';
import { ImmichJwtModule } from './modules/immich-jwt/immich-jwt.module';
import { JwtModule } from '@nestjs/jwt';
import { DeviceInfoModule } from './api-v1/device-info/device-info.module';
import { AppLoggerMiddleware } from './middlewares/app-logger.middleware';
import { ConfigModule, ConfigService } from '@nestjs/config';
@ -26,14 +25,12 @@ import { CommunicationModule } from './api-v1/communication/communication.module
ImmichJwtModule,
DeviceInfoModule,
BullModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
useFactory: async () => ({
redis: {
host: 'immich_redis',
port: 6379,
},
}),
inject: [ConfigService],
}),
ImageOptimizeModule,

View file

@ -0,0 +1,20 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddObjectColumnToSmartInfo1648317474768
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE smart_info
ADD COLUMN objects text[];
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE smart_info
DROP COLUMN objects;
`);
}
}

View file

@ -6,7 +6,7 @@ import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
import { ConfigService } from '@nestjs/config';
import exifr from 'exifr';
import { readFile } from 'fs/promises';
import fs, { rmSync } from 'fs';
import fs from 'fs';
import { Logger } from '@nestjs/common';
import { ExifEntity } from '../../api-v1/asset/entities/exif.entity';
import axios from 'axios';
@ -114,14 +114,37 @@ export class BackgroundTaskProcessor {
@Process('tag-image')
async tagImage(job) {
const { thumbnailPath, asset }: { thumbnailPath: string; asset: AssetEntity } = job.data;
const res = await axios.post('http://immich_tf_fastapi:8000/tagImage', { thumbnail_path: thumbnailPath });
if (res.status == 200) {
const res = await axios.post('http://immich_microservices:3001/image-classifier/tagImage', {
thumbnailPath: thumbnailPath,
});
if (res.status == 201 && res.data.length > 0) {
const smartInfo = new SmartInfoEntity();
smartInfo.assetId = asset.id;
smartInfo.tags = [...res.data];
await this.smartInfoRepository.save(smartInfo);
await this.smartInfoRepository.upsert(smartInfo, {
conflictPaths: ['assetId'],
});
}
}
@Process('detect-object')
async detectObject(job) {
const { thumbnailPath, asset }: { thumbnailPath: string; asset: AssetEntity } = job.data;
const res = await axios.post('http://immich_microservices:3001/object-detection/detectObject', {
thumbnailPath: thumbnailPath,
});
if (res.status == 201 && res.data.length > 0) {
const smartInfo = new SmartInfoEntity();
smartInfo.assetId = asset.id;
smartInfo.objects = [...res.data];
await this.smartInfoRepository.upsert(smartInfo, {
conflictPaths: ['assetId'],
});
}
}
}

View file

@ -43,4 +43,15 @@ export class BackgroundTaskService {
{ jobId: randomUUID() },
);
}
async detectObject(thumbnailPath: string, asset: AssetEntity) {
await this.backgroundTaskQueue.add(
'detect-object',
{
thumbnailPath,
asset,
},
{ jobId: randomUUID() },
);
}
}