Implemented image tagging using TensorFlow InceptionV3 (#28)

* Refactor docker-compose to its own folder
* Added FastAPI development environment
* Added support for GPU in docker file
* Added image classification
* creating endpoint for smart Image info
* added logo with white background on ios
* Added endpoint and trigger for image tagging
* Classify image and save into database
* Update readme
This commit is contained in:
Alex
2022-02-19 22:42:10 -06:00
committed by GitHub
parent 75b1ed08b4
commit 619735fea0
54 changed files with 2297 additions and 10672 deletions

View File

@@ -1,13 +0,0 @@
# STAGE
NODE_ENV=development
# Database
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE_NAME=
# Upload File Config
UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
# JWT SECRET
JWT_SECRET=

View File

@@ -1,67 +0,0 @@
version: '3.8'
services:
immich_server:
image: immich-server-dev:1.0.0
build:
context: .
target: development
dockerfile: ./Dockerfile
command: npm run start:dev
expose:
- "3000"
volumes:
- .:/usr/src/app
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /usr/src/app/node_modules
env_file:
- .env
depends_on:
- redis
- database
networks:
- immich_network
redis:
container_name: immich_redis
image: redis:6.2
networks:
- immich_network
database:
container_name: immich_postgres
image: postgres:14
env_file:
- .env
environment:
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME}
PG_DATA: /var/lib/postgresql/data
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- 5432:5432
networks:
- immich_network
nginx:
container_name: proxy_nginx
image: nginx:latest
volumes:
- ./settings/nginx-conf:/etc/nginx/conf.d
ports:
- 2283:80
- 2284:443
logging:
driver: none
networks:
- immich_network
depends_on:
- immich_server
networks:
immich_network:
volumes:
pgdata:

2
server/entrypoint.sh Normal file
View File

@@ -0,0 +1,2 @@
# npm run typeorm migration:run
npm run start:dev

10598
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,8 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
"test:e2e": "jest --config ./test/jest-e2e.json",
"typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js"
},
"dependencies": {
"@nestjs/bull": "^0.4.2",
@@ -34,6 +35,7 @@
"@nestjs/typeorm": "^8.0.3",
"@nestjs/websockets": "^8.2.6",
"@socket.io/redis-adapter": "^7.1.0",
"axios": "^0.26.0",
"bcrypt": "^5.0.1",
"bull": "^4.4.0",
"class-transformer": "^0.5.1",
@@ -46,10 +48,12 @@
"passport": "^0.5.2",
"passport-jwt": "^4.0.0",
"pg": "^8.7.1",
"redis": "^3.1.2",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
"sharp": "0.28",
"socket.io-redis": "^6.1.1",
"systeminformation": "^5.11.0",
"typeorm": "^0.2.41"
},

View File

@@ -1,5 +1,6 @@
import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn, PrimaryGeneratedColumn, Unique } from 'typeorm';
import { ExifEntity } from './exif.entity';
import { SmartInfoEntity } from './smart-info.entity';
@Entity('assets')
@Unique(['deviceAssetId', 'userId', 'deviceId'])
@@ -42,6 +43,9 @@ export class AssetEntity {
@OneToOne(() => ExifEntity, (exifEntity) => exifEntity.asset)
exifInfo: ExifEntity;
@OneToOne(() => SmartInfoEntity, (smartInfoEntity) => smartInfoEntity.asset)
smartInfo: SmartInfoEntity;
}
export enum AssetType {

View File

@@ -0,0 +1,19 @@
import { Column, Entity, Index, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
import { AssetEntity } from './asset.entity';
@Entity('smart_info')
export class SmartInfoEntity {
@PrimaryGeneratedColumn()
id: string;
@Index({ unique: true })
@Column({ type: 'uuid' })
assetId: string;
@Column({ type: 'text', array: true, nullable: true })
tags: string[];
@OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true })
@JoinColumn({ name: 'assetId', referencedColumnName: 'id' })
asset: SmartInfoEntity;
}

View File

@@ -1,12 +1,11 @@
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import dotenv from 'dotenv';
// import dotenv from 'dotenv';
const result = dotenv.config();
if (result.error) {
console.log(result.error);
}
// const result = dotenv.config();
// if (result.error) {
// console.log(result.error);
// }
export const databaseConfig: TypeOrmModuleOptions = {
type: 'postgres',
host: 'immich_postgres',
@@ -15,13 +14,10 @@ export const databaseConfig: TypeOrmModuleOptions = {
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE_NAME,
entities: [__dirname + '/../**/*.entity.{js,ts}'],
synchronize: true,
// logging: true,
// logger: 'advanced-console',
// ssl: process.env.NODE_ENV == 'production',
// extra: {
// ssl: {
// rejectUnauthorized: false,
// },
// },
synchronize: false,
migrations: [__dirname + '/../migration/*.js'],
cli: {
migrationsDir: __dirname + '/../migration',
},
migrationsRun: true,
};

View File

@@ -1,15 +1,23 @@
import { IoAdapter } from '@nestjs/platform-socket.io';
import { RedisClient, createClient } from 'redis';
import { RedisClient } from 'redis';
import { ServerOptions } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createAdapter } from 'socket.io-redis';
// const pubClient = createClient({ url: 'redis://immich_redis:6379' });
// const subClient = pubClient.duplicate();
const pubClient = new RedisClient({
port: 6379,
host: 'immich_redis',
});
const pubClient = createClient({ url: 'redis://immich_redis:6379' });
const subClient = pubClient.duplicate();
const redisAdapter = createAdapter({ pubClient, subClient });
export class RedisIoAdapter extends IoAdapter {
createIOServer(port: number, options?: ServerOptions): any {
const server = super.createIOServer(port, options);
server.adapter(createAdapter(pubClient, subClient));
server.adapter(redisAdapter);
return server;
}
}

View File

@@ -0,0 +1,22 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateUserTable1645130759468 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
create table if not exists users
(
id uuid default uuid_generate_v4() not null
constraint "PK_a3ffb1c0c8416b9fc6f907b7433"
primary key,
email varchar not null,
password varchar not null,
salt varchar not null,
"createdAt" timestamp default now() not null
);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`drop table users`);
}
}

View File

@@ -0,0 +1,26 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateDeviceInfoTable1645130777674 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
create table if not exists device_info
(
id serial
constraint "PK_b1c15a80b0a4e5f4eebadbdd92c"
primary key,
"userId" varchar not null,
"deviceId" varchar not null,
"deviceType" varchar not null,
"notificationToken" varchar,
"createdAt" timestamp default now() not null,
"isAutoBackup" boolean default false not null,
constraint "UQ_ebad78f36b10d15fbea8560e107"
unique ("userId", "deviceId")
);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`drop table device_info`);
}
}

View File

@@ -0,0 +1,31 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateAssetsTable1645130805273 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
create table if not exists assets
(
id uuid default uuid_generate_v4() not null
constraint "PK_da96729a8b113377cfb6a62439c"
primary key,
"deviceAssetId" varchar not null,
"userId" varchar not null,
"deviceId" varchar not null,
type varchar not null,
"originalPath" varchar not null,
"resizePath" varchar,
"createdAt" varchar not null,
"modifiedAt" varchar not null,
"isFavorite" boolean default false not null,
"mimeType" varchar,
duration varchar,
constraint "UQ_b599ab0bd9574958acb0b30a90e"
unique ("deviceAssetId", "userId", "deviceId")
);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`drop table assets`);
}
}

View File

@@ -0,0 +1,42 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateExifTable1645130817965 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
create table if not exists exif
(
id serial
constraint "PK_28663352d85078ad0046dafafaa"
primary key,
"assetId" uuid not null
constraint "REL_c0117fdbc50b917ef9067740c4"
unique
constraint "FK_c0117fdbc50b917ef9067740c44"
references assets
on delete cascade,
make varchar,
model varchar,
"imageName" varchar,
"exifImageWidth" integer,
"exifImageHeight" integer,
"fileSizeInByte" integer,
orientation varchar,
"dateTimeOriginal" timestamp with time zone,
"modifyDate" timestamp with time zone,
"lensModel" varchar,
"fNumber" double precision,
"focalLength" double precision,
iso integer,
"exposureTime" double precision,
latitude double precision,
longitude double precision
);
create unique index if not exists "IDX_c0117fdbc50b917ef9067740c4" on exif ("assetId");
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`drop table exif`);
}
}

View File

@@ -0,0 +1,30 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateSmartInfoTable1645130870184 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
create table if not exists smart_info
(
id serial
constraint "PK_0beace66440e9713f5c40470e46"
primary key,
"assetId" uuid not null
constraint "UQ_5e3753aadd956110bf3ec0244ac"
unique
constraint "FK_5e3753aadd956110bf3ec0244ac"
references assets
on delete cascade,
tags text[]
);
create unique index if not exists "IDX_5e3753aadd956110bf3ec0244a"
on smart_info ("assetId");
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
drop table smart_info;
`);
}
}

View File

@@ -3,6 +3,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
import { ExifEntity } from '../../api-v1/asset/entities/exif.entity';
import { SmartInfoEntity } from '../../api-v1/asset/entities/smart-info.entity';
import { BackgroundTaskProcessor } from './background-task.processor';
import { BackgroundTaskService } from './background-task.service';
@@ -16,7 +17,7 @@ import { BackgroundTaskService } from './background-task.service';
removeOnFail: false,
},
}),
TypeOrmModule.forFeature([AssetEntity, ExifEntity]),
TypeOrmModule.forFeature([AssetEntity, ExifEntity, SmartInfoEntity]),
],
providers: [BackgroundTaskService, BackgroundTaskProcessor],
exports: [BackgroundTaskService],

View File

@@ -9,6 +9,8 @@ import { readFile } from 'fs/promises';
import fs from 'fs';
import { Logger } from '@nestjs/common';
import { ExifEntity } from '../../api-v1/asset/entities/exif.entity';
import axios from 'axios';
import { SmartInfoEntity } from '../../api-v1/asset/entities/smart-info.entity';
@Processor('background-task')
export class BackgroundTaskProcessor {
@@ -16,6 +18,9 @@ export class BackgroundTaskProcessor {
@InjectRepository(AssetEntity)
private assetRepository: Repository<AssetEntity>,
@InjectRepository(SmartInfoEntity)
private smartInfoRepository: Repository<SmartInfoEntity>,
@InjectRepository(ExifEntity)
private exifRepository: Repository<ExifEntity>,
@@ -76,4 +81,18 @@ 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 smartInfo = new SmartInfoEntity();
smartInfo.assetId = asset.id;
smartInfo.tags = [...res.data];
this.smartInfoRepository.save(smartInfo);
}
}
}

View File

@@ -32,4 +32,15 @@ export class BackgroundTaskService {
{ jobId: randomUUID() },
);
}
async tagImage(thumbnailPath: string, asset: AssetEntity) {
await this.backgroundTaskQueue.add(
'tag-image',
{
thumbnailPath,
asset,
},
{ jobId: randomUUID() },
);
}
}

View File

@@ -1,20 +1,17 @@
import { BullModule } from '@nestjs/bull';
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { join } from 'path';
import { AssetModule } from '../../api-v1/asset/asset.module';
import { AssetService } from '../../api-v1/asset/asset.service';
import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
import { CommunicationGateway } from '../../api-v1/communication/communication.gateway';
import { CommunicationModule } from '../../api-v1/communication/communication.module';
import { UserEntity } from '../../api-v1/user/entities/user.entity';
import { ImmichJwtModule } from '../immich-jwt/immich-jwt.module';
import { BackgroundTaskModule } from '../background-task/background-task.module';
import { BackgroundTaskService } from '../background-task/background-task.service';
import { ImageOptimizeProcessor } from './image-optimize.processor';
import { AssetOptimizeService } from './image-optimize.service';
@Module({
imports: [
CommunicationModule,
BackgroundTaskModule,
BullModule.registerQueue({
name: 'optimize',
defaultJobOptions: {
@@ -23,10 +20,17 @@ import { AssetOptimizeService } from './image-optimize.service';
removeOnFail: false,
},
}),
BullModule.registerQueue({
name: 'background-task',
defaultJobOptions: {
attempts: 3,
removeOnComplete: true,
removeOnFail: false,
},
}),
TypeOrmModule.forFeature([AssetEntity]),
],
providers: [AssetOptimizeService, ImageOptimizeProcessor],
providers: [AssetOptimizeService, ImageOptimizeProcessor, BackgroundTaskService],
exports: [AssetOptimizeService],
})
export class ImageOptimizeModule {}

View File

@@ -11,6 +11,7 @@ import { APP_UPLOAD_LOCATION } from '../../constants/upload_location.constant';
import { WebSocketServer } from '@nestjs/websockets';
import { Socket, Server as SocketIoServer } from 'socket.io';
import { CommunicationGateway } from '../../api-v1/communication/communication.gateway';
import { BackgroundTaskService } from '../background-task/background-task.service';
@Processor('optimize')
export class ImageOptimizeProcessor {
@@ -18,6 +19,8 @@ export class ImageOptimizeProcessor {
private wsCommunicateionGateway: CommunicationGateway,
@InjectRepository(AssetEntity)
private assetRepository: Repository<AssetEntity>,
private backgroundTaskService: BackgroundTaskService,
) {}
@Process('resize-image')
@@ -58,11 +61,15 @@ export class ImageOptimizeProcessor {
}
const res = await this.assetRepository.update(savedAsset, { resizePath: desitnation });
if (res.affected) {
this.wsCommunicateionGateway.server
.to(savedAsset.userId)
.emit('on_upload_success', JSON.stringify(savedAsset));
}
// Tag Image
this.backgroundTaskService.tagImage(desitnation, savedAsset);
});
} else {
sharp(data)
@@ -79,6 +86,9 @@ export class ImageOptimizeProcessor {
.to(savedAsset.userId)
.emit('on_upload_success', JSON.stringify(savedAsset));
}
// Tag Image
this.backgroundTaskService.tagImage(resizePath, savedAsset);
});
}
});
@@ -107,12 +117,18 @@ export class ImageOptimizeProcessor {
filename: `${filename}.png`,
})
.on('end', async (a) => {
const thumbnailPath = `${resizeDir}/${filename}.png`;
const res = await this.assetRepository.update(savedAsset, { resizePath: `${resizeDir}/${filename}.png` });
if (res.affected) {
this.wsCommunicateionGateway.server
.to(savedAsset.userId)
.emit('on_upload_success', JSON.stringify(savedAsset));
}
// Tag Image
this.backgroundTaskService.tagImage(thumbnailPath, savedAsset);
});
return 'ok';