forked from Cutlery/immich
		
	Added machine learning microservice and object detection (#76)
This commit is contained in:
		
							parent
							
								
									fe693db84f
								
							
						
					
					
						commit
						dd9c5244fd
					
				
							
								
								
									
										4
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								Makefile
									
									
									
									
									
								
							| @ -1,8 +1,8 @@ | |||||||
| dev: | dev: | ||||||
| 	docker-compose -f ./docker/docker-compose.yml up | 	docker-compose -f ./docker/docker-compose.yml up --remove-orphans | ||||||
| 
 | 
 | ||||||
| dev-update: | dev-update: | ||||||
| 	docker-compose -f ./docker/docker-compose.yml up --build -V  | 	docker-compose -f ./docker/docker-compose.yml up --build -V  --remove-orphans | ||||||
| 
 | 
 | ||||||
| dev-scale: | dev-scale: | ||||||
| 	docker-compose -f ./docker/docker-compose.yml up --build -V  --scale immich_server=3 --remove-orphans  | 	docker-compose -f ./docker/docker-compose.yml up --build -V  --scale immich_server=3 --remove-orphans  | ||||||
|  | |||||||
| @ -22,6 +22,34 @@ services: | |||||||
|     networks: |     networks: | ||||||
|       - immich_network |       - immich_network | ||||||
| 
 | 
 | ||||||
|  |   immich_microservices: | ||||||
|  |     image: immich-microservices-dev:1.3.2 | ||||||
|  |     build: | ||||||
|  |       context: ../microservices | ||||||
|  |       target: development | ||||||
|  |       dockerfile: ../microservices/Dockerfile | ||||||
|  |     command: npm run start:dev | ||||||
|  |     deploy: | ||||||
|  |       resources: | ||||||
|  |         reservations: | ||||||
|  |           devices: | ||||||
|  |             - driver: nvidia | ||||||
|  |               count: 1 | ||||||
|  |               capabilities: [ gpu ] | ||||||
|  |     expose: | ||||||
|  |       - "3001" | ||||||
|  |     volumes: | ||||||
|  |       - ../microservices:/usr/src/app | ||||||
|  |       - ${UPLOAD_LOCATION}:/usr/src/app/upload | ||||||
|  |       - /usr/src/app/node_modules | ||||||
|  |     env_file: | ||||||
|  |       - .env | ||||||
|  |     depends_on: | ||||||
|  |       - database | ||||||
|  |       - immich_server | ||||||
|  |     networks: | ||||||
|  |       - immich_network | ||||||
|  | 
 | ||||||
|   redis: |   redis: | ||||||
|     container_name: immich_redis |     container_name: immich_redis | ||||||
|     image: redis:6.2 |     image: redis:6.2 | ||||||
| @ -60,35 +88,6 @@ services: | |||||||
|     depends_on: |     depends_on: | ||||||
|       - immich_server |       - immich_server | ||||||
| 
 | 
 | ||||||
|   immich_tf_fastapi: |  | ||||||
|     container_name: immich_tf_fastapi |  | ||||||
|     image: tensor_flow_fastapi:1.0.0 |  | ||||||
|     restart: always |  | ||||||
|     command: uvicorn app.main:app --proxy-headers --host 0.0.0.0 --port 8000 --reload |  | ||||||
|     build: |  | ||||||
|       context: ../machine_learning |  | ||||||
|       target: gpu |  | ||||||
|       dockerfile: ../machine_learning/Dockerfile |  | ||||||
|     deploy: |  | ||||||
|       resources: |  | ||||||
|         reservations: |  | ||||||
|           devices: |  | ||||||
|             - driver: nvidia |  | ||||||
|               count: 1 |  | ||||||
|               capabilities: [gpu] |  | ||||||
|     volumes: |  | ||||||
|       - ../machine_learning/app:/code/app |  | ||||||
|       - ${UPLOAD_LOCATION}:/code/app/upload |  | ||||||
|     ports: |  | ||||||
|       - 2285:8000 |  | ||||||
|     expose: |  | ||||||
|       - "8000" |  | ||||||
|     depends_on: |  | ||||||
|       - database |  | ||||||
|     networks: |  | ||||||
|       - immich_network |  | ||||||
| 
 |  | ||||||
|        |  | ||||||
| networks: | networks: | ||||||
|   immich_network: |   immich_network: | ||||||
| volumes: | volumes: | ||||||
|  | |||||||
| @ -23,6 +23,27 @@ services: | |||||||
|     networks: |     networks: | ||||||
|       - immich_network |       - immich_network | ||||||
| 
 | 
 | ||||||
|  |   immich_microservices: | ||||||
|  |     image: immich-microservices-dev:1.3.2 | ||||||
|  |     build: | ||||||
|  |       context: ../microservices | ||||||
|  |       target: development | ||||||
|  |       dockerfile: ../microservices/Dockerfile | ||||||
|  |     command: npm run start:dev | ||||||
|  |     expose: | ||||||
|  |       - "3001" | ||||||
|  |     volumes: | ||||||
|  |       - ../microservices:/usr/src/app | ||||||
|  |       - ${UPLOAD_LOCATION}:/usr/src/app/upload | ||||||
|  |       - /usr/src/app/node_modules | ||||||
|  |     env_file: | ||||||
|  |       - .env | ||||||
|  |     depends_on: | ||||||
|  |       - database | ||||||
|  |     networks: | ||||||
|  |       - immich_network | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|   redis: |   redis: | ||||||
|     container_name: immich_redis |     container_name: immich_redis | ||||||
|     image: redis:6.2 |     image: redis:6.2 | ||||||
| @ -61,26 +82,26 @@ services: | |||||||
|     depends_on: |     depends_on: | ||||||
|       - immich_server |       - immich_server | ||||||
| 
 | 
 | ||||||
|   immich_tf_fastapi: |   # immich_tf_fastapi: | ||||||
|     container_name: immich_tf_fastapi |   #   container_name: immich_tf_fastapi | ||||||
|     image: tensor_flow_fastapi:1.0.0 |   #   image: tensor_flow_fastapi:1.0.0 | ||||||
|     restart: always |   #   restart: always | ||||||
|     command: uvicorn app.main:app --proxy-headers --host 0.0.0.0 --port 8000 --reload |   #   command: uvicorn app.main:app --proxy-headers --host 0.0.0.0 --port 8000 --reload | ||||||
|     build: |   #   build: | ||||||
|       context: ../machine_learning |   #     context: ../machine_learning | ||||||
|       target: cpu |   #     target: cpu | ||||||
|       dockerfile: ../machine_learning/Dockerfile |   #     dockerfile: ../machine_learning/Dockerfile | ||||||
|     volumes: |   #   volumes: | ||||||
|       - ../machine_learning/app:/code/app |   #     - ../machine_learning/app:/code/app | ||||||
|       - ${UPLOAD_LOCATION}:/code/app/upload |   #     - ${UPLOAD_LOCATION}:/code/app/upload | ||||||
|     ports: |   #   ports: | ||||||
|       - 2285:8000 |   #     - 2285:8000 | ||||||
|     expose: |   #   expose: | ||||||
|       - "8000" |   #     - "8000" | ||||||
|     depends_on: |   #   depends_on: | ||||||
|       - database |   #     - database | ||||||
|     networks: |   #   networks: | ||||||
|       - immich_network |   #     - immich_network | ||||||
| 
 | 
 | ||||||
| networks: | networks: | ||||||
|   immich_network: |   immich_network: | ||||||
|  | |||||||
| @ -13,7 +13,7 @@ server { | |||||||
|   client_max_body_size 50000M; |   client_max_body_size 50000M; | ||||||
| 
 | 
 | ||||||
|   listen 80; |   listen 80; | ||||||
| 
 |   access_log off; | ||||||
|   location / { |   location / { | ||||||
|     proxy_buffering off; |     proxy_buffering off; | ||||||
|     proxy_buffer_size 16k; |     proxy_buffer_size 16k; | ||||||
|  | |||||||
							
								
								
									
										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 | # Microservices for Immich | ||||||
| [circleci-url]: https://circleci.com/gh/nestjs/nest |  | ||||||
| 
 | 
 | ||||||
|   <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> | ## Image Classifier | ||||||
|     <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). |  | ||||||
							
								
								
									
										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": { |   "dependencies": { | ||||||
|     "@nestjs/common": "^8.0.0", |     "@nestjs/common": "^8.0.0", | ||||||
|     "@nestjs/core": "^8.0.0", |     "@nestjs/core": "^8.0.0", | ||||||
|  |     "@nestjs/mapped-types": "^1.0.1", | ||||||
|     "@nestjs/platform-express": "^8.0.0", |     "@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", |     "reflect-metadata": "^0.1.13", | ||||||
|     "rimraf": "^3.0.2", |     "rimraf": "^3.0.2", | ||||||
|     "rxjs": "^7.2.0" |     "rxjs": "^7.2.0", | ||||||
|  |     "typeorm": "^0.2.45" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@nestjs/cli": "^8.0.0", |     "@nestjs/cli": "^8.2.4", | ||||||
|     "@nestjs/schematics": "^8.0.0", |     "@nestjs/schematics": "^8.0.0", | ||||||
|     "@nestjs/testing": "^8.0.0", |     "@nestjs/testing": "^8.0.0", | ||||||
|     "@types/express": "^4.17.13", |     "@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 { Module } from '@nestjs/common'; | ||||||
| import { AppController } from './app.controller'; | import { ImageClassifierModule } from './image-classifier/image-classifier.module'; | ||||||
| import { AppService } from './app.service'; | import { databaseConfig } from './config/database.config'; | ||||||
|  | import { TypeOrmModule } from '@nestjs/typeorm'; | ||||||
|  | import { ObjectDetectionModule } from './object-detection/object-detection.module'; | ||||||
| 
 | 
 | ||||||
| @Module({ | @Module({ | ||||||
|   imports: [], |   imports: [ | ||||||
|   controllers: [AppController], |     TypeOrmModule.forRoot(databaseConfig), | ||||||
|   providers: [AppService], |     ImageClassifierModule, | ||||||
|  |     ObjectDetectionModule, | ||||||
|  |   ], | ||||||
|  |   controllers: [], | ||||||
|  |   providers: [], | ||||||
| }) | }) | ||||||
| export class AppModule {} | 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 { NestFactory } from '@nestjs/core'; | ||||||
| import { AppModule } from './app.module'; | import { AppModule } from './app.module'; | ||||||
| import { AppService } from './app.service'; |  | ||||||
| 
 | 
 | ||||||
| async function bootstrap() { | async function bootstrap() { | ||||||
|   const app = await NestFactory.createApplicationContext(AppModule); |   const app = await NestFactory.create(AppModule); | ||||||
| 
 | 
 | ||||||
|   const appService = app.get(AppService); |   await app.listen(3001); | ||||||
| 
 |  | ||||||
|   appService.getHello(); |  | ||||||
| 
 |  | ||||||
|   await app.close(); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| bootstrap(); | 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 { Test, TestingModule } from '@nestjs/testing'; | ||||||
| import { INestApplication } from '@nestjs/common'; | import { INestApplication } from '@nestjs/common'; | ||||||
| import * as request from 'supertest'; | import * as request from 'supertest'; | ||||||
| import { AppModule } from './../src/app.module'; | import { AppModule } from '../src/app.module'; | ||||||
| 
 | 
 | ||||||
|  | // End to End test
 | ||||||
| describe('AppController (e2e)', () => { | describe('AppController (e2e)', () => { | ||||||
|   let app: INestApplication; |   let app: INestApplication; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -79,4 +79,4 @@ SPEC CHECKSUMS: | |||||||
| 
 | 
 | ||||||
| PODFILE CHECKSUM: 05c3056158482c567a3e0cdab1351ceeee238a07 | PODFILE CHECKSUM: 05c3056158482c567a3e0cdab1351ceeee238a07 | ||||||
| 
 | 
 | ||||||
| COCOAPODS: 1.10.1 | COCOAPODS: 1.11.3 | ||||||
|  | |||||||
| @ -341,7 +341,7 @@ | |||||||
| 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | ||||||
| 				GCC_WARN_UNUSED_FUNCTION = YES; | 				GCC_WARN_UNUSED_FUNCTION = YES; | ||||||
| 				GCC_WARN_UNUSED_VARIABLE = YES; | 				GCC_WARN_UNUSED_VARIABLE = YES; | ||||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 9.0; | 				IPHONEOS_DEPLOYMENT_TARGET = 11.0; | ||||||
| 				MTL_ENABLE_DEBUG_INFO = NO; | 				MTL_ENABLE_DEBUG_INFO = NO; | ||||||
| 				NEW_SETTING = ""; | 				NEW_SETTING = ""; | ||||||
| 				SDKROOT = iphoneos; | 				SDKROOT = iphoneos; | ||||||
| @ -425,7 +425,7 @@ | |||||||
| 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | ||||||
| 				GCC_WARN_UNUSED_FUNCTION = YES; | 				GCC_WARN_UNUSED_FUNCTION = YES; | ||||||
| 				GCC_WARN_UNUSED_VARIABLE = YES; | 				GCC_WARN_UNUSED_VARIABLE = YES; | ||||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 9.0; | 				IPHONEOS_DEPLOYMENT_TARGET = 11.0; | ||||||
| 				MTL_ENABLE_DEBUG_INFO = YES; | 				MTL_ENABLE_DEBUG_INFO = YES; | ||||||
| 				NEW_SETTING = ""; | 				NEW_SETTING = ""; | ||||||
| 				ONLY_ACTIVE_ARCH = YES; | 				ONLY_ACTIVE_ARCH = YES; | ||||||
| @ -475,7 +475,7 @@ | |||||||
| 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; | ||||||
| 				GCC_WARN_UNUSED_FUNCTION = YES; | 				GCC_WARN_UNUSED_FUNCTION = YES; | ||||||
| 				GCC_WARN_UNUSED_VARIABLE = YES; | 				GCC_WARN_UNUSED_VARIABLE = YES; | ||||||
| 				IPHONEOS_DEPLOYMENT_TARGET = 9.0; | 				IPHONEOS_DEPLOYMENT_TARGET = 11.0; | ||||||
| 				MTL_ENABLE_DEBUG_INFO = NO; | 				MTL_ENABLE_DEBUG_INFO = NO; | ||||||
| 				NEW_SETTING = ""; | 				NEW_SETTING = ""; | ||||||
| 				SDKROOT = iphoneos; | 				SDKROOT = iphoneos; | ||||||
|  | |||||||
| @ -15,7 +15,7 @@ class LoginForm extends HookConsumerWidget { | |||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     final usernameController = useTextEditingController(text: 'testuser@email.com'); |     final usernameController = useTextEditingController(text: 'testuser@email.com'); | ||||||
|     final passwordController = useTextEditingController(text: 'password'); |     final passwordController = useTextEditingController(text: 'password'); | ||||||
|     final serverEndpointController = useTextEditingController(text: 'http://192.168.1.103:2283'); |     final serverEndpointController = useTextEditingController(text: 'http://192.168.1.216:2283'); | ||||||
| 
 | 
 | ||||||
|     return Center( |     return Center( | ||||||
|       child: ConstrainedBox( |       child: ConstrainedBox( | ||||||
|  | |||||||
							
								
								
									
										80
									
								
								mobile/lib/modules/search/models/curated_object.model.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								mobile/lib/modules/search/models/curated_object.model.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,80 @@ | |||||||
|  | import 'dart:convert'; | ||||||
|  | 
 | ||||||
|  | class CuratedObject { | ||||||
|  |   final String id; | ||||||
|  |   final String object; | ||||||
|  |   final String resizePath; | ||||||
|  |   final String deviceAssetId; | ||||||
|  |   final String deviceId; | ||||||
|  |   CuratedObject({ | ||||||
|  |     required this.id, | ||||||
|  |     required this.object, | ||||||
|  |     required this.resizePath, | ||||||
|  |     required this.deviceAssetId, | ||||||
|  |     required this.deviceId, | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   CuratedObject copyWith({ | ||||||
|  |     String? id, | ||||||
|  |     String? object, | ||||||
|  |     String? resizePath, | ||||||
|  |     String? deviceAssetId, | ||||||
|  |     String? deviceId, | ||||||
|  |   }) { | ||||||
|  |     return CuratedObject( | ||||||
|  |       id: id ?? this.id, | ||||||
|  |       object: object ?? this.object, | ||||||
|  |       resizePath: resizePath ?? this.resizePath, | ||||||
|  |       deviceAssetId: deviceAssetId ?? this.deviceAssetId, | ||||||
|  |       deviceId: deviceId ?? this.deviceId, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Map<String, dynamic> toMap() { | ||||||
|  |     final result = <String, dynamic>{}; | ||||||
|  | 
 | ||||||
|  |     result.addAll({'id': id}); | ||||||
|  |     result.addAll({'object': object}); | ||||||
|  |     result.addAll({'resizePath': resizePath}); | ||||||
|  |     result.addAll({'deviceAssetId': deviceAssetId}); | ||||||
|  |     result.addAll({'deviceId': deviceId}); | ||||||
|  | 
 | ||||||
|  |     return result; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   factory CuratedObject.fromMap(Map<String, dynamic> map) { | ||||||
|  |     return CuratedObject( | ||||||
|  |       id: map['id'] ?? '', | ||||||
|  |       object: map['object'] ?? '', | ||||||
|  |       resizePath: map['resizePath'] ?? '', | ||||||
|  |       deviceAssetId: map['deviceAssetId'] ?? '', | ||||||
|  |       deviceId: map['deviceId'] ?? '', | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   String toJson() => json.encode(toMap()); | ||||||
|  | 
 | ||||||
|  |   factory CuratedObject.fromJson(String source) => CuratedObject.fromMap(json.decode(source)); | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   String toString() { | ||||||
|  |     return 'CuratedObject(id: $id, object: $object, resizePath: $resizePath, deviceAssetId: $deviceAssetId, deviceId: $deviceId)'; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   bool operator ==(Object other) { | ||||||
|  |     if (identical(this, other)) return true; | ||||||
|  | 
 | ||||||
|  |     return other is CuratedObject && | ||||||
|  |         other.id == id && | ||||||
|  |         other.object == object && | ||||||
|  |         other.resizePath == resizePath && | ||||||
|  |         other.deviceAssetId == deviceAssetId && | ||||||
|  |         other.deviceId == deviceId; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   int get hashCode { | ||||||
|  |     return id.hashCode ^ object.hashCode ^ resizePath.hashCode ^ deviceAssetId.hashCode ^ deviceId.hashCode; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -1,5 +1,6 @@ | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/modules/search/models/curated_location.model.dart'; | import 'package:immich_mobile/modules/search/models/curated_location.model.dart'; | ||||||
|  | import 'package:immich_mobile/modules/search/models/curated_object.model.dart'; | ||||||
| import 'package:immich_mobile/modules/search/models/search_page_state.model.dart'; | import 'package:immich_mobile/modules/search/models/search_page_state.model.dart'; | ||||||
| 
 | 
 | ||||||
| import 'package:immich_mobile/modules/search/services/search.service.dart'; | import 'package:immich_mobile/modules/search/services/search.service.dart'; | ||||||
| @ -64,3 +65,14 @@ final getCuratedLocationProvider = FutureProvider.autoDispose<List<CuratedLocati | |||||||
|     return []; |     return []; | ||||||
|   } |   } | ||||||
| }); | }); | ||||||
|  | 
 | ||||||
|  | final getCuratedObjectProvider = FutureProvider.autoDispose<List<CuratedObject>>((ref) async { | ||||||
|  |   final SearchService _searchService = SearchService(); | ||||||
|  | 
 | ||||||
|  |   var curatedObject = await _searchService.getCuratedObjects(); | ||||||
|  |   if (curatedObject != null) { | ||||||
|  |     return curatedObject; | ||||||
|  |   } else { | ||||||
|  |     return []; | ||||||
|  |   } | ||||||
|  | }); | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ import 'dart:convert'; | |||||||
| 
 | 
 | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:immich_mobile/modules/search/models/curated_location.model.dart'; | import 'package:immich_mobile/modules/search/models/curated_location.model.dart'; | ||||||
|  | import 'package:immich_mobile/modules/search/models/curated_object.model.dart'; | ||||||
| import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | import 'package:immich_mobile/shared/models/immich_asset.model.dart'; | ||||||
| import 'package:immich_mobile/shared/services/network.service.dart'; | import 'package:immich_mobile/shared/services/network.service.dart'; | ||||||
| 
 | 
 | ||||||
| @ -52,4 +53,19 @@ class SearchService { | |||||||
|       throw Error(); |       throw Error(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   Future<List<CuratedObject>?> getCuratedObjects() async { | ||||||
|  |     try { | ||||||
|  |       var res = await _networkService.getRequest(url: "asset/allObjects"); | ||||||
|  | 
 | ||||||
|  |       List<dynamic> decodedData = jsonDecode(res.toString()); | ||||||
|  | 
 | ||||||
|  |       List<CuratedObject> result = List.from(decodedData.map((a) => CuratedObject.fromMap(a))); | ||||||
|  | 
 | ||||||
|  |       return result; | ||||||
|  |     } catch (e) { | ||||||
|  |       debugPrint("[ERROR] [CuratedObject] ${e.toString()}"); | ||||||
|  |       throw Error(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -6,10 +6,12 @@ import 'package:hive_flutter/hive_flutter.dart'; | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/constants/hive_box.dart'; | import 'package:immich_mobile/constants/hive_box.dart'; | ||||||
| import 'package:immich_mobile/modules/search/models/curated_location.model.dart'; | import 'package:immich_mobile/modules/search/models/curated_location.model.dart'; | ||||||
|  | import 'package:immich_mobile/modules/search/models/curated_object.model.dart'; | ||||||
| import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; | import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; | ||||||
| import 'package:immich_mobile/modules/search/ui/search_bar.dart'; | import 'package:immich_mobile/modules/search/ui/search_bar.dart'; | ||||||
| import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart'; | import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart'; | ||||||
| import 'package:immich_mobile/routing/router.dart'; | import 'package:immich_mobile/routing/router.dart'; | ||||||
|  | import 'package:immich_mobile/utils/capitalize_first_letter.dart'; | ||||||
| 
 | 
 | ||||||
| // ignore: must_be_immutable | // ignore: must_be_immutable | ||||||
| class SearchPage extends HookConsumerWidget { | class SearchPage extends HookConsumerWidget { | ||||||
| @ -22,6 +24,7 @@ class SearchPage extends HookConsumerWidget { | |||||||
|     var box = Hive.box(userInfoBox); |     var box = Hive.box(userInfoBox); | ||||||
|     final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled; |     final isSearchEnabled = ref.watch(searchPageStateProvider).isSearchEnabled; | ||||||
|     AsyncValue<List<CuratedLocation>> curatedLocation = ref.watch(getCuratedLocationProvider); |     AsyncValue<List<CuratedLocation>> curatedLocation = ref.watch(getCuratedLocationProvider); | ||||||
|  |     AsyncValue<List<CuratedObject>> curatedObjects = ref.watch(getCuratedObjectProvider); | ||||||
| 
 | 
 | ||||||
|     useEffect(() { |     useEffect(() { | ||||||
|       searchFocusNode = FocusNode(); |       searchFocusNode = FocusNode(); | ||||||
| @ -82,6 +85,54 @@ class SearchPage extends HookConsumerWidget { | |||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     _buildThings() { | ||||||
|  |       return curatedObjects.when( | ||||||
|  |         loading: () => const CircularProgressIndicator(), | ||||||
|  |         error: (err, stack) => Text('Error: $err'), | ||||||
|  |         data: (objects) { | ||||||
|  |           return objects.isNotEmpty | ||||||
|  |               ? SizedBox( | ||||||
|  |                   height: MediaQuery.of(context).size.width / 3, | ||||||
|  |                   child: ListView.builder( | ||||||
|  |                     padding: const EdgeInsets.only(left: 16), | ||||||
|  |                     scrollDirection: Axis.horizontal, | ||||||
|  |                     itemCount: curatedObjects.value?.length, | ||||||
|  |                     itemBuilder: ((context, index) { | ||||||
|  |                       CuratedObject curatedObjectInfo = objects[index]; | ||||||
|  |                       var thumbnailRequestUrl = | ||||||
|  |                           '${box.get(serverEndpointKey)}/asset/file?aid=${curatedObjectInfo.deviceAssetId}&did=${curatedObjectInfo.deviceId}&isThumb=true'; | ||||||
|  | 
 | ||||||
|  |                       return ThumbnailWithInfo( | ||||||
|  |                         imageUrl: thumbnailRequestUrl, | ||||||
|  |                         textInfo: curatedObjectInfo.object, | ||||||
|  |                         onTap: () { | ||||||
|  |                           AutoRouter.of(context) | ||||||
|  |                               .push(SearchResultRoute(searchTerm: curatedObjectInfo.object.capitalizeFirstLetter())); | ||||||
|  |                         }, | ||||||
|  |                       ); | ||||||
|  |                     }), | ||||||
|  |                   ), | ||||||
|  |                 ) | ||||||
|  |               : SizedBox( | ||||||
|  |                   height: MediaQuery.of(context).size.width / 3, | ||||||
|  |                   child: ListView.builder( | ||||||
|  |                     padding: const EdgeInsets.only(left: 16), | ||||||
|  |                     scrollDirection: Axis.horizontal, | ||||||
|  |                     itemCount: 1, | ||||||
|  |                     itemBuilder: ((context, index) { | ||||||
|  |                       return ThumbnailWithInfo( | ||||||
|  |                         imageUrl: | ||||||
|  |                             'https://images.unsplash.com/photo-1612178537253-bccd437b730e?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8NXx8Ymxhbmt8ZW58MHx8MHx8&auto=format&fit=crop&w=700&q=60', | ||||||
|  |                         textInfo: 'No Object Info Available', | ||||||
|  |                         onTap: () {}, | ||||||
|  |                       ); | ||||||
|  |                     }), | ||||||
|  |                   ), | ||||||
|  |                 ); | ||||||
|  |         }, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     return Scaffold( |     return Scaffold( | ||||||
|       appBar: SearchBar( |       appBar: SearchBar( | ||||||
|         searchFocusNode: searchFocusNode, |         searchFocusNode: searchFocusNode, | ||||||
| @ -104,6 +155,14 @@ class SearchPage extends HookConsumerWidget { | |||||||
|                   ), |                   ), | ||||||
|                 ), |                 ), | ||||||
|                 _buildPlaces(), |                 _buildPlaces(), | ||||||
|  |                 const Padding( | ||||||
|  |                   padding: EdgeInsets.all(16.0), | ||||||
|  |                   child: Text( | ||||||
|  |                     "Things", | ||||||
|  |                     style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), | ||||||
|  |                   ), | ||||||
|  |                 ), | ||||||
|  |                 _buildThings() | ||||||
|               ], |               ], | ||||||
|             ), |             ), | ||||||
|             isSearchEnabled ? SearchSuggestionList(onSubmitted: _onSearchSubmitted) : Container(), |             isSearchEnabled ? SearchSuggestionList(onSubmitted: _onSearchSubmitted) : Container(), | ||||||
| @ -160,7 +219,7 @@ class ThumbnailWithInfo extends StatelessWidget { | |||||||
|                 child: SizedBox( |                 child: SizedBox( | ||||||
|                   width: MediaQuery.of(context).size.width / 3, |                   width: MediaQuery.of(context).size.width / 3, | ||||||
|                   child: Text( |                   child: Text( | ||||||
|                     textInfo, |                     textInfo.capitalizeFirstLetter(), | ||||||
|                     style: const TextStyle( |                     style: const TextStyle( | ||||||
|                       color: Colors.white, |                       color: Colors.white, | ||||||
|                       fontWeight: FontWeight.bold, |                       fontWeight: FontWeight.bold, | ||||||
|  | |||||||
| @ -26,6 +26,7 @@ class TabNavigationObserver extends AutoRouterObserver { | |||||||
|     if (route.name == 'SearchRoute') { |     if (route.name == 'SearchRoute') { | ||||||
|       // Refresh Location State |       // Refresh Location State | ||||||
|       ref.refresh(getCuratedLocationProvider); |       ref.refresh(getCuratedLocationProvider); | ||||||
|  |       ref.refresh(getCuratedObjectProvider); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     ref.watch(serverInfoProvider.notifier).getServerVersion(); |     ref.watch(serverInfoProvider.notifier).getServerVersion(); | ||||||
|  | |||||||
							
								
								
									
										5
									
								
								mobile/lib/utils/capitalize_first_letter.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								mobile/lib/utils/capitalize_first_letter.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | extension StringExtension on String { | ||||||
|  |   String capitalizeFirstLetter() { | ||||||
|  |     return "${this[0].toUpperCase()}${substring(1).toLowerCase()}"; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										90
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										90
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -1,12 +1,12 @@ | |||||||
| { | { | ||||||
|   "name": "immich", |   "name": "immich", | ||||||
|   "version": "0.0.1", |   "version": "1.3.2", | ||||||
|   "lockfileVersion": 2, |   "lockfileVersion": 2, | ||||||
|   "requires": true, |   "requires": true, | ||||||
|   "packages": { |   "packages": { | ||||||
|     "": { |     "": { | ||||||
|       "name": "immich", |       "name": "immich", | ||||||
|       "version": "0.0.1", |       "version": "1.3.2", | ||||||
|       "license": "UNLICENSED", |       "license": "UNLICENSED", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@mapbox/mapbox-sdk": "^0.13.3", |         "@mapbox/mapbox-sdk": "^0.13.3", | ||||||
| @ -1547,6 +1547,66 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/@nestjs/microservices": { | ||||||
|  |       "version": "8.4.3", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-8.4.3.tgz", | ||||||
|  |       "integrity": "sha512-/ZT5wo1s65J9Cqp2g5eNrYO34VH7/qUkDu4jJyZCT61I9UqpO49J3+1YIAHfmJJzHcrenjgt1sBtlFhwPR3Lgg==", | ||||||
|  |       "optional": true, | ||||||
|  |       "peer": true, | ||||||
|  |       "dependencies": { | ||||||
|  |         "iterare": "1.2.1", | ||||||
|  |         "json-socket": "0.3.0", | ||||||
|  |         "tslib": "2.3.1" | ||||||
|  |       }, | ||||||
|  |       "funding": { | ||||||
|  |         "type": "opencollective", | ||||||
|  |         "url": "https://opencollective.com/nest" | ||||||
|  |       }, | ||||||
|  |       "peerDependencies": { | ||||||
|  |         "@grpc/grpc-js": "*", | ||||||
|  |         "@nestjs/common": "^8.0.0", | ||||||
|  |         "@nestjs/core": "^8.0.0", | ||||||
|  |         "@nestjs/websockets": "^8.0.0", | ||||||
|  |         "amqp-connection-manager": "*", | ||||||
|  |         "amqplib": "*", | ||||||
|  |         "cache-manager": "*", | ||||||
|  |         "kafkajs": "*", | ||||||
|  |         "mqtt": "*", | ||||||
|  |         "nats": "*", | ||||||
|  |         "redis": "*", | ||||||
|  |         "reflect-metadata": "^0.1.12", | ||||||
|  |         "rxjs": "^7.1.0" | ||||||
|  |       }, | ||||||
|  |       "peerDependenciesMeta": { | ||||||
|  |         "@grpc/grpc-js": { | ||||||
|  |           "optional": true | ||||||
|  |         }, | ||||||
|  |         "@nestjs/websockets": { | ||||||
|  |           "optional": true | ||||||
|  |         }, | ||||||
|  |         "amqp-connection-manager": { | ||||||
|  |           "optional": true | ||||||
|  |         }, | ||||||
|  |         "amqplib": { | ||||||
|  |           "optional": true | ||||||
|  |         }, | ||||||
|  |         "cache-manager": { | ||||||
|  |           "optional": true | ||||||
|  |         }, | ||||||
|  |         "kafkajs": { | ||||||
|  |           "optional": true | ||||||
|  |         }, | ||||||
|  |         "mqtt": { | ||||||
|  |           "optional": true | ||||||
|  |         }, | ||||||
|  |         "nats": { | ||||||
|  |           "optional": true | ||||||
|  |         }, | ||||||
|  |         "redis": { | ||||||
|  |           "optional": true | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "node_modules/@nestjs/passport": { |     "node_modules/@nestjs/passport": { | ||||||
|       "version": "8.1.0", |       "version": "8.1.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-8.1.0.tgz", |       "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-8.1.0.tgz", | ||||||
| @ -7053,6 +7113,13 @@ | |||||||
|       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", |       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", | ||||||
|       "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" |       "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" | ||||||
|     }, |     }, | ||||||
|  |     "node_modules/json-socket": { | ||||||
|  |       "version": "0.3.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/json-socket/-/json-socket-0.3.0.tgz", | ||||||
|  |       "integrity": "sha512-jc8ZbUnYIWdxERFWQKVgwSLkGSe+kyzvmYxwNaRgx/c8NNyuHes4UHnPM3LUrAFXUx1BhNJ94n1h/KCRlbvV0g==", | ||||||
|  |       "optional": true, | ||||||
|  |       "peer": true | ||||||
|  |     }, | ||||||
|     "node_modules/json-stable-stringify-without-jsonify": { |     "node_modules/json-stable-stringify-without-jsonify": { | ||||||
|       "version": "1.0.1", |       "version": "1.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", |       "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", | ||||||
| @ -11943,6 +12010,18 @@ | |||||||
|       "integrity": "sha512-NFvofzSinp00j5rzUd4tf+xi9od6383iY0JP7o0Bnu1fuItAUkWBgc4EKuIQ3D+c2QI3i9pG1kDWAeY27EMGtg==", |       "integrity": "sha512-NFvofzSinp00j5rzUd4tf+xi9od6383iY0JP7o0Bnu1fuItAUkWBgc4EKuIQ3D+c2QI3i9pG1kDWAeY27EMGtg==", | ||||||
|       "requires": {} |       "requires": {} | ||||||
|     }, |     }, | ||||||
|  |     "@nestjs/microservices": { | ||||||
|  |       "version": "8.4.3", | ||||||
|  |       "resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-8.4.3.tgz", | ||||||
|  |       "integrity": "sha512-/ZT5wo1s65J9Cqp2g5eNrYO34VH7/qUkDu4jJyZCT61I9UqpO49J3+1YIAHfmJJzHcrenjgt1sBtlFhwPR3Lgg==", | ||||||
|  |       "optional": true, | ||||||
|  |       "peer": true, | ||||||
|  |       "requires": { | ||||||
|  |         "iterare": "1.2.1", | ||||||
|  |         "json-socket": "0.3.0", | ||||||
|  |         "tslib": "2.3.1" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "@nestjs/passport": { |     "@nestjs/passport": { | ||||||
|       "version": "8.1.0", |       "version": "8.1.0", | ||||||
|       "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-8.1.0.tgz", |       "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-8.1.0.tgz", | ||||||
| @ -16243,6 +16322,13 @@ | |||||||
|       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", |       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", | ||||||
|       "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" |       "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" | ||||||
|     }, |     }, | ||||||
|  |     "json-socket": { | ||||||
|  |       "version": "0.3.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/json-socket/-/json-socket-0.3.0.tgz", | ||||||
|  |       "integrity": "sha512-jc8ZbUnYIWdxERFWQKVgwSLkGSe+kyzvmYxwNaRgx/c8NNyuHes4UHnPM3LUrAFXUx1BhNJ94n1h/KCRlbvV0g==", | ||||||
|  |       "optional": true, | ||||||
|  |       "peer": true | ||||||
|  |     }, | ||||||
|     "json-stable-stringify-without-jsonify": { |     "json-stable-stringify-without-jsonify": { | ||||||
|       "version": "1.0.1", |       "version": "1.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", |       "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", | ||||||
|  | |||||||
| @ -16,13 +16,12 @@ import { | |||||||
| } from '@nestjs/common'; | } from '@nestjs/common'; | ||||||
| import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard'; | import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard'; | ||||||
| import { AssetService } from './asset.service'; | 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 { multerOption } from '../../config/multer-option.config'; | ||||||
| import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator'; | import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator'; | ||||||
| import { CreateAssetDto } from './dto/create-asset.dto'; | import { CreateAssetDto } from './dto/create-asset.dto'; | ||||||
| import { ServeFileDto } from './dto/serve-file.dto'; | import { ServeFileDto } from './dto/serve-file.dto'; | ||||||
| import { AssetOptimizeService } from '../../modules/image-optimize/image-optimize.service'; | import { AssetEntity } from './entities/asset.entity'; | ||||||
| import { AssetEntity, AssetType } from './entities/asset.entity'; |  | ||||||
| import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto'; | import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto'; | ||||||
| import { Response as Res } from 'express'; | import { Response as Res } from 'express'; | ||||||
| import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto'; | import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto'; | ||||||
| @ -61,6 +60,7 @@ export class AssetController { | |||||||
|       if (uploadFiles.thumbnailData != null) { |       if (uploadFiles.thumbnailData != null) { | ||||||
|         await this.assetService.updateThumbnailInfo(savedAsset.id, uploadFiles.thumbnailData[0].path); |         await this.assetService.updateThumbnailInfo(savedAsset.id, uploadFiles.thumbnailData[0].path); | ||||||
|         await this.backgroundTaskService.tagImage(uploadFiles.thumbnailData[0].path, savedAsset); |         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); |       await this.backgroundTaskService.extractExif(savedAsset, file.originalname, file.size); | ||||||
| @ -81,6 +81,11 @@ export class AssetController { | |||||||
|     return this.assetService.serveFile(authUser, query, res, headers); |     return this.assetService.serveFile(authUser, query, res, headers); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   @Get('/allObjects') | ||||||
|  |   async getCuratedObject(@GetAuthUser() authUser: AuthUserDto) { | ||||||
|  |     return this.assetService.getCuratedObject(authUser); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   @Get('/allLocation') |   @Get('/allLocation') | ||||||
|   async getCuratedLocation(@GetAuthUser() authUser: AuthUserDto) { |   async getCuratedLocation(@GetAuthUser() authUser: AuthUserDto) { | ||||||
|     return this.assetService.getCuratedLocation(authUser); |     return this.assetService.getCuratedLocation(authUser); | ||||||
|  | |||||||
| @ -3,9 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm'; | |||||||
| import { MoreThan, Repository } from 'typeorm'; | import { MoreThan, Repository } from 'typeorm'; | ||||||
| import { AuthUserDto } from '../../decorators/auth-user.decorator'; | import { AuthUserDto } from '../../decorators/auth-user.decorator'; | ||||||
| import { CreateAssetDto } from './dto/create-asset.dto'; | import { CreateAssetDto } from './dto/create-asset.dto'; | ||||||
| import { UpdateAssetDto } from './dto/update-asset.dto'; |  | ||||||
| import { AssetEntity, AssetType } from './entities/asset.entity'; | import { AssetEntity, AssetType } from './entities/asset.entity'; | ||||||
| import _, { result } from 'lodash'; | import _ from 'lodash'; | ||||||
| import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto'; | import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto'; | ||||||
| import { GetAllAssetReponseDto } from './dto/get-all-asset-response.dto'; | import { GetAllAssetReponseDto } from './dto/get-all-asset-response.dto'; | ||||||
| import { createReadStream, stat } from 'fs'; | import { createReadStream, stat } from 'fs'; | ||||||
| @ -44,9 +43,7 @@ export class AssetService { | |||||||
|     asset.duration = assetInfo.duration; |     asset.duration = assetInfo.duration; | ||||||
| 
 | 
 | ||||||
|     try { |     try { | ||||||
|       const res = await this.assetRepository.save(asset); |       return await this.assetRepository.save(asset); | ||||||
| 
 |  | ||||||
|       return res; |  | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       Logger.error(`Error Create New Asset ${e}`, 'createUserAsset'); |       Logger.error(`Error Create New Asset ${e}`, 'createUserAsset'); | ||||||
|     } |     } | ||||||
| @ -68,13 +65,11 @@ export class AssetService { | |||||||
| 
 | 
 | ||||||
|   public async getAllAssetsNoPagination(authUser: AuthUserDto) { |   public async getAllAssetsNoPagination(authUser: AuthUserDto) { | ||||||
|     try { |     try { | ||||||
|       const assets = await this.assetRepository |       return await this.assetRepository | ||||||
|         .createQueryBuilder('a') |         .createQueryBuilder('a') | ||||||
|         .where('a."userId" = :userId', { userId: authUser.id }) |         .where('a."userId" = :userId', { userId: authUser.id }) | ||||||
|         .orderBy('a."createdAt"::date', 'DESC') |         .orderBy('a."createdAt"::date', 'DESC') | ||||||
|         .getMany(); |         .getMany(); | ||||||
| 
 |  | ||||||
|       return assets; |  | ||||||
|     } catch (e) { |     } catch (e) { | ||||||
|       Logger.error(e, 'getAllAssets'); |       Logger.error(e, 'getAllAssets'); | ||||||
|     } |     } | ||||||
| @ -226,10 +221,10 @@ export class AssetService { | |||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public async deleteAssetById(authUser: AuthUserDto, assetIds: DeleteAssetDto) { |   public async deleteAssetById(authUser: AuthUserDto, assetIds: DeleteAssetDto) { | ||||||
|     let result = []; |     const result = []; | ||||||
| 
 | 
 | ||||||
|     const target = assetIds.ids; |     const target = assetIds.ids; | ||||||
|     for (let assetId of target) { |     for (const assetId of target) { | ||||||
|       const res = await this.assetRepository.delete({ |       const res = await this.assetRepository.delete({ | ||||||
|         id: assetId, |         id: assetId, | ||||||
|         userId: authUser.id, |         userId: authUser.id, | ||||||
| @ -251,11 +246,11 @@ export class AssetService { | |||||||
|     return result; |     return result; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async getAssetSearchTerm(authUser: AuthUserDto): Promise<String[]> { |   async getAssetSearchTerm(authUser: AuthUserDto): Promise<string[]> { | ||||||
|     const possibleSearchTerm = new Set<String>(); |     const possibleSearchTerm = new Set<string>(); | ||||||
|     const rows = await this.assetRepository.query( |     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 |       from assets a | ||||||
|       left join exif e on a.id = e."assetId" |       left join exif e on a.id = e."assetId" | ||||||
|       left join smart_info si on a.id = si."assetId" |       left join smart_info si on a.id = si."assetId" | ||||||
| @ -268,6 +263,9 @@ export class AssetService { | |||||||
|       // tags
 |       // tags
 | ||||||
|       row['tags']?.map((tag) => possibleSearchTerm.add(tag?.toLowerCase())); |       row['tags']?.map((tag) => possibleSearchTerm.add(tag?.toLowerCase())); | ||||||
| 
 | 
 | ||||||
|  |       // objects
 | ||||||
|  |       row['objects']?.map((object) => possibleSearchTerm.add(object?.toLowerCase())); | ||||||
|  | 
 | ||||||
|       // asset's tyoe
 |       // asset's tyoe
 | ||||||
|       possibleSearchTerm.add(row['type']?.toLowerCase()); |       possibleSearchTerm.add(row['type']?.toLowerCase()); | ||||||
| 
 | 
 | ||||||
| @ -301,17 +299,16 @@ export class AssetService { | |||||||
|        AND  |        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) |          e.exif_text_searchable_column @@ PLAINTO_TSQUERY('english', $2) | ||||||
|         ); |         ); | ||||||
|     `;
 |     `;
 | ||||||
| 
 | 
 | ||||||
|     const rows = await this.assetRepository.query(query, [authUser.id, searchAssetDto.searchTerm]); |     return await this.assetRepository.query(query, [authUser.id, searchAssetDto.searchTerm]); | ||||||
| 
 |  | ||||||
|     return rows; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async getCuratedLocation(authUser: AuthUserDto) { |   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" |         select distinct on (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId" | ||||||
|         from assets a |         from assets a | ||||||
| @ -322,7 +319,18 @@ export class AssetService { | |||||||
|       `,
 |       `,
 | ||||||
|       [authUser.id], |       [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], | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -13,6 +13,9 @@ export class SmartInfoEntity { | |||||||
|   @Column({ type: 'text', array: true, nullable: true }) |   @Column({ type: 'text', array: true, nullable: true }) | ||||||
|   tags: string[]; |   tags: string[]; | ||||||
| 
 | 
 | ||||||
|  |   @Column({ type: 'text', array: true, nullable: true }) | ||||||
|  |   objects: string[]; | ||||||
|  | 
 | ||||||
|   @OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true }) |   @OneToOne(() => AssetEntity, { onDelete: 'CASCADE', nullable: true }) | ||||||
|   @JoinColumn({ name: 'assetId', referencedColumnName: 'id' }) |   @JoinColumn({ name: 'assetId', referencedColumnName: 'id' }) | ||||||
|   asset: SmartInfoEntity; |   asset: SmartInfoEntity; | ||||||
|  | |||||||
| @ -5,7 +5,6 @@ import { UserModule } from './api-v1/user/user.module'; | |||||||
| import { AssetModule } from './api-v1/asset/asset.module'; | import { AssetModule } from './api-v1/asset/asset.module'; | ||||||
| import { AuthModule } from './api-v1/auth/auth.module'; | import { AuthModule } from './api-v1/auth/auth.module'; | ||||||
| import { ImmichJwtModule } from './modules/immich-jwt/immich-jwt.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 { DeviceInfoModule } from './api-v1/device-info/device-info.module'; | ||||||
| import { AppLoggerMiddleware } from './middlewares/app-logger.middleware'; | import { AppLoggerMiddleware } from './middlewares/app-logger.middleware'; | ||||||
| import { ConfigModule, ConfigService } from '@nestjs/config'; | import { ConfigModule, ConfigService } from '@nestjs/config'; | ||||||
| @ -26,14 +25,12 @@ import { CommunicationModule } from './api-v1/communication/communication.module | |||||||
|     ImmichJwtModule, |     ImmichJwtModule, | ||||||
|     DeviceInfoModule, |     DeviceInfoModule, | ||||||
|     BullModule.forRootAsync({ |     BullModule.forRootAsync({ | ||||||
|       imports: [ConfigModule], |       useFactory: async () => ({ | ||||||
|       useFactory: async (configService: ConfigService) => ({ |  | ||||||
|         redis: { |         redis: { | ||||||
|           host: 'immich_redis', |           host: 'immich_redis', | ||||||
|           port: 6379, |           port: 6379, | ||||||
|         }, |         }, | ||||||
|       }), |       }), | ||||||
|       inject: [ConfigService], |  | ||||||
|     }), |     }), | ||||||
| 
 | 
 | ||||||
|     ImageOptimizeModule, |     ImageOptimizeModule, | ||||||
|  | |||||||
| @ -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; | ||||||
|  |     `);
 | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -6,7 +6,7 @@ import { AssetEntity } from '../../api-v1/asset/entities/asset.entity'; | |||||||
| import { ConfigService } from '@nestjs/config'; | import { ConfigService } from '@nestjs/config'; | ||||||
| import exifr from 'exifr'; | import exifr from 'exifr'; | ||||||
| import { readFile } from 'fs/promises'; | import { readFile } from 'fs/promises'; | ||||||
| import fs, { rmSync } from 'fs'; | import fs from 'fs'; | ||||||
| import { Logger } from '@nestjs/common'; | import { Logger } from '@nestjs/common'; | ||||||
| import { ExifEntity } from '../../api-v1/asset/entities/exif.entity'; | import { ExifEntity } from '../../api-v1/asset/entities/exif.entity'; | ||||||
| import axios from 'axios'; | import axios from 'axios'; | ||||||
| @ -114,14 +114,37 @@ export class BackgroundTaskProcessor { | |||||||
|   @Process('tag-image') |   @Process('tag-image') | ||||||
|   async tagImage(job) { |   async tagImage(job) { | ||||||
|     const { thumbnailPath, asset }: { thumbnailPath: string; asset: AssetEntity } = job.data; |     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(); |       const smartInfo = new SmartInfoEntity(); | ||||||
|       smartInfo.assetId = asset.id; |       smartInfo.assetId = asset.id; | ||||||
|       smartInfo.tags = [...res.data]; |       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'], | ||||||
|  |       }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -43,4 +43,15 @@ export class BackgroundTaskService { | |||||||
|       { jobId: randomUUID() }, |       { jobId: randomUUID() }, | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   async detectObject(thumbnailPath: string, asset: AssetEntity) { | ||||||
|  |     await this.backgroundTaskQueue.add( | ||||||
|  |       'detect-object', | ||||||
|  |       { | ||||||
|  |         thumbnailPath, | ||||||
|  |         asset, | ||||||
|  |       }, | ||||||
|  |       { jobId: randomUUID() }, | ||||||
|  |     ); | ||||||
|  |   } | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user