mirror of
https://github.com/immich-app/immich.git
synced 2024-12-28 22:51:59 +00:00
feat(machine-learning)!: move machine learning to Python based image (#1774)
BREAKING CHANGES * Users have to update the docker-compose file, machine-learning portion. * Temporary dropping machine-learning support for Arm64 and Armv7
This commit is contained in:
parent
8c315dfeb1
commit
57136e48fb
27 changed files with 92 additions and 16849 deletions
8
.github/workflows/docker.yml
vendored
8
.github/workflows/docker.yml
vendored
|
@ -16,6 +16,7 @@ jobs:
|
||||||
# Prevent a failure in one image from stopping the other builds
|
# Prevent a failure in one image from stopping the other builds
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
platforms: ["linux/arm/v7,linux/amd64,linux/arm64"]
|
||||||
include:
|
include:
|
||||||
- context: "server"
|
- context: "server"
|
||||||
image: "immich-server"
|
image: "immich-server"
|
||||||
|
@ -23,9 +24,10 @@ jobs:
|
||||||
image: "immich-web"
|
image: "immich-web"
|
||||||
- context: "machine-learning"
|
- context: "machine-learning"
|
||||||
image: "immich-machine-learning"
|
image: "immich-machine-learning"
|
||||||
|
platforms: "linux/amd64"
|
||||||
- context: "nginx"
|
- context: "nginx"
|
||||||
image: "immich-proxy"
|
image: "immich-proxy"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
@ -49,7 +51,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
# Skip when PR from a fork
|
# Skip when PR from a fork
|
||||||
|
@ -92,7 +94,7 @@ jobs:
|
||||||
uses: docker/build-push-action@v4.0.0
|
uses: docker/build-push-action@v4.0.0
|
||||||
with:
|
with:
|
||||||
context: ${{ matrix.context }}
|
context: ${{ matrix.context }}
|
||||||
platforms: linux/arm/v7,linux/amd64,linux/arm64
|
platforms: ${{ matrix.platform }}
|
||||||
# Skip pushing when PR from a fork
|
# Skip pushing when PR from a fork
|
||||||
push: ${{ !github.event.pull_request.head.repo.fork }}
|
push: ${{ !github.event.pull_request.head.repo.fork }}
|
||||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{matrix.image}}
|
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{matrix.image}}
|
||||||
|
|
|
@ -30,18 +30,20 @@ services:
|
||||||
build:
|
build:
|
||||||
context: ../machine-learning
|
context: ../machine-learning
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
target: builder
|
command: python main.py
|
||||||
command: npm run start:dev
|
ports:
|
||||||
|
- 3003:3003
|
||||||
volumes:
|
volumes:
|
||||||
- ../machine-learning:/usr/src/app
|
- ../machine-learning/src:/usr/src/app
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
- /usr/src/app/node_modules
|
- model-cache:/cache
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=development
|
- NODE_ENV=development
|
||||||
depends_on:
|
depends_on:
|
||||||
- database
|
- database
|
||||||
|
restart: always
|
||||||
|
|
||||||
immich-microservices:
|
immich-microservices:
|
||||||
container_name: immich_microservices
|
container_name: immich_microservices
|
||||||
|
@ -126,3 +128,4 @@ services:
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
model-cache:
|
||||||
|
|
|
@ -1,95 +0,0 @@
|
||||||
version: "3.8"
|
|
||||||
|
|
||||||
services:
|
|
||||||
immich-server:
|
|
||||||
container_name: immich_server
|
|
||||||
image: altran1502/immich-server:staging
|
|
||||||
entrypoint: ["/bin/sh", "./start-server.sh"]
|
|
||||||
volumes:
|
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=production
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
- database
|
|
||||||
restart: always
|
|
||||||
|
|
||||||
immich-microservices:
|
|
||||||
container_name: immich_microservices
|
|
||||||
image: altran1502/immich-server:staging
|
|
||||||
entrypoint: ["/bin/sh", "./start-microservices.sh"]
|
|
||||||
volumes:
|
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=production
|
|
||||||
depends_on:
|
|
||||||
- redis
|
|
||||||
- database
|
|
||||||
restart: always
|
|
||||||
|
|
||||||
immich-machine-learning:
|
|
||||||
container_name: immich_machine_learning
|
|
||||||
image: altran1502/immich-machine-learning:staging
|
|
||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
|
||||||
volumes:
|
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=production
|
|
||||||
depends_on:
|
|
||||||
- database
|
|
||||||
restart: always
|
|
||||||
|
|
||||||
immich-web:
|
|
||||||
container_name: immich_web
|
|
||||||
image: altran1502/immich-web:staging
|
|
||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
# Rename these values for svelte public interface
|
|
||||||
- PUBLIC_IMMICH_SERVER_URL=${IMMICH_SERVER_URL}
|
|
||||||
- PUBLIC_IMMICH_API_URL_EXTERNAL=${IMMICH_API_URL_EXTERNAL}
|
|
||||||
restart: always
|
|
||||||
|
|
||||||
redis:
|
|
||||||
container_name: immich_redis
|
|
||||||
image: redis:6.2
|
|
||||||
restart: always
|
|
||||||
|
|
||||||
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
|
|
||||||
restart: always
|
|
||||||
|
|
||||||
immich-proxy:
|
|
||||||
container_name: immich_proxy
|
|
||||||
image: altran1502/immich-proxy:staging
|
|
||||||
environment:
|
|
||||||
# Make sure these values get passed through from the env file
|
|
||||||
- IMMICH_SERVER_URL
|
|
||||||
- IMMICH_WEB_URL
|
|
||||||
ports:
|
|
||||||
- 2283:8080
|
|
||||||
logging:
|
|
||||||
driver: none
|
|
||||||
depends_on:
|
|
||||||
- immich-server
|
|
||||||
restart: always
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
pgdata:
|
|
|
@ -4,7 +4,7 @@ services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
image: altran1502/immich-server:release
|
image: altran1502/immich-server:release
|
||||||
entrypoint: ["/bin/sh", "./start-server.sh"]
|
entrypoint: [ "/bin/sh", "./start-server.sh" ]
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
env_file:
|
env_file:
|
||||||
|
@ -19,7 +19,7 @@ services:
|
||||||
immich-microservices:
|
immich-microservices:
|
||||||
container_name: immich_microservices
|
container_name: immich_microservices
|
||||||
image: altran1502/immich-server:release
|
image: altran1502/immich-server:release
|
||||||
entrypoint: ["/bin/sh", "./start-microservices.sh"]
|
entrypoint: [ "/bin/sh", "./start-microservices.sh" ]
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
env_file:
|
env_file:
|
||||||
|
@ -34,9 +34,10 @@ services:
|
||||||
immich-machine-learning:
|
immich-machine-learning:
|
||||||
container_name: immich_machine_learning
|
container_name: immich_machine_learning
|
||||||
image: altran1502/immich-machine-learning:release
|
image: altran1502/immich-machine-learning:release
|
||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
command: [ "python", "src/main.py" ]
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
|
- model-cache:/cache
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
|
@ -48,7 +49,7 @@ services:
|
||||||
immich-web:
|
immich-web:
|
||||||
container_name: immich_web
|
container_name: immich_web
|
||||||
image: altran1502/immich-web:release
|
image: altran1502/immich-web:release
|
||||||
entrypoint: ["/bin/sh", "./entrypoint.sh"]
|
entrypoint: [ "/bin/sh", "./entrypoint.sh" ]
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
restart: always
|
restart: always
|
||||||
|
@ -89,3 +90,4 @@ services:
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
model-cache:
|
||||||
|
|
|
@ -1,4 +1 @@
|
||||||
node_modules/
|
venv/
|
||||||
upload/
|
|
||||||
dist/
|
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
parserOptions: {
|
|
||||||
project: 'tsconfig.json',
|
|
||||||
sourceType: 'module',
|
|
||||||
},
|
|
||||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
|
||||||
extends: [
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'plugin:prettier/recommended',
|
|
||||||
],
|
|
||||||
root: true,
|
|
||||||
env: {
|
|
||||||
node: true,
|
|
||||||
jest: true,
|
|
||||||
},
|
|
||||||
ignorePatterns: ['.eslintrc.js'],
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/interface-name-prefix': 'off',
|
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
|
||||||
'@typescript-eslint/no-explicit-any': 'off',
|
|
||||||
},
|
|
||||||
};
|
|
41
machine-learning/.gitignore
vendored
41
machine-learning/.gitignore
vendored
|
@ -1,37 +1,4 @@
|
||||||
# compiled output
|
upload/
|
||||||
/dist
|
venv/
|
||||||
/node_modules
|
__pycache__/
|
||||||
|
model-cache/
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Tests
|
|
||||||
/coverage
|
|
||||||
/.nyc_output
|
|
||||||
|
|
||||||
# IDEs and editors
|
|
||||||
/.idea
|
|
||||||
.project
|
|
||||||
.classpath
|
|
||||||
.c9/
|
|
||||||
*.launch
|
|
||||||
.settings/
|
|
||||||
*.sublime-workspace
|
|
||||||
|
|
||||||
# IDE - VSCode
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/settings.json
|
|
||||||
!.vscode/tasks.json
|
|
||||||
!.vscode/launch.json
|
|
||||||
!.vscode/extensions.json
|
|
||||||
|
|
||||||
upload/
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "all"
|
|
||||||
}
|
|
|
@ -1,34 +1,11 @@
|
||||||
FROM node:16-bullseye-slim as builder
|
FROM python:3.10
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ENV TRANSFORMERS_CACHE=/cache
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
RUN apt-get update
|
RUN pip install --user --no-cache-dir torch==1.13.1+cpu -f https://download.pytorch.org/whl/torch_stable.html
|
||||||
RUN apt-get install gcc g++ make cmake python3 python3-pip -y
|
RUN pip install --user transformers tqdm numpy scikit-learn scipy nltk sentencepiece flask Pillow
|
||||||
|
RUN pip install --user --no-deps sentence-transformers
|
||||||
COPY package.json package-lock.json ./
|
|
||||||
|
|
||||||
RUN npm ci
|
|
||||||
RUN npm rebuild @tensorflow/tfjs-node --build-from-source
|
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
FROM builder as prod
|
|
||||||
|
|
||||||
RUN npm run build
|
|
||||||
RUN npm prune --omit=dev
|
|
||||||
|
|
||||||
FROM node:16-bullseye-slim
|
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
COPY --from=prod /usr/src/app/node_modules ./node_modules
|
|
||||||
COPY --from=prod /usr/src/app/dist ./dist
|
|
||||||
|
|
||||||
COPY package.json package-lock.json ./
|
|
||||||
COPY entrypoint.sh ./
|
|
||||||
|
|
||||||
# CMD [ "node", "dist/main" ]
|
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022 Hau Tran
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
|
@ -1,4 +1,5 @@
|
||||||
|
|
||||||
# Microservices for Immich
|
# Immich Machine Learning
|
||||||
|
|
||||||
## Image Classifier
|
- Object Detection
|
||||||
|
- Image Classification
|
|
@ -1,4 +0,0 @@
|
||||||
#! /bin/sh
|
|
||||||
# npm run typeorm migration:run
|
|
||||||
# npm run start:prod
|
|
||||||
exec node dist/main.js
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"collection": "@nestjs/schematics",
|
|
||||||
"sourceRoot": "src"
|
|
||||||
}
|
|
16346
machine-learning/package-lock.json
generated
16346
machine-learning/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,71 +0,0 @@
|
||||||
{
|
|
||||||
"name": "nest_microservices",
|
|
||||||
"version": "0.0.1",
|
|
||||||
"description": "",
|
|
||||||
"author": "",
|
|
||||||
"private": true,
|
|
||||||
"license": "UNLICENSED",
|
|
||||||
"scripts": {
|
|
||||||
"prebuild": "rimraf dist",
|
|
||||||
"build": "nest build",
|
|
||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
||||||
"start": "nest start",
|
|
||||||
"start:dev": "nest start --watch",
|
|
||||||
"start:debug": "nest start --debug --watch",
|
|
||||||
"start:prod": "node dist/main",
|
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
|
||||||
"test": "jest",
|
|
||||||
"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"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@nestjs/common": "^8.0.0",
|
|
||||||
"@nestjs/core": "^8.0.0",
|
|
||||||
"@tensorflow-models/coco-ssd": "^2.2.2",
|
|
||||||
"@tensorflow-models/mobilenet": "^2.1.0",
|
|
||||||
"@tensorflow/tfjs-node": "^3.19.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@nestjs/cli": "^8.2.4",
|
|
||||||
"@nestjs/schematics": "^8.0.0",
|
|
||||||
"@nestjs/testing": "^8.0.0",
|
|
||||||
"@types/express": "^4.17.13",
|
|
||||||
"@types/jest": "27.4.1",
|
|
||||||
"@types/node": "^16.0.0",
|
|
||||||
"@types/supertest": "^2.0.11",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
|
||||||
"@typescript-eslint/parser": "^5.0.0",
|
|
||||||
"eslint": "^8.0.1",
|
|
||||||
"eslint-config-prettier": "^8.3.0",
|
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
|
||||||
"jest": "^27.2.5",
|
|
||||||
"prettier": "^2.3.2",
|
|
||||||
"rimraf": "^3.0.2",
|
|
||||||
"source-map-support": "^0.5.20",
|
|
||||||
"supertest": "^6.1.3",
|
|
||||||
"ts-jest": "^27.0.3",
|
|
||||||
"ts-loader": "^9.2.3",
|
|
||||||
"ts-node": "^10.0.0",
|
|
||||||
"tsconfig-paths": "^3.10.1",
|
|
||||||
"typescript": "^4.3.5"
|
|
||||||
},
|
|
||||||
"jest": {
|
|
||||||
"moduleFileExtensions": [
|
|
||||||
"js",
|
|
||||||
"json",
|
|
||||||
"ts"
|
|
||||||
],
|
|
||||||
"rootDir": "src",
|
|
||||||
"testRegex": ".*\\.spec\\.ts$",
|
|
||||||
"transform": {
|
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
|
||||||
},
|
|
||||||
"collectCoverageFrom": [
|
|
||||||
"**/*.(t|j)s"
|
|
||||||
],
|
|
||||||
"coverageDirectory": "../coverage",
|
|
||||||
"testEnvironment": "node"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { ImageClassifierModule } from './image-classifier/image-classifier.module';
|
|
||||||
import { ObjectDetectionModule } from './object-detection/object-detection.module';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
imports: [ImageClassifierModule, ObjectDetectionModule],
|
|
||||||
controllers: [],
|
|
||||||
providers: [],
|
|
||||||
})
|
|
||||||
export class AppModule {}
|
|
|
@ -1,14 +0,0 @@
|
||||||
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('/tag-image')
|
|
||||||
async tagImage(@Body('thumbnailPath') thumbnailPath: string) {
|
|
||||||
return await this.imageClassifierService.tagImage(thumbnailPath);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
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 {}
|
|
|
@ -1,49 +0,0 @@
|
||||||
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()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tf.dispose(decodedImage);
|
|
||||||
return tags;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Error reading file ', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
61
machine-learning/src/main.py
Normal file
61
machine-learning/src/main.py
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import os
|
||||||
|
from flask import Flask, request
|
||||||
|
from transformers import pipeline
|
||||||
|
|
||||||
|
|
||||||
|
server = Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
classifier = pipeline(
|
||||||
|
task="image-classification",
|
||||||
|
model="microsoft/resnet-50"
|
||||||
|
)
|
||||||
|
|
||||||
|
detector = pipeline(
|
||||||
|
task="object-detection",
|
||||||
|
model="hustvl/yolos-tiny"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Environment resolver
|
||||||
|
is_dev = os.getenv('NODE_ENV') == 'development'
|
||||||
|
server_port = os.getenv('MACHINE_LEARNING_PORT') or 3003
|
||||||
|
|
||||||
|
|
||||||
|
@server.route("/ping")
|
||||||
|
def ping():
|
||||||
|
return "pong"
|
||||||
|
|
||||||
|
|
||||||
|
@server.route("/object-detection/detect-object", methods=['POST'])
|
||||||
|
def object_detection():
|
||||||
|
assetPath = request.json['thumbnailPath']
|
||||||
|
return run_engine(detector, assetPath), 201
|
||||||
|
|
||||||
|
|
||||||
|
@server.route("/image-classifier/tag-image", methods=['POST'])
|
||||||
|
def image_classification():
|
||||||
|
assetPath = request.json['thumbnailPath']
|
||||||
|
return run_engine(classifier, assetPath), 201
|
||||||
|
|
||||||
|
|
||||||
|
def run_engine(engine, path):
|
||||||
|
result = []
|
||||||
|
predictions = engine(path)
|
||||||
|
|
||||||
|
for index, pred in enumerate(predictions):
|
||||||
|
tags = pred['label'].split(', ')
|
||||||
|
if (index == 0):
|
||||||
|
result = tags
|
||||||
|
else:
|
||||||
|
if (pred['score'] > 0.5):
|
||||||
|
result = [*result, *tags]
|
||||||
|
|
||||||
|
if (len(result) > 1):
|
||||||
|
result = list(set(result))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
server.run(debug=is_dev, host='0.0.0.0', port=server_port)
|
|
@ -1,27 +0,0 @@
|
||||||
import { NestFactory } from '@nestjs/core';
|
|
||||||
import { AppModule } from './app.module';
|
|
||||||
import { Logger } from '@nestjs/common';
|
|
||||||
|
|
||||||
async function bootstrap() {
|
|
||||||
const app = await NestFactory.create(AppModule);
|
|
||||||
|
|
||||||
const port = Number(process.env.MACHINE_LEARNING_PORT) || 3003;
|
|
||||||
|
|
||||||
await app.listen(port, () => {
|
|
||||||
if (process.env.NODE_ENV == 'development') {
|
|
||||||
Logger.log(
|
|
||||||
'Running Immich Machine Learning in DEVELOPMENT environment',
|
|
||||||
'IMMICH MICROSERVICES',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV == 'production') {
|
|
||||||
Logger.log(
|
|
||||||
'Running Immich Machine Learning in PRODUCTION environment',
|
|
||||||
'IMMICH MICROSERVICES',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bootstrap();
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { Body, Controller, Post } from '@nestjs/common';
|
|
||||||
import { ObjectDetectionService } from './object-detection.service';
|
|
||||||
import { Logger } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Controller('object-detection')
|
|
||||||
export class ObjectDetectionController {
|
|
||||||
constructor(
|
|
||||||
private readonly objectDetectionService: ObjectDetectionService,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
@Post('/detect-object')
|
|
||||||
async detectObject(@Body('thumbnailPath') thumbnailPath: string) {
|
|
||||||
return await this.objectDetectionService.detectObject(thumbnailPath);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
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 {}
|
|
|
@ -1,39 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tf.dispose(decodedImage);
|
|
||||||
return [...tags];
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.log('Error reading file ', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"module": "commonjs",
|
|
||||||
"declaration": true,
|
|
||||||
"removeComments": true,
|
|
||||||
"emitDecoratorMetadata": true,
|
|
||||||
"experimentalDecorators": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"target": "es2017",
|
|
||||||
"sourceMap": true,
|
|
||||||
"outDir": "./dist",
|
|
||||||
"baseUrl": "./",
|
|
||||||
"incremental": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"strictNullChecks": false,
|
|
||||||
"noImplicitAny": false,
|
|
||||||
"strictBindCallApply": false,
|
|
||||||
"forceConsistentCasingInFileNames": false,
|
|
||||||
"noFallthroughCasesInSwitch": false
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -33,7 +33,6 @@ export class MachineLearningProcessor {
|
||||||
const smartInfo = new SmartInfoEntity();
|
const smartInfo = new SmartInfoEntity();
|
||||||
smartInfo.assetId = asset.id;
|
smartInfo.assetId = asset.id;
|
||||||
smartInfo.tags = [...res.data];
|
smartInfo.tags = [...res.data];
|
||||||
|
|
||||||
await this.smartInfoRepository.upsert(smartInfo, {
|
await this.smartInfoRepository.upsert(smartInfo, {
|
||||||
conflictPaths: ['assetId'],
|
conflictPaths: ['assetId'],
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue