mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	Added machine learning microservice and object detection (#76)
This commit is contained in:
		
							
								
								
									
										4
									
								
								microservices/.dockerignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								microservices/.dockerignore
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| node_modules/ | ||||
| upload/ | ||||
| dist/ | ||||
|  | ||||
							
								
								
									
										43
									
								
								microservices/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								microservices/Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| ################################## | ||||
| # DEVELOPMENT | ||||
| ################################## | ||||
| FROM node:16-bullseye-slim AS development | ||||
|  | ||||
| ARG DEBIAN_FRONTEND=noninteractive | ||||
|  | ||||
| WORKDIR /usr/src/app | ||||
|  | ||||
| COPY package.json package-lock.json ./ | ||||
|  | ||||
| RUN apt-get update | ||||
| RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y | ||||
|  | ||||
| RUN npm install | ||||
|  | ||||
| COPY . . | ||||
|  | ||||
| RUN npm run build | ||||
|  | ||||
| ################################# | ||||
| # PRODUCTION | ||||
| ################################# | ||||
| FROM node:16-bullseye-slim AS production | ||||
|  | ||||
| ARG DEBIAN_FRONTEND=noninteractive | ||||
| ARG NODE_ENV=production | ||||
| ENV NODE_ENV=${NODE_ENV} | ||||
|  | ||||
| WORKDIR /usr/src/app | ||||
|  | ||||
| COPY package.json package-lock.json ./ | ||||
|  | ||||
| RUN apt-get update | ||||
| RUN apt-get install gcc g++ make cmake python3 python3-pip ffmpeg -y | ||||
|  | ||||
| RUN npm install --only=production | ||||
|  | ||||
| COPY . . | ||||
|  | ||||
| COPY --from=development /usr/src/app/dist ./dist | ||||
|  | ||||
| CMD ["node", "dist/main"] | ||||
| @@ -1,73 +1,4 @@ | ||||
| <p align="center"> | ||||
|   <a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo_text.svg" width="320" alt="Nest Logo" /></a> | ||||
| </p> | ||||
|  | ||||
| [circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 | ||||
| [circleci-url]: https://circleci.com/gh/nestjs/nest | ||||
| # Microservices for Immich | ||||
|  | ||||
|   <p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p> | ||||
|     <p align="center"> | ||||
| <a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a> | ||||
| <a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a> | ||||
| <a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a> | ||||
| <a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a> | ||||
| <a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a> | ||||
| <a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a> | ||||
| <a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a> | ||||
| <a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a> | ||||
|   <a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a> | ||||
|     <a href="https://opencollective.com/nest#sponsor"  target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a> | ||||
|   <a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a> | ||||
| </p> | ||||
|   <!--[](https://opencollective.com/nest#backer) | ||||
|   [](https://opencollective.com/nest#sponsor)--> | ||||
|  | ||||
| ## Description | ||||
|  | ||||
| [Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. | ||||
|  | ||||
| ## Installation | ||||
|  | ||||
| ```bash | ||||
| $ npm install | ||||
| ``` | ||||
|  | ||||
| ## Running the app | ||||
|  | ||||
| ```bash | ||||
| # development | ||||
| $ npm run start | ||||
|  | ||||
| # watch mode | ||||
| $ npm run start:dev | ||||
|  | ||||
| # production mode | ||||
| $ npm run start:prod | ||||
| ``` | ||||
|  | ||||
| ## Test | ||||
|  | ||||
| ```bash | ||||
| # unit tests | ||||
| $ npm run test | ||||
|  | ||||
| # e2e tests | ||||
| $ npm run test:e2e | ||||
|  | ||||
| # test coverage | ||||
| $ npm run test:cov | ||||
| ``` | ||||
|  | ||||
| ## Support | ||||
|  | ||||
| Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). | ||||
|  | ||||
| ## Stay in touch | ||||
|  | ||||
| - Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) | ||||
| - Website - [https://nestjs.com](https://nestjs.com/) | ||||
| - Twitter - [@nestframework](https://twitter.com/nestframework) | ||||
|  | ||||
| ## License | ||||
|  | ||||
| Nest is [MIT licensed](LICENSE). | ||||
| ## Image Classifier | ||||
							
								
								
									
										10979
									
								
								microservices/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10979
									
								
								microservices/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -23,13 +23,25 @@ | ||||
|   "dependencies": { | ||||
|     "@nestjs/common": "^8.0.0", | ||||
|     "@nestjs/core": "^8.0.0", | ||||
|     "@nestjs/mapped-types": "^1.0.1", | ||||
|     "@nestjs/platform-express": "^8.0.0", | ||||
|     "@nestjs/typeorm": "^8.0.3", | ||||
|     "@tensorflow-models/coco-ssd": "^2.2.2", | ||||
|     "@tensorflow-models/mobilenet": "^2.1.0", | ||||
|     "@tensorflow/tfjs": "^3.15.0", | ||||
|     "@tensorflow/tfjs-converter": "^3.15.0", | ||||
|     "@tensorflow/tfjs-core": "^3.15.0", | ||||
|     "@tensorflow/tfjs-node": "^3.15.0", | ||||
|     "@tensorflow/tfjs-node-gpu": "^3.15.0", | ||||
|     "@trpc/server": "^9.20.3", | ||||
|     "pg": "^8.7.3", | ||||
|     "reflect-metadata": "^0.1.13", | ||||
|     "rimraf": "^3.0.2", | ||||
|     "rxjs": "^7.2.0" | ||||
|     "rxjs": "^7.2.0", | ||||
|     "typeorm": "^0.2.45" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@nestjs/cli": "^8.0.0", | ||||
|     "@nestjs/cli": "^8.2.4", | ||||
|     "@nestjs/schematics": "^8.0.0", | ||||
|     "@nestjs/testing": "^8.0.0", | ||||
|     "@types/express": "^4.17.13", | ||||
|   | ||||
| @@ -1,12 +0,0 @@ | ||||
| import { Controller, Get } from '@nestjs/common'; | ||||
| import { AppService } from './app.service'; | ||||
|  | ||||
| @Controller() | ||||
| export class AppController { | ||||
|   constructor(private readonly appService: AppService) {} | ||||
|  | ||||
|   @Get() | ||||
|   getHello(): string { | ||||
|     return this.appService.getHello(); | ||||
|   } | ||||
| } | ||||
| @@ -1,10 +1,16 @@ | ||||
| import { Module } from '@nestjs/common'; | ||||
| import { AppController } from './app.controller'; | ||||
| import { AppService } from './app.service'; | ||||
| import { ImageClassifierModule } from './image-classifier/image-classifier.module'; | ||||
| import { databaseConfig } from './config/database.config'; | ||||
| import { TypeOrmModule } from '@nestjs/typeorm'; | ||||
| import { ObjectDetectionModule } from './object-detection/object-detection.module'; | ||||
|  | ||||
| @Module({ | ||||
|   imports: [], | ||||
|   controllers: [AppController], | ||||
|   providers: [AppService], | ||||
|   imports: [ | ||||
|     TypeOrmModule.forRoot(databaseConfig), | ||||
|     ImageClassifierModule, | ||||
|     ObjectDetectionModule, | ||||
|   ], | ||||
|   controllers: [], | ||||
|   providers: [], | ||||
| }) | ||||
| export class AppModule {} | ||||
|   | ||||
| @@ -1,9 +0,0 @@ | ||||
| import { Injectable } from '@nestjs/common'; | ||||
|  | ||||
| @Injectable() | ||||
| export class AppService { | ||||
|   getHello(): string { | ||||
|     console.log('Hello World 123'); | ||||
|     return 'Hello World!'; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										11
									
								
								microservices/src/config/database.config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								microservices/src/config/database.config.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import { TypeOrmModuleOptions } from '@nestjs/typeorm'; | ||||
|  | ||||
| export const databaseConfig: TypeOrmModuleOptions = { | ||||
|   type: 'postgres', | ||||
|   host: 'immich_postgres', | ||||
|   port: 5432, | ||||
|   username: process.env.DB_USERNAME, | ||||
|   password: process.env.DB_PASSWORD, | ||||
|   database: process.env.DB_DATABASE_NAME, | ||||
|   synchronize: false, | ||||
| }; | ||||
| @@ -0,0 +1,14 @@ | ||||
| import { Body, Controller, Post } from '@nestjs/common'; | ||||
| import { ImageClassifierService } from './image-classifier.service'; | ||||
|  | ||||
| @Controller('image-classifier') | ||||
| export class ImageClassifierController { | ||||
|   constructor( | ||||
|     private readonly imageClassifierService: ImageClassifierService, | ||||
|   ) {} | ||||
|  | ||||
|   @Post('/tagImage') | ||||
|   async tagImage(@Body('thumbnailPath') thumbnailPath: string) { | ||||
|     return await this.imageClassifierService.tagImage(thumbnailPath); | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,9 @@ | ||||
| import { Module } from '@nestjs/common'; | ||||
| import { ImageClassifierService } from './image-classifier.service'; | ||||
| import { ImageClassifierController } from './image-classifier.controller'; | ||||
|  | ||||
| @Module({ | ||||
|   controllers: [ImageClassifierController], | ||||
|   providers: [ImageClassifierService], | ||||
| }) | ||||
| export class ImageClassifierModule {} | ||||
| @@ -0,0 +1,48 @@ | ||||
| import { Injectable, Logger } from '@nestjs/common'; | ||||
| import * as mobilenet from '@tensorflow-models/mobilenet'; | ||||
| import * as cocoSsd from '@tensorflow-models/coco-ssd'; | ||||
| import * as tf from '@tensorflow/tfjs-node'; | ||||
| import * as fs from 'fs'; | ||||
|  | ||||
| @Injectable() | ||||
| export class ImageClassifierService { | ||||
|   private readonly MOBILENET_VERSION = 2; | ||||
|   private readonly MOBILENET_ALPHA = 1.0; | ||||
|  | ||||
|   private mobileNetModel: mobilenet.MobileNet; | ||||
|  | ||||
|   constructor() { | ||||
|     Logger.log( | ||||
|       `Running Node TensorFlow Version : ${tf.version['tfjs']}`, | ||||
|       'ImageClassifier', | ||||
|     ); | ||||
|     mobilenet | ||||
|       .load({ | ||||
|         version: this.MOBILENET_VERSION, | ||||
|         alpha: this.MOBILENET_ALPHA, | ||||
|       }) | ||||
|       .then((mobilenetModel) => (this.mobileNetModel = mobilenetModel)); | ||||
|   } | ||||
|  | ||||
|   async tagImage(thumbnailPath: string) { | ||||
|     try { | ||||
|       const isExist = fs.existsSync(thumbnailPath); | ||||
|       if (isExist) { | ||||
|         const tags = []; | ||||
|         const image = fs.readFileSync(thumbnailPath); | ||||
|         const decodedImage = tf.node.decodeImage(image, 3) as tf.Tensor3D; | ||||
|         const predictions = await this.mobileNetModel.classify(decodedImage); | ||||
|  | ||||
|         for (const prediction of predictions) { | ||||
|           if (prediction.probability >= 0.1) { | ||||
|             tags.push(...prediction.className.split(',').map((e) => e.trim())); | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         return tags; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       console.log('Error reading file ', e); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,15 +1,10 @@ | ||||
| import { NestFactory } from '@nestjs/core'; | ||||
| import { AppModule } from './app.module'; | ||||
| import { AppService } from './app.service'; | ||||
|  | ||||
| async function bootstrap() { | ||||
|   const app = await NestFactory.createApplicationContext(AppModule); | ||||
|   const app = await NestFactory.create(AppModule); | ||||
|  | ||||
|   const appService = app.get(AppService); | ||||
|  | ||||
|   appService.getHello(); | ||||
|  | ||||
|   await app.close(); | ||||
|   await app.listen(3001); | ||||
| } | ||||
|  | ||||
| bootstrap(); | ||||
|   | ||||
| @@ -0,0 +1,14 @@ | ||||
| import { Body, Controller, Post } from '@nestjs/common'; | ||||
| import { ObjectDetectionService } from './object-detection.service'; | ||||
|  | ||||
| @Controller('object-detection') | ||||
| export class ObjectDetectionController { | ||||
|   constructor( | ||||
|     private readonly objectDetectionService: ObjectDetectionService, | ||||
|   ) {} | ||||
|  | ||||
|   @Post('/detectObject') | ||||
|   async detectObject(@Body('thumbnailPath') thumbnailPath: string) { | ||||
|     return await this.objectDetectionService.detectObject(thumbnailPath); | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,9 @@ | ||||
| import { Module } from '@nestjs/common'; | ||||
| import { ObjectDetectionService } from './object-detection.service'; | ||||
| import { ObjectDetectionController } from './object-detection.controller'; | ||||
|  | ||||
| @Module({ | ||||
|   controllers: [ObjectDetectionController], | ||||
|   providers: [ObjectDetectionService], | ||||
| }) | ||||
| export class ObjectDetectionModule {} | ||||
| @@ -0,0 +1,38 @@ | ||||
| import { Injectable, Logger } from '@nestjs/common'; | ||||
| import * as cocoSsd from '@tensorflow-models/coco-ssd'; | ||||
| import * as tf from '@tensorflow/tfjs-node'; | ||||
| import * as fs from 'fs'; | ||||
|  | ||||
| @Injectable() | ||||
| export class ObjectDetectionService { | ||||
|   private cocoSsdModel: cocoSsd.ObjectDetection; | ||||
|  | ||||
|   constructor() { | ||||
|     Logger.log( | ||||
|       `Running Node TensorFlow Version : ${tf.version['tfjs']}`, | ||||
|       'ObjectDetection', | ||||
|     ); | ||||
|     cocoSsd.load().then((model) => (this.cocoSsdModel = model)); | ||||
|   } | ||||
|   async detectObject(thumbnailPath: string) { | ||||
|     try { | ||||
|       const isExist = fs.existsSync(thumbnailPath); | ||||
|       if (isExist) { | ||||
|         const tags = new Set(); | ||||
|         const image = fs.readFileSync(thumbnailPath); | ||||
|         const decodedImage = tf.node.decodeImage(image, 3) as tf.Tensor3D; | ||||
|         const predictions = await this.cocoSsdModel.detect(decodedImage); | ||||
|  | ||||
|         for (const result of predictions) { | ||||
|           if (result.score > 0.5) { | ||||
|             tags.add(result.class); | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         return [...tags]; | ||||
|       } | ||||
|     } catch (e) { | ||||
|       console.log('Error reading file ', e); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -1,8 +1,9 @@ | ||||
| import { Test, TestingModule } from '@nestjs/testing'; | ||||
| import { INestApplication } from '@nestjs/common'; | ||||
| import * as request from 'supertest'; | ||||
| import { AppModule } from './../src/app.module'; | ||||
| import { AppModule } from '../src/app.module'; | ||||
|  | ||||
| // End to End test | ||||
| describe('AppController (e2e)', () => { | ||||
|   let app: INestApplication; | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user