Bläddra i källkod

chore: pull main

shalong-tanwen 1 år sedan
förälder
incheckning
c08d9a7001
100 ändrade filer med 6325 tillägg och 1969 borttagningar
  1. 7 5
      .github/workflows/build-mobile.yml
  2. 1 1
      .github/workflows/cache-cleanup.yml
  3. 1 1
      .github/workflows/codeql-analysis.yml
  4. 2 2
      .github/workflows/docker-cleanup.yml
  5. 15 18
      .github/workflows/docker.yml
  6. 2 2
      .github/workflows/prepare-release.yml
  7. 2 2
      .github/workflows/static_analysis.yml
  8. 30 22
      .github/workflows/test.yml
  9. 1 0
      .gitignore
  10. 3 0
      .gitmodules
  11. 5 2
      Makefile
  12. 7 2
      README.md
  13. 4 0
      README_ca_ES.md
  14. 3 0
      README_es_ES.md
  15. 113 0
      README_fr_FR.md
  16. 113 0
      README_it_IT.md
  17. 112 0
      README_ja_JP.md
  18. 113 0
      README_nl_NL.md
  19. 4 0
      README_tr_TR.md
  20. 44 43
      README_zh_CN.md
  21. 1 0
      cli/.eslintrc.js
  22. 334 280
      cli/package-lock.json
  23. 718 95
      cli/src/api/open-api/api.ts
  24. 1 1
      cli/src/api/open-api/base.ts
  25. 1 1
      cli/src/api/open-api/common.ts
  26. 1 1
      cli/src/api/open-api/configuration.ts
  27. 1 1
      cli/src/api/open-api/index.ts
  28. 2 2
      cli/src/cli/base-command.ts
  29. 1 0
      cli/src/cores/constants.ts
  30. 1 1
      cli/src/cores/models/crawled-asset.ts
  31. 8 8
      cli/src/index.ts
  32. 1 1
      cli/src/services/session.service.spec.ts
  33. 8 1
      cli/src/services/session.service.ts
  34. 4 15
      cli/src/services/upload.service.spec.ts
  35. 3 3
      cli/src/services/upload.service.ts
  36. 0 16
      docker/.env.test
  37. 39 27
      docker/docker-compose.dev.yml
  38. 5 3
      docker/docker-compose.prod.yml
  39. 18 31
      docker/docker-compose.test.yml
  40. 6 2
      docker/docker-compose.yml
  41. 12 109
      docker/example.env
  42. 1 1
      docker/hwaccel.yml
  43. 51 21
      docs/docs/FAQ.md
  44. 1 1
      docs/docs/administration/reverse-proxy.md
  45. 39 6
      docs/docs/administration/server-commands.md
  46. 6 0
      docs/docs/developer/architecture.md
  47. 17 0
      docs/docs/developer/testing.md
  48. 10 6
      docs/docs/features/bulk-upload.md
  49. 170 0
      docs/docs/features/libraries.md
  50. 7 13
      docs/docs/features/read-only-gallery.md
  51. 85 0
      docs/docs/guides/database-queries.md
  52. 0 4
      docs/docs/guides/docker-help.md
  53. 26 0
      docs/docs/guides/machine-learning.md
  54. 113 0
      docs/docs/install/config-file.md
  55. 0 1
      docs/docs/install/docker-compose.md
  56. 53 30
      docs/docs/install/environment-variables.md
  57. 1 1
      docs/docs/install/post-install.mdx
  58. 1 1
      docs/docs/overview/help.md
  59. 1 1
      docs/docs/overview/introduction.mdx
  60. 7 0
      docs/docusaurus.config.js
  61. 345 488
      docs/package-lock.json
  62. 6 3
      docs/package.json
  63. 86 0
      docs/src/components/timeline.tsx
  64. 4 0
      docs/src/css/custom.css
  65. 12 9
      docs/src/pages/index.tsx
  66. 0 7
      docs/src/pages/markdown-page.md
  67. 509 0
      docs/src/pages/milestones.tsx
  68. 98 0
      docs/static/img/immich-logo.svg
  69. 3 1
      docs/tsconfig.json
  70. 7 5
      machine-learning/Dockerfile
  71. 4 2
      machine-learning/README.md
  72. 22 0
      machine-learning/README_fr_FR.md
  73. 46 9
      machine-learning/app/config.py
  74. 6 86
      machine-learning/app/conftest.py
  75. 83 98
      machine-learning/app/main.py
  76. 1 1
      machine-learning/app/models/__init__.py
  77. 95 12
      machine-learning/app/models/base.py
  78. 2 2
      machine-learning/app/models/cache.py
  79. 122 12
      machine-learning/app/models/clip.py
  80. 72 24
      machine-learning/app/models/facial_recognition.py
  81. 50 11
      machine-learning/app/models/image_classification.py
  82. 2 30
      machine-learning/app/schemas.py
  83. 111 55
      machine-learning/app/test_main.py
  84. 0 24
      machine-learning/load_test.sh
  85. 62 22
      machine-learning/locustfile.py
  86. 17 0
      machine-learning/log_conf.json
  87. 579 274
      machine-learning/poetry.lock
  88. 30 6
      machine-learning/pyproject.toml
  89. 2 0
      machine-learning/requirements.txt
  90. 1570 0
      machine-learning/responses.json
  91. 15 0
      machine-learning/start.sh
  92. 1 1
      mobile/.fvm/fvm_config.json
  93. 11 0
      mobile/.vscode/settings.json
  94. 6 1
      mobile/android/app/build.gradle
  95. 10 2
      mobile/android/app/src/main/AndroidManifest.xml
  96. BIN
      mobile/android/app/src/main/res/drawable-hdpi/notification_icon.png
  97. BIN
      mobile/android/app/src/main/res/drawable-mdpi/notification_icon.png
  98. BIN
      mobile/android/app/src/main/res/drawable-xhdpi/notification_icon.png
  99. BIN
      mobile/android/app/src/main/res/drawable-xxhdpi/notification_icon.png
  100. BIN
      mobile/android/app/src/main/res/drawable-xxxhdpi/notification_icon.png

+ 7 - 5
.github/workflows/build-mobile.yml

@@ -19,7 +19,7 @@ jobs:
   build-sign-android:
     name: Build and sign Android
     # Skip when PR from a fork
-    if: ${{ !github.event.pull_request.head.repo.fork }}
+    if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }}
     runs-on: macos-12
 
     steps:
@@ -31,7 +31,7 @@ jobs:
           ref="${input_ref:-$github_ref}"
           echo "ref=$ref" >> $GITHUB_OUTPUT
 
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
         with:
           ref: ${{ steps.get-ref.outputs.ref }}
 
@@ -45,7 +45,7 @@ jobs:
         uses: subosito/flutter-action@v2
         with:
           channel: "stable"
-          flutter-version: "3.10.5"
+          flutter-version: "3.13.6"
           cache: true
 
       - name: Create the Keystore
@@ -64,10 +64,12 @@ jobs:
           ALIAS: ${{ secrets.ALIAS }}
           ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
           ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
-        run: flutter build apk --release
+        run: |
+          flutter build apk --release
+          flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
 
       - name: Publish Android Artifact
         uses: actions/upload-artifact@v3
         with:
           name: release-apk-signed
-          path: mobile/build/app/outputs/flutter-apk/app-release.apk
+          path: mobile/build/app/outputs/flutter-apk/*.apk

+ 1 - 1
.github/workflows/cache-cleanup.yml

@@ -13,7 +13,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Check out code
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
 
       - name: Cleanup
         run: |

+ 1 - 1
.github/workflows/codeql-analysis.yml

@@ -42,7 +42,7 @@ jobs:
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v3
+      uses: actions/checkout@v4
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL

+ 2 - 2
.github/workflows/docker-cleanup.yml

@@ -38,7 +38,7 @@ jobs:
       -
         name: Clean temporary images
         if: "${{ env.TOKEN != '' }}"
-        uses: stumpylog/image-cleaner-action/ephemeral@v0.2.0
+        uses: stumpylog/image-cleaner-action/ephemeral@v0.3.0
         with:
           token: "${{ env.TOKEN }}"
           owner: "immich-app"
@@ -70,7 +70,7 @@ jobs:
       -
         name: Clean untagged images
         if: "${{ env.TOKEN != '' }}"
-        uses: stumpylog/image-cleaner-action/untagged@v0.2.0
+        uses: stumpylog/image-cleaner-action/untagged@v0.3.0
         with:
           token: "${{ env.TOKEN }}"
           owner: "immich-app"

+ 15 - 18
.github/workflows/docker.yml

@@ -24,9 +24,6 @@ jobs:
       fail-fast: false
       matrix:
         include:
-          - context: "server"
-            image: "immich-server"
-            platforms: "linux/amd64"
           - context: "web"
             image: "immich-web"
             platforms: "linux/amd64,linux/arm64"
@@ -39,13 +36,13 @@ jobs:
 
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
 
       - name: Set up QEMU
-        uses: docker/setup-qemu-action@v2.2.0
+        uses: docker/setup-qemu-action@v3.0.0
 
       - name: Set up Docker Buildx
-        uses: docker/setup-buildx-action@v2.9.1
+        uses: docker/setup-buildx-action@v3.0.0
         # Workaround to fix error:
         # failed to push: failed to copy: io: read/write on closed pipe
         # See https://github.com/docker/build-push-action/issues/761
@@ -56,13 +53,13 @@ jobs:
       - name: Login to Docker Hub
         # Only push to Docker Hub when making a release
         if: ${{ github.event_name == 'release' }}
-        uses: docker/login-action@v2
+        uses: docker/login-action@v3
         with:
           username: ${{ secrets.DOCKERHUB_USERNAME }}
           password: ${{ secrets.DOCKERHUB_TOKEN }}
 
       - name: Login to GitHub Container Registry
-        uses: docker/login-action@v2
+        uses: docker/login-action@v3
         # Skip when PR from a fork
         if: ${{ !github.event.pull_request.head.repo.fork }}
         with:
@@ -72,7 +69,7 @@ jobs:
 
       - name: Generate docker image tags
         id: metadata
-        uses: docker/metadata-action@v4
+        uses: docker/metadata-action@v5
         with:
           flavor: |
             # Disable latest tag
@@ -100,7 +97,7 @@ jobs:
           fi
 
       - name: Build and push image
-        uses: docker/build-push-action@v4.1.1
+        uses: docker/build-push-action@v5.0.0
         with:
           context: ${{ matrix.context }}
           platforms: ${{ matrix.platforms }}
@@ -120,16 +117,16 @@ jobs:
         include:
           - context: "server"
             image: "immich-server"
-            platforms: "linux/arm64"
+            platforms: "linux/arm64,linux/amd64"
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
 
       - name: Set up QEMU
-        uses: docker/setup-qemu-action@v2.2.0
+        uses: docker/setup-qemu-action@v3.0.0
 
       - name: Set up Docker Buildx
-        uses: docker/setup-buildx-action@v2.9.1
+        uses: docker/setup-buildx-action@v3.0.0
         # Workaround to fix error:
         # failed to push: failed to copy: io: read/write on closed pipe
         # See https://github.com/docker/build-push-action/issues/761
@@ -140,13 +137,13 @@ jobs:
       - name: Login to Docker Hub
         # Only push to Docker Hub when making a release
         if: ${{ github.event_name == 'release' }}
-        uses: docker/login-action@v2
+        uses: docker/login-action@v3
         with:
           username: ${{ secrets.DOCKERHUB_USERNAME }}
           password: ${{ secrets.DOCKERHUB_TOKEN }}
 
       - name: Login to GitHub Container Registry
-        uses: docker/login-action@v2
+        uses: docker/login-action@v3
         # Skip when PR from a fork
         if: ${{ !github.event.pull_request.head.repo.fork }}
         with:
@@ -156,7 +153,7 @@ jobs:
 
       - name: Generate docker image tags
         id: metadata
-        uses: docker/metadata-action@v4
+        uses: docker/metadata-action@v5
         with:
           flavor: |
             # Disable latest tag
@@ -184,7 +181,7 @@ jobs:
           fi
 
       - name: Build and push image
-        uses: docker/build-push-action@v4.1.1
+        uses: docker/build-push-action@v5.0.0
         with:
           context: ${{ matrix.context }}
           platforms: ${{ matrix.platforms }}

+ 2 - 2
.github/workflows/prepare-release.yml

@@ -30,7 +30,7 @@ jobs:
 
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           token: ${{ secrets.ORG_RELEASE_TOKEN }}
 
@@ -64,7 +64,7 @@ jobs:
 
     steps:
       - name: Checkout
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
         with:
           token: ${{ secrets.ORG_RELEASE_TOKEN }}
 

+ 2 - 2
.github/workflows/static_analysis.yml

@@ -17,13 +17,13 @@ jobs:
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
 
       - name: Setup Flutter SDK
         uses: subosito/flutter-action@v2
         with:
           channel: "stable"
-          flutter-version: "3.10.5"
+          flutter-version: "3.13.6"
 
       - name: Install dependencies
         run: dart pub get

+ 30 - 22
.github/workflows/test.yml

@@ -13,20 +13,15 @@ jobs:
   e2e-tests:
     name: Run end-to-end test suites
     runs-on: ubuntu-latest
-    defaults:
-      run:
-        working-directory: ./server
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v3
-
-      - name: Run npm install
-        run: npm ci
+        uses: actions/checkout@v4
+        with:
+          submodules: "recursive"
 
       - name: Run e2e tests
-        run: npm run test:e2e
-        if: ${{ !cancelled() }}
+        run: docker compose -f ./docker/docker-compose.test.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build
 
   doc-tests:
     name: Run documentation checks
@@ -37,7 +32,7 @@ jobs:
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
 
       - name: Run npm install
         run: npm ci
@@ -59,7 +54,7 @@ jobs:
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
 
       - name: Run npm install
         run: npm ci
@@ -89,7 +84,7 @@ jobs:
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
 
       - name: Run npm install
         run: npm ci
@@ -115,7 +110,7 @@ jobs:
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v3
+        uses: actions/checkout@v4
 
       - name: Run npm install
         run: npm ci
@@ -144,12 +139,12 @@ jobs:
     name: Run mobile unit tests
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
       - name: Setup Flutter SDK
         uses: subosito/flutter-action@v2
         with:
           channel: "stable"
-          flutter-version: "3.10.5"
+          flutter-version: "3.13.6"
       - name: Run tests
         working-directory: ./mobile
         run: flutter test -j 1
@@ -161,7 +156,7 @@ jobs:
       run:
         working-directory: ./machine-learning
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
       - name: Install poetry
         run: pipx install poetry
       - uses: actions/setup-python@v4
@@ -171,6 +166,7 @@ jobs:
       - name: Install dependencies
         run: |
           poetry install --with dev
+          poetry run pip install --no-deps -r requirements.txt
       - name: Lint with ruff
         run: |
           poetry run ruff check --format=github app
@@ -188,7 +184,7 @@ jobs:
     name: Check generated files are up-to-date
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
       - name: Run API generation
         run: npm --prefix server run api:generate
       - name: Find file changes
@@ -222,15 +218,27 @@ jobs:
           --health-retries 5
         ports:
           - 5432:5432
+    defaults:
+      run:
+        working-directory: ./server
+
     steps:
-      - uses: actions/checkout@v3
+      - name: Checkout code
+        uses: actions/checkout@v4
+
       - name: Install server dependencies
-        run: npm --prefix server ci
+        run: npm ci
+
+      - name: Build the
+        run: npm run build
+
       - name: Run existing migrations
-        run: npm --prefix server run typeorm:migrations:run
+        run: npm run typeorm:migrations:run
+
       - name: Generate new migrations
         continue-on-error: true
-        run: npm --prefix server run typeorm:migrations:generate ./src/infra/migrations/TestMigration
+        run: npm run typeorm:migrations:generate ./src/infra/migrations/TestMigration
+
       - name: Find file changes
         uses: tj-actions/verify-changed-files@v13.1
         id: verify-changed-files
@@ -248,7 +256,7 @@ jobs:
   #   name: Run mobile end-to-end integration tests
   #   runs-on: macos-latest
   #   steps:
-  #     - uses: actions/checkout@v3
+  #     - uses: actions/checkout@v4
   #     - uses: actions/setup-java@v3
   #       with:
   #         distribution: 'zulu'

+ 1 - 0
.gitignore

@@ -4,6 +4,7 @@
 .idea
 
 docker/upload
+docker/library
 uploads
 coverage
 

+ 3 - 0
.gitmodules

@@ -1,3 +1,6 @@
 [submodule "mobile/.isar"]
 	path = mobile/.isar
 	url = https://github.com/isar/isar
+[submodule "server/test/assets"]
+	path = server/test/assets
+	url = https://github.com/immich-app/test-assets

+ 5 - 2
Makefile

@@ -4,6 +4,9 @@ dev:
 dev-new:
 	docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
 
+dev-down:
+	docker compose -f ./docker/docker-compose.dev.yml down --remove-orphans
+
 dev-new-update:
 	docker compose -f ./docker/docker-compose.dev.yml up --build -V --remove-orphans
 
@@ -20,7 +23,7 @@ pull-stage:
 	docker-compose -f ./docker/docker-compose.staging.yml pull
 
 test-e2e:
-	docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test -p immich-test-e2e up  --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server-test --remove-orphans --build
+	docker compose -f ./docker/docker-compose.test.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build
 
 prod:
 	docker-compose -f ./docker/docker-compose.prod.yml up --build -V --remove-orphans
@@ -32,4 +35,4 @@ api:
 	cd ./server && npm run api:generate
 
 attach-server:
-	docker exec -it docker_immich-server_1 sh
+	docker exec -it docker_immich-server_1 sh

+ 7 - 2
README.md

@@ -22,13 +22,18 @@
   <a href="README_tr_TR.md">Türkçe</a>
   <a href="README_ca_ES.md">Català</a>
   <a href="README_es_ES.md">Español</a>
+  <a href="README_fr_FR.md">Français</a>
+  <a href="README_nl_NL.md">Nederlands</a>
+  <a href="README_ja_JP.md">日本語</a>
+  <a href="README_it_IT.md">Italiano</a>
 </p>
 
 ## Disclaimer
 
 - ⚠️ The project is under **very active** development.
 - ⚠️ Expect bugs and breaking changes.
-- ⚠️ **Do not use the app as the only way to store your photos and videos!**
+- ⚠️ **Do not use the app as the only way to store your photos and videos.**
+- ⚠️ Always follow [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) backup plan for your precious photos and videos!
 
 ## Content
 
@@ -84,7 +89,7 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
 | User-defined storage structure               | Yes    | Yes |
 | Public Sharing                               | No     | Yes |
 | Archive and Favorites                        | Yes    | Yes |
-| Global Map                                   | No     | Yes |
+| Global Map                                   | Yes    | Yes |
 | Partner Sharing                              | Yes    | Yes |
 | Facial recognition and clustering            | Yes    | Yes |
 | Memories (x years ago)                       | Yes    | Yes |

+ 4 - 0
README_ca_ES.md

@@ -22,6 +22,10 @@
   <a href="README_zh_CN.md">中文</a>
   <a href="README_tr_TR.md">Türkçe</a>
   <a href="README_ca_ES.md">Español</a>
+  <a href="README_fr_FR.md">Français</a>
+  <a href="README_nl_NL.md">Nederlands</a>
+  <a href="README_ja_JP.md">日本語</a>
+  <a href="README_it_IT.md">Italiano</a>
 </p>
 
 ## Avís legal

+ 3 - 0
README_es_ES.md

@@ -22,6 +22,9 @@
   <a href="README_zh_CN.md">中文</a>
   <a href="README_tr_TR.md">Türkçe</a>
   <a href="README_ca_ES.md">Català</a>
+  <a href="README_fr_FR.md">Français</a>
+  <a href="README_ja_JP.md">日本語</a>
+  <a href="README_it_IT.md">Italiano</a>
 </p>
 
 ## Descargo de responsabilidad

+ 113 - 0
README_fr_FR.md

@@ -0,0 +1,113 @@
+<p align="center"> 
+  <br/>  
+  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-green.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: MIT"></a>
+  <a href="https://discord.gg/D8JsnBEuKb">
+    <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Discord"/>
+  </a>
+  <br/>  
+  <br/>   
+</p>
+
+<p align="center">
+<img src="design/immich-logo.svg" width="150" title="Login With Custom URL">
+</p>
+<h3 align="center">Immich - Solution de sauvegarde performante et auto-hébergée des photos et des vidéos</h3>
+<br/>
+<a href="https://immich.app">
+<img src="design/immich-screenshots.png" title="Main Screenshot">
+</a>
+<br/>
+<p align="center">
+  <a href="README_zh_CN.md">中文</a>
+  <a href="README_tr_TR.md">Türkçe</a>
+  <a href="README_ca_ES.md">Català</a>
+  <a href="README_es_ES.md">Español</a>
+  <a href="README_fr_FR.md">Français</a>
+  <a href="README_nl_NL.md">Nederlands</a>
+  <a href="README_ja_JP.md">日本語</a>
+  <a href="README_it_IT.md">Italiano</a>
+</p>
+
+## Clause de non-responsabilité
+
+- ⚠️ Le projet est en **très fort** développement.
+- ⚠️ Attendez-vous à rencontrer des bugs et des changements importants.
+- ⚠️ **N'utilisez pas cette application comme seule façon de sauvegarder vos photos et vos vidéos.**
+- ⚠️ Ayez toujours un plan de sauvegarde en [3-2-1](https://www.seagate.com/fr/fr/blog/what-is-a-3-2-1-backup-strategy/) pour vos précieuses photos et vidéos !
+
+## Sommaire
+
+- [Documentation officielle](https://immich.app/docs)
+- [Feuille de route](https://github.com/orgs/immich-app/projects/1)
+- [Démo](#demo)
+- [Fonctionnalités](#features)
+- [Introduction](https://immich.app/docs/overview/introduction)
+- [Installation](https://immich.app/docs/install/requirements)
+- [Contribution](https://immich.app/docs/overview/support-the-project)
+- [Soutenir le projet](#support-the-project)
+
+## Documentation
+
+Vous pouvez trouver la documentation principale ainsi que les guides d'installation sur https://immich.app/.
+
+## Démo
+
+Vous pouvez accéder à la démo Web sur https://demo.immich.app
+
+Pour l'application mobile, vous pouvez utiliser `https://demo.immich.app/api` dans le champ 'URL du point d'accès au serveur'
+
+```bash title="Demo Credential"
+Les identifiants
+email: demo@immich.app
+mot de passe: demo
+```
+
+```
+Caractéristiques: Plan gratuit Oracle VM - Amsterdam - 2.4Ghz quatre-cœurs ARM64 CPU, 24GB RAM
+```
+
+# Fonctionnalités
+
+| Fonctionnalités                                                  | Mobile | Web |
+| ---------------------------------------------------------------- | ------ | --- |
+| Téléverser et voir les vidéos et photos                          | Oui    | Oui |
+| Sauvegarde automatique quand l'application est ouverte           | Oui    | N/A |
+| Sélection des albums à sauvegarder                               | Oui    | N/A |
+| Télécharger les photos et les vidéos sur l'appareil              | Oui    | Oui |
+| Support multi-utilisateur                                        | Oui    | Oui |
+| Albums et albums partagés                                        | Oui    | Oui |
+| Barre de défilement mobile                                       | Oui    | Oui |
+| Support des formats raw                                          | Oui    | Oui |
+| Vue sur les métadonnées (EXIF, carte)                            | Oui    | Oui |
+| Rechercher par métadonnées, objets, faces et CLIP                | Oui    | Oui |
+| Fonctions d'administration (gestion des utilisateurs)            | Non    | Oui |
+| Sauvegarde en tâche de fond                                      | Oui    | N/A |
+| Défilement virtuel                                               | Oui    | Oui |
+| Support de l'OAuth                                               | Oui    | Oui |
+| Clés d'API                                                       | N/A    | Oui |
+| Sauvegarde et lecture des LivePhotos                             | iOS    | Oui |
+| Structure de stockage définissable                               | Oui    | Oui |
+| Partage public                                                   | Non    | Oui |
+| Archives et favoris                                              | Oui    | Oui |
+| Carte globale                                                    | Non    | Oui |
+| Partage entre utilisateurs                                       | Oui    | Oui |
+| Reconnaissance et regroupement facial                            | Oui    | Oui |
+| Souvenirs (il y a x années)                                      | Oui    | Oui |
+| Support hors-ligne                                               | Oui    | Non |
+| Gallerie en lecture seule                                        | Oui    | Oui |
+
+# Soutenir le projet
+
+Je me suis engagé sur ce projet, et je ne compte pas m'arrêter. Je continuerai à mettre à jour les documentations, d'ajouter de nouvelles fonctionnalités et de résoudre des bugs. Mais je ne peux pas faire cela seul. Donc j'ai besoin de votre aide pour me donner encore plus de motivation et ainsi continuer.
+
+Comme l'ont dit nos hôtes dans le [selfhosted.show - Dans l'épisode 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418), c'est un travail colossal ce que l'équipe et moi faisons. J'aimerais un jour être capable de faire ça à temps plein, c'est pourquoi je vous demande votre aide pour rendre cela possible.
+
+Si vous estimez que c'est pour la bonne cause et que vous prévoyez d'utiliser l'application pour un moment, s'il-vous-plaît, pensez à soutenir le projet avec les moyens ci-dessous.
+
+## Donation
+
+- [Donation mensuelle](https://github.com/sponsors/alextran1502) via GitHub Sponsors
+- [Donation occasionnelle](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors
+- [Librepay](https://liberapay.com/alex.tran1502/)
+- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
+- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX

+ 113 - 0
README_it_IT.md

@@ -0,0 +1,113 @@
+<p align="center"> 
+  <br/>  
+  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-green.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: MIT"></a>
+  <a href="https://discord.gg/D8JsnBEuKb">
+    <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Discord"/>
+  </a>
+  <br/>  
+  <br/>   
+</p>
+
+<p align="center">
+<img src="design/immich-logo.svg" width="150" title="Login With Custom URL">
+</p>
+<h3 align="center">Immich - Soluzione self-hosted ad alte prestazioni per backup di foto e video</h3>
+<br/>
+<a href="https://immich.app">
+<img src="design/immich-screenshots.png" title="Main Screenshot">
+</a>
+<br/>
+<p align="center">
+  <a href="README.md">English</a>
+  <a href="README_zh_CN.md">中文</a>
+  <a href="README_tr_TR.md">Türkçe</a>
+  <a href="README_ca_ES.md">Català</a>
+  <a href="README_es_ES.md">Español</a>
+  <a href="README_fr_FR.md">Français</a>
+  <a href="README_nl_NL.md">Nederlands</a>
+  <a href="README_ja_JP.md">日本語</a>
+</p>
+
+## Declino di responsabilità
+
+- ⚠️ Il progetto è in fase di sviluppo **molto avanzato**.
+- ⚠️ Possibilità di bug e cambiamenti rilevanti.
+- ⚠️ **Non utilizzare l'app come unico salvataggio delle tue foto e dei tuoi video.**
+- ⚠️ Utilizza sempre una tecnica [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) di backup per le foto e i video a cui tieni!
+
+## Contenuto
+
+- [Documentazione Ufficiale](https://immich.app/docs)
+- [Roadmap](https://github.com/orgs/immich-app/projects/1)
+- [Demo](#demo)
+- [Funzionalità](#features)
+- [Introduzione](https://immich.app/docs/overview/introduction)
+- [Installazione](https://immich.app/docs/install/requirements)
+- [Linee Guida per Contribuire](https://immich.app/docs/overview/support-the-project)
+- [Supporta il Progetto](#support-the-project)
+
+## Documentazione
+
+La documentazione ufficiale, inclusa la guida all'installazione, è disponibile qui: https://immich.app/.
+
+## Demo
+
+Prova la demo del progetto https://demo.immich.app
+
+Sull'app mobile, imposta `https://demo.immich.app/api` come `Server Endpoint URL`
+
+```bash title="Demo Credential"
+Credenziali di accesso
+email: demo@immich.app
+password: demo
+```
+
+```
+Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
+```
+
+# Funzionalità
+
+| Funzionalità                                   | Mobile | Web |
+| ---------------------------------------------- | ------ | --- |
+| Caricamento e visualizzazione di foto e video  |   Sì   | Sì  |
+| Backup automatico quando l'app è in esecuzione |   Sì   | N/A |
+| Selezione degli album per backup               |   Sì   | N/A |
+| Download foto e video sul dispositivo          |   Sì   | Sì  |
+| Supporto multi utente                          |   Sì   | Sì  |
+| Album e album condivisi                        |   Sì   | Sì  |
+| Barra di scorrimento con trascinamento         |   Sì   | Sì  |
+| Supporto formati raw                           |   Sì   | Sì  |
+| Visualizzazione metadata (EXIF, map)           |   Sì   | Sì  |
+| Ricerca per metadata, oggetti, volti e CLIP    |   Sì   | Sì  |
+| Funzioni di amministrazione degli utenti       |   No   | Sì  |
+| Backup in background                           |   Sì   | N/A |
+| Scroll virtuale                                |   Sì   | Sì  |
+| Supporto OAuth                                 |   Sì   | Sì  |
+| API Keys                                       |  N/A   | Sì  |
+| Backup e riproduzione di LivePhoto             |  iOS   | Sì  |
+| Archiviazione impostata dall'utente            |   Sì   | Sì  |
+| Condivisione pubblica                          |   No   | Sì  |
+| Archivio e Preferiti                           |   Sì   | Sì  |
+| Mappa globale                                  |   Sì   | Sì  |
+| Collaborazione con utenti                      |   Sì   | Sì  |
+| Riconoscimento facciale e categorizzazione     |   Sì   | Sì  |
+| Ricordi (x anni fa)                            |   Sì   | Sì  |
+| Supporto offline                               |   Sì   | No  |
+| Galleria sola lettura                          |   Sì   | Sì  |
+
+# Supporta il progetto
+
+Mi dedico al progetto e non smetterò di farlo. Manterrò aggiornata la documentazione, aggiungerò nuove funzioni e risolverò i bug, ma non posso farlo da solo. Ho bisogno del tuo aiuto che mi da motivazione per continuare.
+
+Come detto dal nostro host [selfhosted.show - Nell'episodio 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418), quello che il team ed io stiamo facendo è un lavoro enorme. Mi piacerebbe dedicarmi al progetto full-time e chiedo il tuo aiuto affinchè sia possibile.
+
+Se pensi che Immich sia una buona causa e che l'app sia qualcosa che useresti nel lungo termine, sappi che puoi supportare il progetto scegliendo tra le opzioni sotto elencate.
+
+## Donazioni
+
+- [Donazione mensile](https://github.com/sponsors/alextran1502) tramite GitHub Sponsors
+- [Donazione una tantum](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) tramite GitHub Sponsors
+- [Librepay](https://liberapay.com/alex.tran1502/)
+- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
+- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX

+ 112 - 0
README_ja_JP.md

@@ -0,0 +1,112 @@
+<p align="center">
+  <br/>
+  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-green.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: MIT"></a>
+  <a href="https://discord.gg/D8JsnBEuKb">
+    <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Discord"/>
+  </a>
+  <br/>
+  <br/>
+</p>
+
+<p align="center">
+<img src="design/immich-logo.svg" width="150" title="Login With Custom URL">
+</p>
+<h3 align="center">Immich - 高性能なセルフホスト 写真/ビデオバックアップソリューション</h3>
+<br/>
+<a href="https://immich.app">
+<img src="design/immich-screenshots.png" title="Main Screenshot">
+</a>
+<br/>
+<p align="center">
+  <a href="README_zh_CN.md">中文</a>
+  <a href="README_tr_TR.md">Türkçe</a>
+  <a href="README_ca_ES.md">Català</a>
+  <a href="README_es_ES.md">Español</a>
+  <a href="README_fr_FR.md">Français</a>
+  <a href="README_nl_NL.md">Nederlands</a>
+  <a href="README_it_IT.md">Italiano</a>
+</p>
+
+## 免責事項
+
+- ⚠️ このプロジェクトは **非常に活発に** 開発中です。
+- ⚠️ バグの存在や変更が入ることも予想されます。
+- ⚠️ **写真やビデオを保存する唯一の方法としてこのアプリを使用しないでください。**
+- ⚠️ 大切な写真やビデオは、常に [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) のバックアッププランに従ってください!
+
+## コンテンツ
+
+- [公式ドキュメント](https://immich.app/docs)
+- [ロードマップ](https://github.com/orgs/immich-app/projects/1)
+- [デモ](#デモ)
+- [機能](#機能)
+- [紹介](https://immich.app/docs/overview/introduction)
+- [インストール](https://immich.app/docs/install/requirements)
+- [コントリビューションガイド](https://immich.app/docs/overview/support-the-project)
+- [プロジェクトのサポート](#プロジェクトのサポート)
+
+## ドキュメント
+
+インストールガイドを含む主なドキュメントは、https://immich.app/ です。
+
+## デモ
+
+web デモは https://demo.immich.app からアクセスできます
+
+モバイルアプリの場合、`Server Endpoint URL` には `https://demo.immich.app/api` を使用することができます
+
+```bash title="Demo Credential"
+The credential
+email: demo@immich.app
+password: demo
+```
+
+```
+Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
+```
+
+# 機能
+
+| 機能                                        | モバイル | Web |
+| ------------------------------------------- | ------ | --- |
+| ビデオや写真のアップロードと表示                 | はい    | はい |
+| アプリを開いたとき自動バックアップ               | はい    | N/A |
+| バックアップ用アルバム選択                      | はい    | N/A |
+| 写真やビデオをローカルデバイスにダウンロード       | はい    | はい |
+| マルチユーザー対応                             | はい    | はい |
+| アルバムと共有アルバム                         | はい    | はい |
+| スクラブ可能/ドラッグ可能スクロールバ            | はい    | はい |
+| 生のフォーマットに対応                         | はい    | はい |
+| メタデータ表示(EXIF、地図)                   | はい    | はい |
+| メタデータ、オブジェクト、フェース、CLIPによる検索 | はい    | はい |
+| 管理機能(ユーザー管理)                       | いいえ     | はい |
+| バックグラウンドバックアップ                    | はい    | N/A |
+| 仮想スクロール                                | はい    | はい |
+| OAuth サポート                               | はい    | はい |
+| API キー                                    | N/A    | はい |
+| LivePhoto のバックアップと再生                | iOS    | はい |
+| ユーザー定義のストレージ構造                   | はい    | はい |
+| 公開シェアリング                             | いいえ     | はい |
+| アーカイブとお気に入り                        | はい    | はい |
+| グローバルマップ                             | はい    | はい |
+| パートナー共有                               | はい    | はい |
+| 思い出(x 年前)顔認識とクラスタリング           | はい    | はい |
+| 思い出(x 年前)                             | はい    | はい |
+| オフラインサポート                            | はい    | いいえ  |
+| 読み取り専用ギャラリー                        | はい    | はい |
+
+# プロジェクトのサポート
+
+私はこのプロジェクトにコミットしてきました。ドキュメントを更新し、新しい機能を追加し、バグを修正し続けるつもりですが、私ひとりではできません。だから、続けるためのモチベーションをさらに高めてくれる皆さんの助けが必要なのです。
+
+[selfhosted.show - In the episode 'The-organization-must-いいえt-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) のホストが言ったように、これはチームと私がやっていることの大規模な事業だ。そしていつの日か、フルタイムでこの仕事ができるようになりたいと思っています。
+
+もし、あなたがこのプロジェクトに賛同し、このアプリを長く使い続けたいと思われるのであれば、以下のオプションから支援をご検討ください。
+
+## 寄付
+
+- GitHub スポンサー経由の[毎月の寄付](https://github.com/sponsors/alextran1502)
+- GitHub スポンサー経由の[一回寄付](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502)
+- [Librepay](https://liberapay.com/alex.tran1502/)
+- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
+- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX

+ 113 - 0
README_nl_NL.md

@@ -0,0 +1,113 @@
+<p align="center"> 
+  <br/>  
+  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-green.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: MIT"></a>
+  <a href="https://discord.gg/D8JsnBEuKb">
+    <img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" atl="Discord"/>
+  </a>
+  <br/>  
+  <br/>   
+</p>
+
+<p align="center">
+<img src="design/immich-logo.svg" width="150" title="Login met aangepaste URL">
+</p>
+<h3 align="center">Immich - Hoogwaardige, self-hosted back-up oplossing voor foto's en video's</h3>
+<br/>
+<a href="https://immich.app">
+<img src="design/immich-screenshots.png" title="Main Screenshot">
+</a>
+<br/>
+<p align="center">
+  <a href="README_zh_CN.md">中文</a>
+  <a href="README_tr_TR.md">Türkçe</a>
+  <a href="README_ca_ES.md">Català</a>
+  <a href="README_es_ES.md">Español</a>
+  <a href="README_fr_FR.md">Français</a>
+  <a href="README_nl_NL.md">Nederlands</a>
+  <a href="README_ja_JP.md">日本語</a>
+  <a href="README_it_IT.md">Italiano</a>
+</p>
+
+## Disclaimer
+
+- ⚠️ Het project wordt momenteel **zeer actief** ontwikkeld.
+- ⚠️ Verwacht bugs en ingrijpende wijzigingen.
+- ⚠️ **Gebruik de app niet als de enige manier om uw foto's en video's op te slaan.**
+- ⚠️ Volg altijd het [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) backup plan voor je kostbare foto's en video's!
+
+## Inhoud
+
+- [Officiële documentatie](https://immich.app/docs)
+- [Roadmap](https://github.com/orgs/immich-app/projects/1)
+- [Demo](#demo)
+- [Functies](#functies)
+- [Introductie](https://immich.app/docs/overview/introduction)
+- [Installatie](https://immich.app/docs/install/requirements)
+- [Richtlijnen voor bijdragen](https://immich.app/docs/overview/support-the-project)
+- [Steun het project](#steun-het-project)
+
+## Documentatie
+
+De belangrijkste documentatie, inclusief installatie handleidingen, zijn te vinden op https://immich.app/.
+
+## Demo
+
+De demo is te bekijken op https://demo.immich.app.
+
+Voor de mobiele app kunt u gebruik maken van `https://demo.immich.app/api` voor de `Server Endpoint URL`
+
+```bash title="Demo Credential"
+De inloggegevens
+email: demo@immich.app
+wachtwoord: demo
+```
+
+```
+Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
+```
+
+# Functies
+
+| Functies                                            | Mobiel | Web |
+|-----------------------------------------------------|--------|-----|
+| Upload en bekijk video's en foto's                  | Ja     | Ja  |
+| Automatische back-up wanneer de app wordt geopend   | Ja     | NVT |
+| Selectieve album(s) voor back-up                    | Ja     | NVT |
+| Download foto's en video's naar een lokaal apparaat | Ja     | Ja  |
+| Ondersteuning voor meerdere gebruikers              | Ja     | Ja  |
+| Album en gedeelde albums                            | Ja     | Ja  |
+| Versleepbare scroll balk                            | Ja     | Ja  |
+| Ondersteuning voor het RAW formaat                  | Ja     | Ja  |
+| Metagegevensweergave (EXIF, kaart)                  | Ja     | Ja  |
+| Zoek op metagegevens, objecten, gezichten en CLIP   | Ja     | Ja  |
+| Administratieve functies (gebruikersbeheer)         | Nee    | Ja  |
+| Back-up op de achtergrond                           | Ja     | NVT |
+| Virtueel scrollen                                   | Ja     | Ja  |
+| OAuth-ondersteuning                                 | Ja     | Ja  |
+| API-sleutels                                        | NVT    | Ja  |
+| LivePhoto-back-up en weergave                       | iOS    | Ja  |
+| Door de gebruiker gedefinieerde opslagstructuur     | Ja     | Ja  |
+| Openbaar delen                                      | Nee    | Ja  |
+| Archief en Favorieten                               | Ja     | Ja  |
+| Wereldkaart                                         | Ja     | Ja  |
+| Delen met partner                                   | Ja     | Ja  |
+| Gezichtsherkenning en groepering                    | Ja     | Ja  |
+| Herinneringen (x jaar geleden)                      | Ja     | Ja  |
+| Offline-ondersteuning                               | Ja     | Nee |
+| Alleen-lezen galerij                                | Ja     | Ja  |
+
+# Steun het project
+
+Ik ben trouw aan dit project en ik zal niet stoppen. Ik zal de documenten blijven bijwerken, nieuwe functies toevoegen en bugs oplossen. Maar ik kan het niet alleen. Ik heb dus jouw hulp nodig om mij extra motivatie te geven om door te gaan.
+
+Als onze gastheren in de [selfhosted.show - In de aflevering 'The-organization-must-Neet-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) zeiden, dit is een eNeerme onderneming van wat het team en ik doen. En ik zou dit graag fulltime willen doen, ik vraag jouw hulp om dat mogelijk te maken.
+
+Als je denkt dat dit het juiste doel is en de app iets is dat je jezelf al heel lang ziet gebruiken, overweeg dan om het project te steunen met de onderstaande optie.
+
+## Doneren
+
+- [Maandelijkse donatie](https://github.com/sponsors/alextran1502) via GitHub Sponsors
+- [Eenmalige donatie](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) via GitHub Sponsors
+- [Librepay](https://liberapay.com/alex.tran1502/)
+- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
+- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX

+ 4 - 0
README_tr_TR.md

@@ -22,6 +22,10 @@
   <a href="README_zh_CN.md">中文</a>
   <a href="README_ca_ES.md">Català</a>
   <a href="README_es_ES.md">Español</a>
+  <a href="README_fr_FR.md">Français</a>
+  <a href="README_nl_NL.md">Nederlands</a>
+  <a href="README_ja_JP.md">日本語</a>
+  <a href="README_it_IT.md">Italiano</a>
 </p>
 
 ## Feragatname

+ 44 - 43
README_zh_CN.md

@@ -13,7 +13,7 @@
 </p>
 <h3 align="center">Immich - 高性能的自托管照片和视频备份方案</h3>
 <p align="center">  
-请注意: 此README不是由Immich团队维护, 这意味着它在某一时间点不会被更新,因为我们是依靠贡献者来更新的。感谢理解。
+请注意: 此 README 不是由 Immich 团队维护, 而是依靠贡献者来更新的,这意味着它可能并不会被及时更新。感谢理解。
 </p>
 <br/>
 <a href="https://immich.app">
@@ -26,34 +26,40 @@
   <a href="README_tr_TR.md">Türkçe</a>
   <a href="README_ca_ES.md">Català</a>
   <a href="README_es_ES.md">Español</a>
+  <a href="README_fr_FR.md">Français</a>
+  <a href="README_nl_NL.md">Nederlands</a>
+  <a href="README_ja_JP.md">日本語</a>
+  <a href="README_it_IT.md">Italiano</a>
 </p>
 
 
 ## 免责声明
 
-- ⚠️ 本项目正在 **非常活跃** 的开发中。
-- ⚠️ 可能存在bug或者重大变更。
-- ⚠️ **不要把本软件作为你存储照片或视频的唯一方式!**
+- ⚠️ 本项目正在 **非常活跃** 地开发中。
+- ⚠️ 可能存在 bug 或者随时有重大变更。
+- ⚠️ **不要把本软件作为您存储照片或视频的唯一方式。**
+- ⚠️ 为了您宝贵的照片与视频,始终遵守 [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) 备份方案!
 
 ## 目录
 
-- [官方文档](https://immich.app/docs/overview/introduction)
+- [官方文档](https://immich.app/docs)
+- [路线图](https://github.com/orgs/immich-app/projects/1)
 - [示例](#示例)
 - [功能特性](#功能特性)
 - [介绍](https://immich.app/docs/overview/introduction)
 - [安装](https://immich.app/docs/install/requirements)
 - [贡献指南](https://immich.app/docs/overview/support-the-project)
-- [支持本项目](#support-the-project)
-- [已知问题](#known-issues)
+- [支持本项目](#支持本项目)
 
 ## 官方文档
 
-你可以在 https://immich.app/ 找到包含安装手册的官方文档.
+您可以在 https://immich.app/ 找到官方文档(包含安装手册)。
+
 ## 示例
 
-你可以在 https://demo.immich.app  访问示例.
+您可以在 https://demo.immich.app  访问示例。
 
-在移动端, 你可以使用 `https://demo.immich.app/api`获取`服务终端链接`
+在移动端, 您可以使用 `https://demo.immich.app/api` 获取 `服务终端链接`
 
 ```bash title="示例认证信息"
 认证信息
@@ -62,57 +68,52 @@
 ```
 
 ```
-规格: 甲骨文免费虚拟机套餐-阿姆斯特丹 4核 2.4Ghz ARM64 CPU, 24GB RAM。
+规格: 甲骨文免费虚拟机套餐——阿姆斯特丹 4核 2.4Ghz ARM64 CPU, 24GB RAM。
 ```
 
 # 功能特性
 
 | 功能特性                                     | 移动端  | 网页端 |
 | ------------------------------------------- | ------- | --- |
-| 上传并查看照片和视频                       | 是     | 是 |
-| 软件运行时自动备份          | 是     | N/A |
+| 上传并查看照片和视频                         | 是     | 是 |
+| 软件运行时自动备份                           | 是     | N/A |
 | 选择需要备份的相册          | 是     | N/A |
-| 下载照片和视频到本地  | 是     | 是 |
+| 下载照片和视频到本地        | 是     | 是 |
 | 多用户支持                          | 是     | 是 |
 | 相册                                       | 是     | 是 |
 | 共享相册                               | 是     | 是 |
 | 可拖动的快速导航栏   | 是     | 是 |
 | 支持RAW格式 (HEIC, HEIF, DNG, Apple ProRaw) | 是     | 是 |
-| 元数据视图 (EXIF, 地图)                   | 是     | 是 |
-| 通过元数据、对象和标签进行搜索  | 是     | No  |
-| 管理功能 (用户管理)  | N/A     | 是 |
-| 后台备份                         | Android | N/A |
+| 元数据视图(EXIF, 地图)                   | 是     | 是 |
+| 通过元数据、对象和标签进行搜索  | 是     |   |
+| 管理功能(用户管理)  | 否     | 是 |
+| 后台备份                         |  | N/A |
 | 虚拟滚动                             | 是     | 是 |
-| OAuth支持                               | 是     | 是 |
-| 实时照片备份和查看 (仅iOS)   | 是     | 是 |
+| OAuth 支持                               | 是     | 是 |
+| API Keys|N/A|是|
+| 实况照片备份和查看   | 仅 iOS     | 是 |
+|用户自定义存储结构|是|是|
+|公共分享|否|是|
+|归档与收藏功能|是|是|
+|全局地图|否|是|
+|好友分享|是|是|
+|人像识别与分组|是|是|
+|回忆(那年今日)|是|是|
+|离线支持|是|否|
+|只读相册|是|是|
 
 # 支持本项目
 
-我已经致力于本项目并且将我会持续更新文档、新增功能和修复问题。但是我不能一个人走下去,所以我需要你给予我走下去的动力。
+我已经致力于本项目并且将我会持续更新文档、新增功能和修复问题。但是独木不成林,我需要您给予我坚持下去的动力。
 
-就像我主页里面 [selfhosted.show - In the episode 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) 说的一样,这是我和团队的一项艰巨的任务。我希望某一天我能够全职开发本项目,在此我希望你们能够助我梦想成真。
+就像我 [selfhosted.show - In the episode 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418) 节目里说的一样,这是我和团队的一项艰巨任务。并且我希望某一天我能够全职开发本项目,在此我请求您能够助我梦想成真。
 
-如果你使用了本项目一段时间,并且觉得上面的话有道理,那么请你按照如下方式帮助我吧。
+如果您使用了本项目一段时间,并且觉得上面的话有道理,那么请您考虑通过下列任一方式支持我吧。
 
 ## 捐赠
 
-- [按月捐赠](https://github.com/sponsors/alextran1502) via GitHub Sponsors
-- [一次捐赠](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) via Github Sponsors
-
-# 已知问题
-
-## TensorFlow 构建问题
-
-_这是一个针对于Proxmox的已知问题_
-
-TensorFlow 不能运行在很旧的CPU架构上, 需要运行在AVX和AVX2指令集的CPU上。如果你在docker-compose的命令行中遇到了 `illegal instruction core dump`的错误, 通过如下命令检查你的CPU flag寄存器然后确保你能够看到`AVX`和`AVX2`的字样:
-
-```bash
-more /proc/cpuinfo | grep flags
-```
-
-如果你在Proxmox中运行虚拟机, 虚拟机中没有启用flag寄存器。
-
-你需要在虚拟机的硬件面板中把CPU类型从`kvm64`改为`host`。
-
-`Hardware > Processors > Edit > Advanced > Type (dropdown menu) > host`
+- 通过 GitHub Sponsors [按月捐赠](https://github.com/sponsors/alextran1502)
+- 通过 Github Sponsors [单次捐赠](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502)
+- [Librepay](https://liberapay.com/alex.tran1502/)
+- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
+- 比特币: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX

+ 1 - 0
cli/.eslintrc.js

@@ -18,6 +18,7 @@ module.exports = {
     '@typescript-eslint/explicit-function-return-type': 'off',
     '@typescript-eslint/explicit-module-boundary-types': 'off',
     '@typescript-eslint/no-explicit-any': 'off',
+    '@typescript-eslint/no-floating-promises': 'error',
     'prettier/prettier': 0,
   },
 };

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 334 - 280
cli/package-lock.json


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 718 - 95
cli/src/api/open-api/api.ts


+ 1 - 1
cli/src/api/open-api/base.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.71.0
+ * The version of the OpenAPI document: 1.82.1
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
cli/src/api/open-api/common.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.71.0
+ * The version of the OpenAPI document: 1.82.1
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
cli/src/api/open-api/configuration.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.71.0
+ * The version of the OpenAPI document: 1.82.1
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
cli/src/api/open-api/index.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.71.0
+ * The version of the OpenAPI document: 1.82.1
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 2 - 2
cli/src/cli/base-command.ts

@@ -4,14 +4,14 @@ import { SessionService } from '../services/session.service';
 import { LoginError } from '../cores/errors/login-error';
 import { exit } from 'node:process';
 import os from 'os';
-import { ServerVersionReponseDto, UserResponseDto } from 'src/api/open-api';
+import { ServerVersionResponseDto, UserResponseDto } from 'src/api/open-api';
 
 export abstract class BaseCommand {
   protected sessionService!: SessionService;
   protected immichApi!: ImmichApi;
   protected deviceId!: string;
   protected user!: UserResponseDto;
-  protected serverVersion!: ServerVersionReponseDto;
+  protected serverVersion!: ServerVersionResponseDto;
 
   protected configDir;
   protected authPath;

+ 1 - 0
cli/src/cores/constants.ts

@@ -34,6 +34,7 @@ const other = [
   'orf',
   'ori',
   'pef',
+  'psd',
   'raf',
   'raw',
   'rwl',

+ 1 - 1
cli/src/cores/models/crawled-asset.ts

@@ -25,7 +25,7 @@ export class CrawledAsset {
   async process() {
     const stats = await fs.promises.stat(this.path);
     this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replace(/\s+/g, '');
-    this.fileCreatedAt = stats.ctime.toISOString();
+    this.fileCreatedAt = stats.mtime.toISOString();
     this.fileModifiedAt = stats.mtime.toISOString();
     this.fileSize = stats.size;
 

+ 8 - 8
cli/src/index.ts

@@ -19,9 +19,9 @@ program
   )
   .addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS'))
   .argument('[paths...]', 'One or more paths to assets to be uploaded')
-  .action((paths, options) => {
+  .action(async (paths, options) => {
     options.excludePatterns = options.ignore;
-    new Upload().run(paths, options);
+    await new Upload().run(paths, options);
   });
 
 program
@@ -37,18 +37,18 @@ program
   .addOption(new Option('-i, --ignore [paths...]', 'Paths to ignore').env('IMMICH_IGNORE_PATHS').default(false))
   .addOption(new Option('--no-read-only', 'Import files without read-only protection, allowing Immich to manage them'))
   .argument('[paths...]', 'One or more paths to assets to be imported')
-  .action((paths, options) => {
+  .action(async (paths, options) => {
     options.import = true;
     options.excludePatterns = options.ignore;
-    new Upload().run(paths, options);
+    await new Upload().run(paths, options);
   });
 
 program
   .command('server-info')
   .description('Display server information')
 
-  .action(() => {
-    new ServerInfo().run();
+  .action(async () => {
+    await new ServerInfo().run();
   });
 
 program
@@ -56,8 +56,8 @@ program
   .description('Login using an API key')
   .argument('[instanceUrl]')
   .argument('[apiKey]')
-  .action((paths, options) => {
-    new LoginKey().run(paths, options);
+  .action(async (paths, options) => {
+    await new LoginKey().run(paths, options);
   });
 
 program.parse(process.argv);

+ 1 - 1
cli/src/services/session.service.spec.ts

@@ -67,7 +67,7 @@ describe('SessionService', () => {
     });
   });
 
-  it('should create auth file when logged in', async () => {
+  it.skip('should create auth file when logged in', async () => {
     mockfs();
 
     await sessionService.keyLogin('https://test/api', 'pNussssKSYo5WasdgalvKJ1n9kdvaasdfbluPg');

+ 8 - 1
cli/src/services/session.service.ts

@@ -53,7 +53,14 @@ export class SessionService {
 
     if (!fs.existsSync(this.configDir)) {
       // Create config folder if it doesn't exist
-      fs.mkdirSync(this.configDir, { recursive: true });
+      const created = await fs.promises.mkdir(this.configDir, { recursive: true });
+      if (!created) {
+        throw new Error(`Failed to create config folder ${this.configDir}`);
+      }
+    }
+
+    if (!fs.existsSync(this.configDir)) {
+      console.error('waah');
     }
 
     fs.writeFileSync(this.authPath, yaml.stringify({ instanceUrl, apiKey }));

+ 4 - 15
cli/src/services/upload.service.spec.ts

@@ -1,35 +1,24 @@
 import { UploadService } from './upload.service';
-import mockfs from 'mock-fs';
 import axios from 'axios';
-import mockAxios from 'jest-mock-axios';
 import FormData from 'form-data';
 import { ApiConfiguration } from '../cores/api-configuration';
 
+jest.mock('axios', () => jest.fn());
+
 describe('UploadService', () => {
   let uploadService: UploadService;
 
-  beforeAll(() => {
-    // Write a dummy output before mock-fs to prevent some annoying errors
-    console.log();
-  });
-
   beforeEach(() => {
     const apiConfiguration = new ApiConfiguration('https://example.com/api', 'key');
 
     uploadService = new UploadService(apiConfiguration);
   });
 
-  it('should upload a single file', async () => {
+  it('should call axios', async () => {
     const data = new FormData();
 
-    uploadService.upload(data);
+    await uploadService.upload(data);
 
-    mockAxios.mockResponse();
     expect(axios).toHaveBeenCalled();
   });
-
-  afterEach(() => {
-    mockfs.restore();
-    mockAxios.reset();
-  });
 });

+ 3 - 3
cli/src/services/upload.service.ts

@@ -42,21 +42,21 @@ export class UploadService {
     };
   }
 
-  public checkIfAssetAlreadyExists(path: string, checksum: string): Promise<any> {
+  public checkIfAssetAlreadyExists(path: string, checksum: string) {
     this.checkAssetExistenceConfig.data = JSON.stringify({ assets: [{ id: path, checksum: checksum }] });
 
     // TODO: retry on 500 errors?
     return axios(this.checkAssetExistenceConfig);
   }
 
-  public upload(data: FormData): Promise<any> {
+  public upload(data: FormData) {
     this.uploadConfig.data = data;
 
     // TODO: retry on 500 errors?
     return axios(this.uploadConfig);
   }
 
-  public import(data: any): Promise<any> {
+  public import(data: any) {
     this.importConfig.data = data;
 
     // TODO: retry on 500 errors?

+ 0 - 16
docker/.env.test

@@ -1,16 +0,0 @@
-# Database
-DB_HOSTNAME=immich-database-test
-DB_USERNAME=postgres
-DB_PASSWORD=postgres
-DB_DATABASE_NAME=e2e_test
-
-# Redis
-REDIS_HOSTNAME=immich-redis-test
-
-# Upload File Config
-UPLOAD_LOCATION=./upload
-
-# WEB
-VITE_SERVER_ENDPOINT=http://localhost:2283/api
-
-TYPESENSE_ENABLED=false

+ 39 - 27
docker/docker-compose.dev.yml

@@ -11,8 +11,9 @@ services:
     command: npm run start:debug immich
     volumes:
       - ../server:/usr/src/app
-      - ${UPLOAD_LOCATION}:/usr/src/app/upload
+      - ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
       - /usr/src/app/node_modules
+      - /etc/localtime:/etc/localtime:ro
     ports:
       - 3001:3001
       - 9230:9230
@@ -20,30 +21,15 @@ services:
       - .env
     environment:
       - NODE_ENV=development
+    ulimits:
+      nofile:
+        soft: 1048576
+        hard: 1048576
     depends_on:
       - redis
       - database
       - typesense
 
-  immich-machine-learning:
-    container_name: immich_machine_learning
-    image: immich-machine-learning-dev:latest
-    build:
-      context: ../machine-learning
-      dockerfile: Dockerfile
-    ports:
-      - 3003:3003
-    volumes:
-      - ../machine-learning/app:/usr/src/app
-      - model-cache:/cache
-    env_file:
-      - .env
-    environment:
-      - NODE_ENV=development
-    depends_on:
-      - database
-    restart: unless-stopped
-
   immich-microservices:
     container_name: immich_microservices
     image: immich-microservices:latest
@@ -57,14 +43,19 @@ services:
     command: npm run start:debug microservices
     volumes:
       - ../server:/usr/src/app
-      - ${UPLOAD_LOCATION}:/usr/src/app/upload
+      - ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
       - /usr/src/app/node_modules
+      - /etc/localtime:/etc/localtime:ro
     env_file:
       - .env
     ports:
       - 9231:9230
     environment:
       - NODE_ENV=development
+    ulimits:
+      nofile:
+        soft: 1048576
+        hard: 1048576
     depends_on:
       - database
       - immich-server
@@ -90,20 +81,43 @@ services:
     volumes:
       - ../web:/usr/src/app
       - /usr/src/app/node_modules
+    ulimits:
+      nofile:
+        soft: 1048576
+        hard: 1048576
     restart: unless-stopped
     depends_on:
       - immich-server
 
+  immich-machine-learning:
+    container_name: immich_machine_learning
+    image: immich-machine-learning-dev:latest
+    build:
+      context: ../machine-learning
+      dockerfile: Dockerfile
+    ports:
+      - 3003:3003
+    volumes:
+      - ../machine-learning:/usr/src/app
+      - model-cache:/cache
+    env_file:
+      - .env
+    environment:
+      - NODE_ENV=development
+    depends_on:
+      - database
+    restart: unless-stopped
+
   typesense:
     container_name: immich_typesense
     image: typesense/typesense:0.24.1@sha256:9bcff2b829f12074426ca044b56160ca9d777a0c488303469143dd9f8259d4dd
     environment:
       - TYPESENSE_API_KEY=${TYPESENSE_API_KEY}
       - TYPESENSE_DATA_DIR=/data
-    logging:
-      driver: none
+      # remove this to get debug messages
+      - GLOG_minloglevel=1
     volumes:
-      - tsdata:/data
+      - ${UPLOAD_LOCATION}/typesense:/data
 
   redis:
     container_name: immich_redis
@@ -119,7 +133,7 @@ services:
       POSTGRES_USER: ${DB_USERNAME}
       POSTGRES_DB: ${DB_DATABASE_NAME}
     volumes:
-      - pgdata:/var/lib/postgresql/data
+      - ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
     ports:
       - 5432:5432
 
@@ -141,6 +155,4 @@ services:
     restart: unless-stopped
 
 volumes:
-  pgdata:
   model-cache:
-  tsdata:

+ 5 - 3
docker/docker-compose.prod.yml

@@ -10,6 +10,7 @@ services:
     command: ["./start-server.sh"]
     volumes:
       - ${UPLOAD_LOCATION}:/usr/src/app/upload
+      - /etc/localtime:/etc/localtime:ro
     env_file:
       - .env
     depends_on:
@@ -29,7 +30,7 @@ services:
     env_file:
       - .env
     restart: always
-  
+
   immich-microservices:
     container_name: immich_microservices
     image: immich-microservices:latest
@@ -42,6 +43,7 @@ services:
     command: ["./start-microservices.sh"]
     volumes:
       - ${UPLOAD_LOCATION}:/usr/src/app/upload
+      - /etc/localtime:/etc/localtime:ro
     env_file:
       - .env
     depends_on:
@@ -68,8 +70,8 @@ services:
     environment:
       - TYPESENSE_API_KEY=${TYPESENSE_API_KEY}
       - TYPESENSE_DATA_DIR=/data
-    logging:
-      driver: none
+      # remove this to get debug messages
+      - GLOG_minloglevel=1
     volumes:
       - tsdata:/data
     restart: always

+ 18 - 31
docker/docker-compose.test.yml

@@ -1,46 +1,33 @@
 version: "3.8"
 
+name: "immich-test-e2e"
+
 services:
-  immich-server-test:
-    image: immich-server-test
+  immich-server:
+    image: immich-server-dev:latest
     build:
       context: ../server
       dockerfile: Dockerfile
       target: builder
     command: npm run test:e2e
-    expose:
-      - "3000"
     volumes:
       - ../server:/usr/src/app
       - /usr/src/app/node_modules
-    env_file:
-      - .env.test
     environment:
-      - NODE_ENV=development
-      - TYPESENSE_ENABLED=false
+      - DB_HOSTNAME=database
+      - DB_USERNAME=postgres
+      - DB_PASSWORD=postgres
+      - DB_DATABASE_NAME=e2e_test
+      - IMMICH_RUN_ALL_TESTS=true
     depends_on:
-      - immich-redis-test
-      - immich-database-test
-    networks:
-      - immich-test-network
-  immich-redis-test:
-    container_name: immich-redis-test
-    image: redis:6.2-alpine@sha256:70a7a5b641117670beae0d80658430853896b5ef269ccf00d1827427e3263fa3
-    networks:
-      - immich-test-network
-  immich-database-test:
-    container_name: immich-database-test
+      - database
+
+  database:
     image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
-    env_file:
-      - .env.test
+    command: -c fsync=off
     environment:
-      POSTGRES_PASSWORD: ${DB_PASSWORD}
-      POSTGRES_USER: ${DB_USERNAME}
-      POSTGRES_DB: ${DB_DATABASE_NAME}
-    volumes:
-      - /var/lib/postgresql/data
-    networks:
-      - immich-test-network
-
-networks:
-  immich-test-network:
+      POSTGRES_PASSWORD: postgres
+      POSTGRES_USER: postgres
+      POSTGRES_DB: e2e_test
+    logging:
+      driver: none

+ 6 - 2
docker/docker-compose.yml

@@ -4,9 +4,10 @@ services:
   immich-server:
     container_name: immich_server
     image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
-    command: [ "start.sh", "immich" ]
+    command: ["start.sh", "immich"]
     volumes:
       - ${UPLOAD_LOCATION}:/usr/src/app/upload
+      - /etc/localtime:/etc/localtime:ro
     env_file:
       - .env
     depends_on:
@@ -21,9 +22,10 @@ services:
     # extends:
     #   file: hwaccel.yml
     #   service: hwaccel
-    command: [ "start.sh", "microservices" ]
+    command: ["start.sh", "microservices"]
     volumes:
       - ${UPLOAD_LOCATION}:/usr/src/app/upload
+      - /etc/localtime:/etc/localtime:ro
     env_file:
       - .env
     depends_on:
@@ -54,6 +56,8 @@ services:
     environment:
       - TYPESENSE_API_KEY=${TYPESENSE_API_KEY}
       - TYPESENSE_DATA_DIR=/data
+      # remove this to get debug messages
+      - GLOG_minloglevel=1
     volumes:
       - tsdata:/data
     restart: always

+ 12 - 109
docker/example.env

@@ -1,116 +1,19 @@
-###################################################################################
-# Database
-###################################################################################
-
-# NOTE: The following four database variables support Docker secrets by adding a *_FILE suffix to the variable name
-# See the docker-compose documentation on secrets for additional details: https://docs.docker.com/compose/compose-file/compose-file-v3/#secrets
-DB_HOSTNAME=immich_postgres
-DB_USERNAME=postgres
-DB_PASSWORD=postgres
-DB_DATABASE_NAME=immich
-
-# Optional Database settings:
-# DB_PORT=5432
+# You can find documentation for all the supported env variables at https://immich.app/docs/install/environment-variables
 
-###################################################################################
-# Redis
-###################################################################################
-
-REDIS_HOSTNAME=immich_redis
-
-# REDIS_URL will be used to pass custom options to ioredis.
-# Example for Sentinel
-# {"sentinels":[{"host":"redis-sentinel-node-0","port":26379},{"host":"redis-sentinel-node-1","port":26379},{"host":"redis-sentinel-node-2","port":26379}],"name":"redis-sentinel"}
-# REDIS_URL=ioredis://eyJzZW50aW5lbHMiOlt7Imhvc3QiOiJyZWRpcy1zZW50aW5lbDEiLCJwb3J0IjoyNjM3OX0seyJob3N0IjoicmVkaXMtc2VudGluZWwyIiwicG9ydCI6MjYzNzl9XSwibmFtZSI6Im15bWFzdGVyIn0=
-
-# Optional Redis settings:
-
-# Note: these parameters are not automatically passed to the Redis Container
-# to do so, please edit the docker-compose.yml file as well. Redis is not configured
-# via environment variables, only redis.conf or the command line
-
-# REDIS_PORT=6379
-# REDIS_DBINDEX=0
-# REDIS_USERNAME=
-# REDIS_PASSWORD=
-# REDIS_SOCKET=
-
-###################################################################################
-# Upload File Location
-#
-# This is the location where uploaded files are stored.
-###################################################################################
+# The location where your uploaded files are stored
+UPLOAD_LOCATION=./library
 
-UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
+# The Immich version to use. You can pin this to a specific version like "v1.71.0"
+IMMICH_VERSION=release
 
-
-###################################################################################
-# Typesense
-###################################################################################
+# Connection secrets for postgres and typesense. You should change these to random passwords
 TYPESENSE_API_KEY=some-random-text
-# TYPESENSE_ENABLED=false
-# TYPESENSE_URL uses base64 encoding for the nodes json.
-# Example JSON that was used:
-# [
-#      { "host": "typesense-1.example.net", "port": "443", "protocol": "https" },
-#      { "host": "typesense-2.example.net", "port": "443", "protocol": "https" },
-#      { "host": "typesense-3.example.net", "port": "443", "protocol": "https" },
-# ]
-# TYPESENSE_URL=ha://WwogIHsgImhvc3QiOiAidHlwZXNlbnNlLTEuZXhhbXBsZS5uZXQiLCAicG9ydCI6ICI0NDMiLCAicHJvdG9jb2wiOiAiaHR0cHMiIH0sCiAgeyAiaG9zdCI6ICJ0eXBlc2Vuc2UtMi5leGFtcGxlLm5ldCIsICJwb3J0IjogIjQ0MyIsICJwcm90b2NvbCI6ICJodHRwcyIgfSwKICB7ICJob3N0IjogInR5cGVzZW5zZS0zLmV4YW1wbGUubmV0IiwgInBvcnQiOiAiNDQzIiwgInByb3RvY29sIjogImh0dHBzIiB9Cl0=
-
-###################################################################################
-# Reverse Geocoding
-#
-# Reverse geocoding is done locally which has a small impact on memory usage
-# This memory usage can be altered by changing the REVERSE_GEOCODING_PRECISION variable
-# This ranges from 0-3 with 3 being the most precise
-# 3 - Cities > 500 population: ~200MB RAM
-# 2 - Cities > 1000 population: ~150MB RAM
-# 1 - Cities > 5000 population: ~80MB RAM
-# 0 - Cities > 15000 population: ~40MB RAM
-####################################################################################
-
-# DISABLE_REVERSE_GEOCODING=false
-# REVERSE_GEOCODING_PRECISION=3
-
-####################################################################################
-# WEB - Optional
-#
-# Custom message on the login page, should be written in HTML form.
-# For example:
-# PUBLIC_LOGIN_PAGE_MESSAGE="This is a demo instance of Immich.<br><br>Email: <i>demo@demo.de</i><br>Password: <i>demo</i>"
-####################################################################################
-
-PUBLIC_LOGIN_PAGE_MESSAGE=
-
-####################################################################################
-# Alternative Service Addresses - Optional
-#
-# This is an advanced feature for users who may be running their immich services on different hosts.
-# It will not change which address or port that services bind to within their containers, but it will change where other services look for their peers.
-# Note: immich-microservices is bound to 3002, but no references are made
-####################################################################################
-
-IMMICH_WEB_URL=http://immich-web:3000
-IMMICH_SERVER_URL=http://immich-server:3001
-IMMICH_MACHINE_LEARNING_URL=http://immich-machine-learning:3003
-
-####################################################################################
-# Alternative API's External Address - Optional
-#
-# This is an advanced feature used to control the public server endpoint returned to clients during Well-known discovery.
-# You should only use this if you want mobile apps to access the immich API over a custom URL. Do not include trailing slash.
-# NOTE: At this time, the web app will not be affected by this setting and will continue to use the relative path: /api
-# Examples: http://localhost:3001, http://immich-api.example.com, etc
-####################################################################################
-
-#IMMICH_API_URL_EXTERNAL=http://localhost:3001
+DB_PASSWORD=postgres
 
+# The values below this line do not need to be changed
 ###################################################################################
-# Immich Version - Optional
-#
-# This allows all immich docker images to be pinned to a specific version. By default,
-# the version is "release" but could be a specific version, like "v1.59.0".
-###################################################################################
+DB_HOSTNAME=immich_postgres
+DB_USERNAME=postgres
+DB_DATABASE_NAME=immich
 
-#IMMICH_VERSION=
+REDIS_HOSTNAME=immich_redis

+ 1 - 1
docker/hwaccel.yml

@@ -20,4 +20,4 @@ services:
     #       devices:
     #         - driver: nvidia
     #           count: 1
-    #           capabilities: [gpu]
+    #           capabilities: [gpu,video]

+ 51 - 21
docs/docs/FAQ.md

@@ -16,38 +16,46 @@ sidebar_position: 7
 
 Immich doesn't have two-way synchronization ([yet](https://github.com/immich-app/immich/discussions/1006)), but the [command line tool](/docs/features/bulk-upload.md) can bulk upload items from a directory to Immich.
 
-### Why doesn't Immich watch an existing photo gallery directory?
+### Why are only photos and not videos being uploaded to Immich?
 
-The initial approach of Immich is to become a backup tool, primarily for mobile device usage. Thus, all the assets must be uploaded from the mobile client. The app was architectured to perform that job well.
+This often happens when using a reverse proxy or cloudflare tunnel in front of Immich. Make sure to set your reverse proxy to allow large POST requests. In `nginx`, set `client_max_body_size 50000M;` or similar. Cloudflare tunnels are limited to 100 mb file sizes. Also check the disk space of your reverse proxy, in some cases proxies caches requests to disk before passing them on, and if disk space runs out the request fails.
 
-### Why does my uploaded photo show up with the wrong date or time in Immich?
+### Why is Immich slow on low-memory systems like the Raspberry Pi?
 
-When a photo is initially uploaded Immich uses the create date of the file to determine where it belongs in the timeline. After that, background jobs will run that extract [exif metadata](https://en.wikipedia.org/wiki/Exif), including the CreateDate, to provide a more accurate date for the photo. If that is not available it will fallback to the modified date. If you want to ensure your photo has the right date, check the exif metadata before uploading.
+Immich optionally uses machine learning for several features. However, it can be too heavy to run on a Raspberry Pi. You can [mitigate](/docs/FAQ#how-can-i-lower-immichs-cpu-usage) this or [disable](/docs/FAQ.md#how-can-i-disable-machine-learning) machine learning entirely.
 
-If the timezone is incorrect in an uploaded photo, check the `DateTimeOriginal` exif field of the uploaded file. Immich uses the very competent library [exiftool-vendored.js](https://github.com/photostructure/exiftool-vendored.js#dates) to handle timezone parsing, but in some cases (like photos taken with DSLR cameras) it has to fallback on the local timezone. If you are using docker, this fallback will be UTC. (Note that even the photo backup app that can't be named [has the same bug!](https://photo.stackexchange.com/a/126978)) In Immich, it is possible to change this assumed fallback timezone system-wide by setting the timezone in the microservices docker container. You might need to run the "Extract Metadata" job after to effect the change.
+### How can I lower Immich's CPU usage?
 
-As an example, the following modification of `docker-compose.yml` will set the timezone of the microservices container to be `Europe/Stockholm`
+The initial backup is the most intensive due to the number of jobs running. The most CPU-intensive ones are transcoding and machine learning jobs (Tag Images, Encode CLIP, Recognize Faces), and to a lesser extent thumbnail generation. Here are some ways to lower their CPU usage:
 
-```
-    environment:
-      - TZ=Europe/Stockholm # <---- Add this line in the microservices config
-```
+- Lower the job concurrency for these jobs to 1.
+- Under Settings > Transcoding Settings > Threads, set the number of threads to a low number like 1 or 2.
+- Set the `TYPESENSE_THREAD_POOL_SIZE` environmental variable and restart the Typesense container. For instance, `TYPESENSE_THREAD_POOL_SIZE=8` will limit it to 8 threads.
+- Under Settings > Machine Learning Settings > Facial Recognition > Model Name, you can change the facial recognition model to `buffalo_s` instead of `buffalo_l`. The former is a smaller and faster model, albeit not as good.
+  - You _must_ re-run the Recognize Faces job for all images after this for facial recognition on new images to work properly.
+- If these changes are not enough, see [below](/docs/FAQ.md#how-can-i-disable-machine-learning) for how you can disable machine learning.
 
-### Why are only photos and not videos being uploaded to Immich?
+### How can I disable machine learning?
 
-This often happens when using a reverse proxy or cloudflare tunnel in front of Immich. Make sure to set your reverse proxy to allow large POST requests. In `nginx`, set `client_max_body_size 50000M;` or similar. Cloudflare tunnels are limited to 100 mb file sizes.
+:::info
+Disabling machine learning will result in a poor experience for searching and the 'Explore' page, as these are reliant on it to work as intended.
+:::
 
-### Why is Immich slow on low-memory systems like the Raspberry Pi?
+Machine learning can be disabled under Settings > Machine Learning Settings, either entirely or by model type. For instance, you can choose to disable smart search with CLIP, but keep facial recognition enabled. This means that the machine learning service will only process the enabled jobs.
 
-Immich uses optional machine-learning features to enhance search results. This feature, however, can be too heavy to run on a Raspberry Pi. To disable machine learning, comment out the `immich-machine-learning` section of your docker-compose.yml and set `IMMICH_MACHINE_LEARNING_URL=false` in your .env file.
+However, disabling all jobs will not disable the machine learning service itself. To prevent it from starting up at all in this case, you can comment out the `immich-machine-learning` section of the docker-compose.yml.
 
-### How to disable machine-learning and TypeSense?
+### How can I disable TypeSense?
 
-:::warning
-Disabling both will result in poor search experience and typesense utilizes CLIP embeddings which are generated by machine-learning.
+:::info
+Disabling Typesense will result in a poor search experience since searching is reliant on it.
 :::
 
-These features can be disabled by commenting out `immich-typesense` and `immich-machine-learning` sections of the docker-compose.yml and setting `IMMICH_MACHINE_LEARNING_URL=false` & `TYPESENSE_ENABLED=false` in your .env file.
+You can disable Typesense by commenting out the `immich-typesense` section of the docker-compose.yml and setting `TYPESENSE_ENABLED=false` in your .env file.
+
+### I'm getting errors about models being corrupt or failing to download. What do I do?
+
+You can delete the model cache volume, which is where models are downloaded. This will give the service a clean environment to download the model again.
 
 ### What happens to existing files after I choose a new [Storage Template](/docs/administration/storage-template.mdx)?
 
@@ -59,7 +67,7 @@ This is fixed by running the storage migration job.
 
 ### Why is object detection not very good?
 
-The model we used for machine learning is a prebuilt model, so the accuracy is not very good. It will hopefully be replaced with a better solution in the future.
+The default image tagging model is relatively small. You can change this for a larger model like `google/vit-base-patch16-224` by setting the model name under Settings > Machine Learning Settings > Image Tagging. You can then re-run the Image Tagging job to get improved tags.
 
 ### How can I see Immich logs?
 
@@ -96,6 +104,28 @@ docker-compose down -v
 
 After removing the containers and volumes, the **Files** can be cleaned up (if necessary) from the `UPLOAD_LOCATION` by simply deleting an unwanted files or folders.
 
-### Why iOS app shows duplicate photos on the timeline while the web doesn't?
+### How can I move all data (photos, persons, albums) from one user to another?
+
+This requires some database queries. You can do this on the command line (in the PostgreSQL container using the psql command), or you can add for example an [Adminer](https://www.adminer.org/) container to the `docker-compose.yml` file, so that you can use a web-interface.
+
+:::warning
+This is an advanced operation. If you can't to do it with the steps described here, this is not for you.
+:::
+
+1. **MAKE A BACKUP** - See [backup and restore](/docs/administration/backup-and-restore.md).
+2. Find the id of both the 'source' and the 'destination' user (it's the id column in the users table)
+3. Three tables need to be updated:
+
+   ```sql
+   // reassign albums
+   update albums set "ownerId" = '<destinationId>' where "ownerId" = '<sourceId>';
+
+   // reassign people
+   update person set "ownerId" = '<destinationId>' where "ownerId" = '<sourceId>';
+
+   // reassign assets
+   update assets set "ownerId" = '<destinationId>' where "ownerId" = '<sourceId>'
+    and checksum not in (select checksum from assets where "ownerId" = '<destinationId>');
+   ```
 
-If you are using `My Photo Stream`, the Photos app temporarily creates duplicates of photos taken in the last 30 days. These photos are included in the `Recents` album and thus shown up twice. To fix this, you can disable `My Photo Stream` in the native Photos app or choose a different album in the backup screen in Immich.
+4. There might be left-over assets in the 'source' user's library if they are skipped by the last query because of duplicate checksums. These are probably duplicates anyway, and can probably be removed.

+ 1 - 1
docs/docs/administration/reverse-proxy.md

@@ -19,7 +19,7 @@ Users can deploy a custom reverse proxy that forwards requests to Immich's rever
 
 ### Nginx example config
 
-Below is an example config for nginx:
+Below is an example config for nginx. Make sure to include `client_max_body_size 50000M;` also in a `http` block in `/etc/nginx/nginx.conf`.
 
 ```nginx
 server {

+ 39 - 6
docs/docs/administration/server-commands.md

@@ -16,20 +16,53 @@ To run a command, [connect](/docs/guides/docker-help.md#attach-to-a-container) t
 
 ## Examples
 
-Note that the commands below should begin with `immich-admin`.
-
 Reset Admin Password
 
-![Reset Admin Password](./img/reset-admin-password.png)
+```
+immich-admin reset-admin-password
+Found Admin:
+- ID=e65e6f88-2a30-4dbe-8dd9-1885f4889b53
+- OAuth ID=
+- Email=admin@example.com
+- Name=Immich Admin
+? Please choose a new password (optional) immich-is-cool
+The admin password has been updated.
+```
 
 Disable Password Login
 
-![Disable Password Login](./img/disable-password-login.png)
+```
+immich-admin disable-password-login
+Password login has been disabled.
+```
 
 Enabled Password Login
 
-![Enable Password Login](./img/enable-password-login.png)
+```
+immich-admin enable-password-login
+Password login has been enabled.
+```
 
 List Users
 
-![List Users](./img/list-users.png)
+```
+immich-admin list-users
+[
+  {
+    id: 'e65e6f88-2a30-4dbe-8dd9-1885f4889b53',
+    email: 'immich@example.com.com',
+    firstName: 'Immich',
+    lastName: 'Admin',
+    storageLabel: 'admin',
+    externalPath: null,
+    profileImagePath: 'upload/profile/e65e6f88-2a30-4dbe-8dd9-1885f4889b53/e65e6f88-2a30-4dbe-8dd9-1885f4889b53.jpg',
+    shouldChangePassword: true,
+    isAdmin: true,
+    createdAt: 2023-07-11T20:12:20.602Z,
+    deletedAt: null,
+    updatedAt: 2023-09-21T15:42:28.129Z,
+    oauthId: '',
+    memoriesEnabled: true
+  }
+]
+```

+ 6 - 0
docs/docs/developer/architecture.md

@@ -89,6 +89,12 @@ The machine learning service is written in [Python](https://www.python.org/) and
 
 All machine learning related operations have been externalized to this service, `immich-machine-learning`. Python is a natural choice for AI and machine learning. It also has some pretty specific hardware requirements. Running it as a separate container makes it possible to run the container on a separate machine, or easily disable it entirely.
 
+Each request to the machine learning service contains the relevant metadata for the model task, model name, and so on. These settings are stored in Postgres along with other system configs. For each request, the microservices container fetches these settings in order to attach them to the request.
+
+Internally, the machine learning service downloads, loads and configures the specified model for a given request before processing the text or image payload with it. Models that have been loaded are cached and reused across requests. A thread pool is used to process each request in a different thread so as not to block the async event loop.
+
+All models are in ONNX format. This format has wide industry support, meaning that most other model formats can be exported to it and many hardware APIs support it. It's also quite fast.
+
 Machine learning models are also quite _large_, requiring _quite a bit_ of memory. We are always looking for ways to improve and optimize this aspect of this container specifically.
 
 ### Postgres

+ 17 - 0
docs/docs/developer/testing.md

@@ -0,0 +1,17 @@
+# Testing
+
+## Server
+
+### Unit tests
+
+Unit are run by calling `npm run test` from the `server` directory.
+
+### End to end tests
+
+The backend has an end-to-end test suite that can be called with `npm run test:e2e` from the `server` directory. This will set up a dummy database inside a temporary container and run the tests against it. Setup and teardown is automatically taken care of. That test, however, can not set up all prerequisites to parse file formats, as that is very complex and error-prone. As such, this test excludes some test cases like HEIC file imports. The test suite will also print a friendly warning to remind you that not all tests are being run.
+
+Note that there is a bug in nodejs <20.8 that causes segmentation faults when running these tests. If you run into segfaults, ensure you are using at least version 20.8.
+
+To perform a full e2e test, you need to run e2e tests inside docker. The easiest way to do that is to run `make test-e2e` in the root directory. This will build and start a docker-compose consisting of the server, microservices, and a postgres database. It will then perfom the tests and exit.
+
+If you manually install the dependencies (see the DOCKERFILE) on your development machine, you can also run the full e2e tests manually by setting the `IMMICH_RUN_ALL_TESTS` environment value to true, i.e. `IMMICH_RUN_ALL_TESTS=true npm run test:e2e`.

+ 10 - 6
docs/docs/features/bulk-upload.md

@@ -56,8 +56,6 @@ The API key can be obtained in the user setting panel on the web interface.
 
 ---
 
-## Uploading existing libraries
-
 ### Run via Docker
 
 You can run the CLI inside of a docker container to avoid needing to install anything.
@@ -68,16 +66,16 @@ Be aware that as this runs inside a container, you need to mount the folder from
 
 ```bash title="Upload current directory"
 cd /DIRECTORY/WITH/IMAGES
-docker run -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
+docker run -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest upload --recursive --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
 ```
 
 ```bash title="Upload target directory"
-docker run -it --rm -v "/DIRECTORY/WITH/IMAGES:/import" ghcr.io/immich-app/immich-cli:latest upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
+docker run -it --rm -v "/DIRECTORY/WITH/IMAGES:/import" ghcr.io/immich-app/immich-cli:latest upload --recursive --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
 ```
 
 ```bash title="Create an alias"
 alias immich='docker run -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest'
-immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
+immich upload --recursive --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api
 ```
 
 :::tip Internal networking
@@ -88,7 +86,7 @@ If you are running the CLI container on the same machine as your Immich server,
 3. Use `--server http://immich-server:3001` for the upload command instead of the external address.
 
 ```bash title="Upload to internal address"
-docker run --network immich_default -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest upload --key HFEJ38DNSDUEG --server http://immich-server:3001
+docker run --network immich_default -it --rm -v "$(pwd):/import" ghcr.io/immich-app/immich-cli:latest upload --recursive --key HFEJ38DNSDUEG --server http://immich-server:3001
 ```
 
 :::
@@ -170,4 +168,10 @@ The proper command for above would be as shown below. You should have access to
 immich upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recursive /path/to/media --import
 ```
 
+If you are running the import using the docker command, please note that the volumes should point to the `/path/to/media` exactly on the environment the CLI command is being run on
+
+```
+docker run -it --rm -v "/path/to/media:/path/to/media" ghcr.io/immich-app/immich-cli:latest upload --key HFEJ38DNSDUEG --server http://192.168.1.216:2283/api --recursive /path/to/media --import
+```
+
 :::

+ 170 - 0
docs/docs/features/libraries.md

@@ -0,0 +1,170 @@
+# Libraries
+
+## Overview
+
+Immich supports the creation of libraries which is a top-level asset container. Currently, there are two types of libraries: traditional upload libraries that can sync with a mobile device, and external libraries, that keeps up to date with files on disk. Libraries are different from albums in that an asset can belong to multiple albums but only one library, and deleting a library deletes all assets contained within. As of August 2023, this is a new feature and libraries have a lot of potential for future development beyond what is documented here. This document attempts to describe the current state of libraries.
+
+## The Upload Library
+
+Immich comes preconfigured with an upload library for each user. All assets uploaded to Immich are added to this library. This library can be renamed, but not deleted. The upload library is the only library that can be synced with a mobile device. No items in an upload library is allowed to have the same sha1 hash as another item in the same library in order to prevent duplicates.
+
+## External Libraries
+
+External libraries tracks assets stored outside of immich, i.e. in the file system. Immich will only read data from the files, and will not modify them in any way. Therefore, the delete button is disabled for external assets. When the external library is scanned, immich will read the metadata from the file and create an asset in the library for each image or video file. These items will then be shown in the main timeline, and they will look and behave like any other asset, including viewing on the map, adding to albums, etc.
+
+If a file is modified outside of Immich, the changes will not be reflected in immich until the library is scanned again. There are different ways to scan a library depending on the use case:
+
+- Scan Library Files: This is the default scan method and also the quickest. It will scan all files in the library and add new files to the library. It will notice if any files are missing (see below) but not check existing assets
+- Scan All Library Files: Same as above, but will check each existing asset to see if the modification time has changed. If it has, the asset will be updated. Since it has to check each asset, this is slower than Scan Library Files.
+- Force Scan All Library Files: Same as above, but will read each asset from disk no matter the modification time. This is useful in some cases where an asset has been modified externally but the modification time has not changed. This is the slowest way to scan because it reads each asset from disk.
+
+:::caution
+
+Due to aggressive caching it can take some time for a refreshed asset to appear correctly in the web view. You need to clear the cache in your browser to see the changes. This is a known issue and will be fixed in a future release. In Chrome, you need to open the developer console with F12, then reload the page with F5, and finally right click on the reload button and select "Empty Cache and Hard Reload".
+
+:::
+
+In external libraries, the file path is used for duplicate detection. This means that if a file is moved to a different location, it will be added as a new asset. If the file is moved back to its original location, it will be added as a new asset. In contrast to upload libraries, two identical files can be uploaded if they are in different locations. This is a deliberate design choice to make Immich reflect the file system as closely as possible. Remember that duplication detection is only done within the same library, so if you have multiple external libraries, the same file can be added to multiple libraries.
+
+:::caution
+
+If you add assets from an external library to an album and then move the asset to another location within the library, the asset will be removed from the album upon rescan. This is because the asset is considered a new asset after the move. This is a known issue and will be fixed in a future release.
+
+:::
+
+### Deleted External Assets
+
+In all above scan methods, Immich will check if any files are missing. This can happen if files are deleted, or if they are on a storage location that is currently unavailable, like a network drive that is not mounted, or a USB drive that has been unplugged. In order to prevent accidental deletion of assets, Immich will not immediately delete an asset from the library if the file is missing. Instead, the asset will be internally marked as offline and will still be visible in the main timeline. If the file is moved back to its original location and the library is scanned again, the asset will be restored.
+
+Finally, files can be deleted from Immich via the `Remove Offline Files` job. Any assets marked as offline will then be removed from Immich. Run this job whenever files have been deleted from the file system and you want to remove them from Immich. Note that a library scan must be performed first to mark the assets as offline.
+
+### Import Paths
+
+External libraries use import paths to determine which files to scan. Each library can have multiple import paths so that files from different locations can be added to the same library. Import paths are scanned recursively, and if a file is in multiple import paths, it will only be added once. If the import paths are edited in a way that an external file is no longer in any import path, it will be removed from the library in the same way a deleted file would. If the file is moved back to an import path, it will be added again as if it was a new file.
+
+### Troubleshooting
+
+Sometimes, an external library will not scan correctly. This can happen if the immich_server or immich_microservices can't access the files. Here are some things to check:
+
+- Is the external path set correctly?
+- In the docker-compose file, are the volumes mounted correctly?
+- Are the volumes identical between the `server` and `microservices` container?
+- Are the import paths set correctly, and do they match the path set in docker-compose file?
+- Are the permissions set correctly?
+
+If all else fails, you can always start a shell inside the container and check if the path is accessible. For example, `docker exec -it immich_microservices /bin/bash` will start a bash shell. If your import path, for instance, is `/data/import/photos`, you can check if the files are accessible by running `ls /data/import/photos`. Also check the `immich_server` container in the same way.
+
+### Security Considerations
+
+:::caution
+
+Please read and understand this section before setting external paths, as there are important security considerations.
+
+:::
+
+For security purposes, each Immich user is disallowed to add external files by default. This is to prevent devastating [path traversal attacks](https://owasp.org/www-community/attacks/Path_Traversal). An admin can allow individual users to use external path feature via the `external path` setting found in the admin panel. Without the external path restriction, a user can add any image or video file on the Immich host filesystem to be imported into Immich, potentially allowing sensitive data to be accessed. If you are running Immich as root in your Docker setup (which is the default), all external file reads are done with root privileges. This is particularly dangerous if the Immich host is a shared server.
+
+With the `external path` set, a user is restricted to accessing external files to files or directories within that path. The Immich admin should still be careful not set the external path too generously. For example, `user1` wants to read their photos in to `/home/user1`. A lazy admin sets that user's external path to `/home/` since it "gets the job done". However, that user will then be able to read all photos in `/home/user2/private-photos`, too! Please set the external path as specific as possible. If multiple folders must be added, do this using the docker volume mount feature described below.
+
+### Exclusion Patterns and Scan Settings
+
+By default, all files in the import paths will be added to the library. If there are files that should not be added, exclusion patterns can be used to exclude them. Exclusion patterns are glob patterns are matched against the full file path. If a file matches an exclusion pattern, it will not be added to the library. Exclusion patterns can be added in the Scan Settings page for each library. Under the hood, Immich uses the [glob](https://www.npmjs.com/package/glob) package to match patterns, so please refer to [their documentation](https://github.com/isaacs/node-glob#glob-primer) to see what patterns are supported.
+
+Some basic examples:
+
+- `*.tif` will exclude all files with the extension `.tif`
+- `hidden.jpg` will exclude all files named `hidden.jpg`
+- `**/Raw/**` will exclude all files in any directory named `Raw`
+- `*.(tif,jpg)` will exclude all files with the extension `.tif` or `.jpg`
+
+### Nightly job
+
+There is an automatic job that's run once a day and refreshes all modified files in all libraries as well as cleans up any libraries stuck in deletion.
+
+## Usage
+
+Let's show a concrete example where we add an existing gallery to Immich. Here, we have the following folders we want to add:
+
+- `/home/user/old-pics`: a folder contining childhood photos.
+- `/mnt/nas/christmas-trip`: photos from a christmas trip. The subfolder `/mnt/nas/christmas-trip/Raw` contains the raw files directly from the DSLR. We don't want to import the raw files to Immich
+- `/mnt/media/videos`: Videos from the same christmas trip.
+
+First, we need to plan how we want to organize the libraries. The christmas trip photos should belong to its own library since we want to exclude the raw files. The videos and old photos can be in the same library since we want to import all files. We could also add all three folders to the same library if there are no files matching the Raw exclusion pattern in the other folders.
+
+### Mount Docker Volumes
+
+`immich-server` and `immich-microservices` containers will need access to the gallery. Modify your docker compose file as follows
+
+```diff title="docker-compose.yml"
+  immich-server:
+    volumes:
+      - ${UPLOAD_LOCATION}:/usr/src/app/upload
++     - /mnt/nas/christmas-trip:/mnt/media/christmas-trip:ro
++     - /home/user/old-pics:/mnt/media/old-pics:ro
++     - /mnt/media/videos:/mnt/media/videos:ro
+
+
+  immich-microservices:
+    volumes:
+      - ${UPLOAD_LOCATION}:/usr/src/app/upload
++     - /mnt/nas/christmas-trip:/mnt/media/christmas-trip:ro
++     - /home/user/old-pics:/mnt/media/old-pics:ro
++     - /mnt/media/videos:/mnt/media/videos:ro
+```
+
+:::tip
+The `ro` flag at the end only gives read-only access to the volumes. While Immich does not modify files, it's a good practice to mount read-only.
+:::
+
+_Remember to bring the container down/up to register the changes. Make sure you can see the mounted path in the container._
+
+### Set External Path
+
+Only an admin can do this.
+
+- Navigate to `Administration > Users` page on the web.
+- Click on the user edit button.
+- Set `/mnt/media` to be the external path. This folder will only contain the three folders that we want to import, so nothing else can be accessed.
+
+### Create External Libraries
+
+- Click on your user name in the top right corner -> Account Settings
+- Click on Libraries
+- Click on Create External Library
+- Click the drop-down menu on the newly created library
+- Click on Rename Library and rename it to "Christmas Trip"
+- Click Edit Import Paths
+- Click on Add Path
+- Enter `/mnt/media/christmas-trip` then click Add
+
+NOTE: We have to use the `/mnt/media/christmas-trip` path and not the `/mnt/nas/christmas-trip` path since all paths have to be what the Docker containers see.
+
+Next, we'll add an exclusion pattern to filter out raw files.
+
+- Click the drop-down menu on the newly christmas library
+- Click on Manage
+- Click on Scan Settings
+- Click on Add Exclusion Pattern
+- Enter `**/Raw/**` and click save.
+- Click save
+- Click the drop-down menu on the newly created library
+- Click on Scan Library Files
+
+The christmas trip library will now be scanned in the background. In the meantime, let's add the videos and old photos to another library.
+
+- Click on Create External Library.
+
+:::info Note
+If you get an error here, please rename the other external library to something else. This is a bug that will be fixed in a future release.
+:::
+
+- Click the drop-down menu on the newly created library
+- Click Edit Import Paths
+- Click on Add Path
+- Enter `/mnt/media/old-pics` then click Add
+- Click on Add Path
+- Enter `/mnt/media/videos` then click Add
+- Click Save
+- Click on Scan Library Files
+
+Within seconds, the assets from the old-pics and videos folders should show up in the main timeline.

+ 7 - 13
docs/docs/features/read-only-gallery.md

@@ -1,22 +1,16 @@
-# Read-only Gallery [Experimental]
-
-## Overview
-
-This feature enables users to use an existing gallery without uploading the assets to Immich.
-
-Upon syncing the file information, it will be read by Immich to generate supported files.
+# Read-only Gallery [Deprecated]
 
 :::caution
 
-This feature is still in an experimental stage. And this is an initial implementation and will receive improvements in the future.
+This feature is being deprecated in favor of [Libraries](/docs/features/libraries.md).
 
-The current limitations of this feature are:
+:::
 
-- Assets are not automatically synced and must instead be manually synced with the CLI tool.
-- Only new files that are added to the gallery will be detected.
-- Deleted and moved files will not be detected.
+## Overview
 
-:::
+This feature enables users to use an existing gallery without uploading the assets to Immich.
+
+Upon syncing the file information, it will be read by Immich to generate supported files.
 
 ## Usage
 

+ 85 - 0
docs/docs/guides/database-queries.md

@@ -0,0 +1,85 @@
+# Database Queries
+
+:::danger
+Keep in mind that mucking around in the database might set the moon on fire. Avoid modifying the database directly when possible, and always have current backups.
+:::
+
+:::tip
+Run `docker exec -it immich_postgres psql immich <DB_USERNAME>` to connect to the database via the container directly.
+
+(Replace `<DB_USERNAME>` wit the value from your [`.env` file](/docs/install/environment-variables#database)).
+:::
+
+## Assets
+
+:::note
+The `"originalFileName"` column is the name of the uploaded file _without_ the extension.
+:::
+
+```sql title="Find by original filename"
+SELECT * FROM "assets" WHERE "originalFileName" = 'PXL_20230903_232542848';
+SELECT * FROM "assets" WHERE "originalFileName" LIKE 'PXL_%'; -- all files starting with PXL_
+SELECT * FROM "assets" WHERE "originalFileName" LIKE '%_2023_%'; -- all files with _2023_ in the middle
+```
+
+```sql title="Find by path"
+SELECT * FROM "assets" WHERE "originalPath" = 'upload/library/admin/2023/2023-09-03/PXL_20230903_232542848.jpg';
+SELECT * FROM "assets" WHERE "originalPath" LIKE 'upload/library/admin/2023/%';
+```
+
+```sql title="Find by checksum" (sha1)
+SELECT encode("checksum", 'hex') FROM "assets";
+SELECT * FROM "assets" WHERE "checksum" = decode('69de19c87658c4c15d9cacb9967b8e033bf74dd1', 'hex');
+```
+
+```sql title="Live photos"
+SELECT * FROM "assets" where "livePhotoVideoId" IS NOT NULL;
+```
+
+```sql title="Without metadata"
+SELECT "assets".* FROM "exif"  LEFT JOIN "assets" ON "assets"."id" = "exif"."assetId" WHERE "exif"."assetId" IS NULL;
+```
+
+```sql title="Without thumbnails"
+SELECT * FROM "assets" WHERE "assets"."resizePath" IS NULL OR "assets"."webpPath" IS NULL;
+```
+
+```sql title="By type"
+SELECT * FROM "assets" WHERE "assets"."type" = 'VIDEO';
+SELECT * FROM "assets" WHERE "assets"."type" = 'IMAGE';
+```
+
+```sql title="Count by type"
+SELECT "assets"."type", count(*) FROM "assets" GROUP BY "assets"."type";
+```
+
+```sql title="Count by type (per user)"
+SELECT
+  "users"."email", "assets"."type", COUNT(*)
+FROM
+  "assets"
+JOIN
+  "users" ON "assets"."ownerId" = "users"."id"
+GROUP BY
+  "assets"."type", "users"."email"
+ORDER BY
+  "users"."email";
+```
+
+```sql title="Failed file movements"
+SELECT * FROM "move_history";
+```
+
+## Users
+
+```sql title="List"
+SELECT * FROM "users";
+```
+
+## System Config
+
+```sql title="Custom settings"
+SELECT "key", "value" FROM "system_config";
+```
+
+(Only used when not using the [config file](/docs/install/config-file))

+ 0 - 4
docs/docs/guides/docker-help.md

@@ -1,7 +1,3 @@
----
-sidebar_position: 1
----
-
 # Docker Help
 
 ## Containers

+ 26 - 0
docs/docs/guides/machine-learning.md

@@ -0,0 +1,26 @@
+# Remote Machine Learning
+
+To alleviate [performance issues on low-memory systems](/docs/FAQ.md#why-is-immich-slow-on-low-memory-systems-like-the-raspberry-pi) like the Raspberry Pi, you may also host Immich's machine-learning container on a more powerful system (e.g. your laptop or desktop computer):
+
+- Set `IMMICH_MACHINE_LEARNING_URL` to point to the designated ML system, e.g. `http://workstation:3003`.
+- Copy the following `docker-compose.yml` to your ML system.
+- Start the container by running `docker-compose up -d` or `docker compose up -d` (depending on your Docker version).
+
+```yaml
+version: '3.8'
+
+services:
+  immich-machine-learning:
+    container_name: immich_machine_learning
+    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
+    volumes:
+      - model-cache:/cache
+    restart: always
+    ports:
+      - 3003:3003
+
+volumes:
+  model-cache:
+```
+
+Please note that version mismatches between both hosts may cause instabilities and bugs, so make sure to always perform updates together.

+ 113 - 0
docs/docs/install/config-file.md

@@ -0,0 +1,113 @@
+# Config File
+
+A config file can be provided as an alternative to the UI configuration.
+
+### Step 1 - Create a new config file
+
+In JSON format, create a new config file (e.g. `immich.config`) and put it in a location that can be accessed by Immich.
+The default configuration looks like this:
+
+```json
+{
+  "ffmpeg": {
+    "crf": 23,
+    "threads": 0,
+    "preset": "ultrafast",
+    "targetVideoCodec": "h264",
+    "targetAudioCodec": "aac",
+    "targetResolution": "720",
+    "maxBitrate": "0",
+    "twoPass": false,
+    "transcode": "required",
+    "tonemap": "hable",
+    "accel": "disabled"
+  },
+  "job": {
+    "backgroundTask": {
+      "concurrency": 5
+    },
+    "clipEncoding": {
+      "concurrency": 2
+    },
+    "metadataExtraction": {
+      "concurrency": 5
+    },
+    "objectTagging": {
+      "concurrency": 2
+    },
+    "recognizeFaces": {
+      "concurrency": 2
+    },
+    "search": {
+      "concurrency": 5
+    },
+    "sidecar": {
+      "concurrency": 5
+    },
+    "storageTemplateMigration": {
+      "concurrency": 5
+    },
+    "thumbnailGeneration": {
+      "concurrency": 5
+    },
+    "videoConversion": {
+      "concurrency": 1
+    }
+  },
+  "machineLearning": {
+    "classification": {
+      "minScore": 0.7,
+      "enabled": true,
+      "modelName": "microsoft/resnet-50"
+    },
+    "enabled": true,
+    "url": "http://immich-machine-learning:3003",
+    "clip": {
+      "enabled": true,
+      "modelName": "ViT-B-32::openai"
+    },
+    "facialRecognition": {
+      "enabled": true,
+      "modelName": "buffalo_l",
+      "minScore": 0.7,
+      "maxDistance": 0.6,
+      "minFaces": 1
+    }
+  },
+  "oauth": {
+    "enabled": false,
+    "issuerUrl": "",
+    "clientId": "",
+    "clientSecret": "",
+    "mobileOverrideEnabled": false,
+    "mobileRedirectUri": "",
+    "scope": "openid email profile",
+    "storageLabelClaim": "preferred_username",
+    "buttonText": "Login with OAuth",
+    "autoRegister": true,
+    "autoLaunch": false
+  },
+  "passwordLogin": {
+    "enabled": true
+  },
+  "storageTemplate": {
+    "template": "{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}"
+  },
+  "thumbnail": {
+    "webpSize": 250,
+    "jpegSize": 1440,
+    "quality": 90,
+    "colorspace": "p3"
+  }
+}
+```
+
+:::tip
+In Administration > Settings is a button to copy the current configuration to your clipboard.
+So you can just grab it from there, paste it into a file and you're pretty much good to go.
+:::
+
+### Step 2 - Specify the file location
+
+In your `.env` file, set the variable `IMMICH_CONFIG_FILE` to the path of your config.
+For more information, refer to the [Environment Variables](https://docs.immich.app/docs/install/environment-variables) section.

+ 0 - 1
docs/docs/install/docker-compose.md

@@ -132,7 +132,6 @@ PUBLIC_LOGIN_PAGE_MESSAGE="My Family Photos and Videos Backup Server"
 
 IMMICH_WEB_URL=http://immich-web:3000
 IMMICH_SERVER_URL=http://immich-server:3001
-IMMICH_MACHINE_LEARNING_URL=http://immich-machine-learning:3003
 
 ####################################################################################
 # Alternative API's External Address - Optional

+ 53 - 30
docs/docs/install/environment-variables.md

@@ -1,5 +1,20 @@
+---
+sidebar_position: 90
+---
+
 # Environment Variables
 
+:::caution
+
+To change environment variables, you must recreate the Immich containers.
+Just restarting the containers does not replace the environment within the container!
+
+In order to recreate the container using docker compose, run `docker compose up -d`.
+In most cases docker will recognize that the `.env` file has changed and recreate the affected containers.
+If this should not work, try running `docker compose up -d --force-recreate`.
+
+:::
+
 ## Docker Compose
 
 | Variable          | Description           |  Default  | Services                                                       |
@@ -22,6 +37,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N
 | `LOG_LEVEL`                 | Log Level (verbose, debug, log, warn, error) |    `log`     | server, microservices                        |
 | `IMMICH_MEDIA_LOCATION`     | Media Location                               |  `./upload`  | server, microservices                        |
 | `PUBLIC_LOGIN_PAGE_MESSAGE` | Public Login Page Message                    |              | web                                          |
+| `IMMICH_CONFIG_FILE`        | Path to config file                          |              | server                                       |
 
 :::tip
 
@@ -33,30 +49,28 @@ These environment variables are used by the `docker-compose.yml` file and do **N
 
 ## Geocoding
 
-| Variable                           | Description                         |           Default            | Services      |
-| :--------------------------------- | :---------------------------------- | :--------------------------: | :------------ |
-| `DISABLE_REVERSE_GEOCODING`        | Disable Reverse Geocoding Precision |           `false`            | microservices |
-| `REVERSE_GEOCODING_PRECISION`      | Reverse Geocoding Precision         |             `3`              | microservices |
-| `REVERSE_GEOCODING_DUMP_DIRECTORY` | Reverse Geocoding Dump Directory    | `./.reverse-geocoding-dump/` | microservices |
+| Variable                           | Description                      |           Default            | Services      |
+| :--------------------------------- | :------------------------------- | :--------------------------: | :------------ |
+| `REVERSE_GEOCODING_DUMP_DIRECTORY` | Reverse Geocoding Dump Directory | `./.reverse-geocoding-dump/` | microservices |
 
 ## Ports
 
-| Variable                | Description           | Default | Services         |
-| :---------------------- | :-------------------- | :-----: | :--------------- |
-| `PORT`                  | Web Port              | `3000`  | web              |
-| `SERVER_PORT`           | Server Port           | `3001`  | server           |
-| `MICROSERVICES_PORT`    | Microservices Port    | `3002`  | microservices    |
-| `MACHINE_LEARNING_PORT` | Machine Learning Port | `3003`  | machine learning |
+| Variable                | Description           |  Default  | Services         |
+| :---------------------- | :-------------------- | :-------: | :--------------- |
+| `PORT`                  | Web Port              |  `3000`   | web              |
+| `SERVER_PORT`           | Server Port           |  `3001`   | server           |
+| `MICROSERVICES_PORT`    | Microservices Port    |  `3002`   | microservices    |
+| `MACHINE_LEARNING_HOST` | Machine Learning Host | `0.0.0.0` | machine learning |
+| `MACHINE_LEARNING_PORT` | Machine Learning Port |  `3003`   | machine learning |
 
 ## URLs
 
-| Variable                      | Description                                              |                Default                | Services              |
-| :---------------------------- | :------------------------------------------------------- | :-----------------------------------: | :-------------------- |
-| `IMMICH_WEB_URL`              | Immich Web URL                                           |       `http://immich-web:3000`        | proxy                 |
-| `IMMICH_SERVER_URL`           | Immich Server URL                                        |      `http://immich-server:3001`      | web, proxy            |
-| `IMMICH_MACHINE_LEARNING_URL` | Immich Machine Learning URL, set `"false"` to disable ML | `http://immich-machine-learning:3003` | server, microservices |
-| `PUBLIC_IMMICH_SERVER_URL`    | Public Immich URL                                        |      `http://immich-server:3001`      | web                   |
-| `IMMICH_API_URL_EXTERNAL`     | Immich API URL External                                  |                `/api`                 | web                   |
+| Variable                   | Description             |           Default           | Services   |
+| :------------------------- | :---------------------- | :-------------------------: | :--------- |
+| `IMMICH_WEB_URL`           | Immich Web URL          |  `http://immich-web:3000`   | proxy      |
+| `IMMICH_SERVER_URL`        | Immich Server URL       | `http://immich-server:3001` | web, proxy |
+| `PUBLIC_IMMICH_SERVER_URL` | Public Immich URL       | `http://immich-server:3001` | web        |
+| `IMMICH_API_URL_EXTERNAL`  | Immich API URL External |           `/api`            | web        |
 
 :::info
 
@@ -172,18 +186,27 @@ Typesense URL example JSON before encoding:
 
 ## Machine Learning
 
-| Variable                                    | Description                    |        Default        | Services         |
-| :------------------------------------------ | :----------------------------- | :-------------------: | :--------------- |
-| `MACHINE_LEARNING_MIN_FACE_SCORE`           | Minimum Face Score             |         `0.7`         | machine learning |
-| `MACHINE_LEARNING_MODEL_TTL`                | Model TTL                      |         `300`         | machine learning |
-| `MACHINE_LEARNING_EAGER_STARTUP`            | Eager Startup                  |        `true`         | machine learning |
-| `MACHINE_LEARNING_MIN_TAG_SCORE`            | Minimum Tag Score              |         `0.9`         | machine learning |
-| `MACHINE_LEARNING_FACIAL_RECOGNITION_MODEL` | Facial Recognition Model       |      `buffalo_l`      | machine learning |
-| `MACHINE_LEARNING_CLIP_TEXT_MODEL`          | Clip Text Model                |    `clip-ViT-B-32`    | machine learning |
-| `MACHINE_LEARNING_CLIP_IMAGE_MODEL`         | Clip Image Model               |    `clip-ViT-B-32`    | machine learning |
-| `MACHINE_LEARNING_CLASSIFICATION_MODEL`     | Classification Model           | `microsoft/resnet-50` | machine learning |
-| `MACHINE_LEARNING_CACHE_FOLDER`             | ML Cache Location              |       `/cache`        | machine learning |
-| `TRANSFORMERS_CACHE`                        | ML Transformers Cache Location |       `/cache`        | machine learning |
+| Variable                                         | Description                                                       |       Default       | Services         |
+| :----------------------------------------------- | :---------------------------------------------------------------- | :-----------------: | :--------------- |
+| `MACHINE_LEARNING_MODEL_TTL`<sup>\*1</sup>       | Inactivity time (s) before a model is unloaded (disabled if <= 0) |         `0`         | machine learning |
+| `MACHINE_LEARNING_CACHE_FOLDER`                  | Directory where models are downloaded                             |      `/cache`       | machine learning |
+| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*2</sup> | Thread count of the request thread pool (disabled if <= 0)        | number of CPU cores | machine learning |
+| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS`        | Number of parallel model operations                               |         `1`         | machine learning |
+| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS`        | Number of threads for each model operation                        |         `2`         | machine learning |
+| `MACHINE_LEARNING_WORKERS`<sup>\*3</sup>         | Number of worker processes to spawn                               |         `1`         | machine learning |
+| `MACHINE_LEARNING_WORKER_TIMEOUT`                | Maximum time (s) of unresponsiveness before a worker is killed    |        `120`        | machine learning |
+
+\*1: This is an experimental feature. It may result in increased memory use over time when loading models repeatedly.
+
+\*2: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.
+
+\*3: Since each process duplicates models in memory, changing this is not recommended unless you have abundant memory to go around.
+
+:::info
+
+Other machine learning parameters can be tuned from the admin UI.
+
+:::
 
 ## Docker Secrets
 

+ 1 - 1
docs/docs/install/post-install.mdx

@@ -1,5 +1,5 @@
 ---
-sidebar_position: 100
+sidebar_position: 80
 ---
 
 import RegisterAdminUser from '../partials/_register-admin.md';

+ 1 - 1
docs/docs/overview/help.md

@@ -11,6 +11,6 @@ Running into an issue or have a question? Try the following:
 3. Search through existing [GitHub Issues][github-issues].
 4. Open a help ticket on [Discord][discord-link].
 
-[github-issues]: https://github.com/immich-app/immich/releases
+[github-issues]: https://github.com/immich-app/immich/issues
 [github-releases]: https://github.com/immich-app/immich/releases
 [discord-link]: https://discord.com/invite/D8JsnBEuKb

+ 1 - 1
docs/docs/overview/introduction.mdx

@@ -4,7 +4,7 @@ sidebar_position: 1
 
 # Introduction
 
-<img src={require('./img/feature-panel.png').default} alt="Immich" />
+<img src={require('./img/feature-panel.png').default} alt="Immich - Self-hosted photos and videos backup tool" />
 
 ## Welcome!
 

+ 7 - 0
docs/docusaurus.config.js

@@ -89,6 +89,7 @@ const config = {
         },
       },
       navbar: {
+        title: 'IMMICH',
         logo: {
           alt: 'Immich University Logo',
           src: 'img/color-logo.png',
@@ -100,6 +101,11 @@ const config = {
             position: 'right',
             label: 'Docs',
           },
+          {
+            to: '/milestones',
+            position: 'right',
+            label: 'Milestones',
+          },
           {
             to: '/docs/api',
             position: 'right',
@@ -161,6 +167,7 @@ const config = {
       prism: {
         theme: lightCodeTheme,
         darkTheme: darkCodeTheme,
+        additionalLanguages: ['sql'],
       },
       image: 'overview/img/feature-panel.png',
     }),

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 345 - 488
docs/package-lock.json


+ 6 - 3
docs/package.json

@@ -6,7 +6,7 @@
     "docusaurus": "docusaurus",
     "format": "prettier --check .",
     "format:fix": "prettier --write .",
-    "start": "docusaurus start",
+    "start": "docusaurus start --port 3005",
     "build": "docusaurus build",
     "swizzle": "docusaurus swizzle",
     "deploy": "docusaurus deploy",
@@ -17,10 +17,13 @@
     "check": "tsc"
   },
   "dependencies": {
-    "@docusaurus/core": "^2.4.1",
-    "@docusaurus/preset-classic": "^2.4.1",
+    "@docusaurus/core": "^2.4.3",
+    "@docusaurus/preset-classic": "^2.4.3",
+    "@mdi/js": "^7.3.67",
+    "@mdi/react": "^1.6.1",
     "@mdx-js/react": "^1.6.22",
     "autoprefixer": "^10.4.13",
+    "classnames": "^2.3.2",
     "clsx": "^1.2.1",
     "docusaurus-lunr-search": "^2.3.2",
     "docusaurus-preset-openapi": "^0.6.3",

+ 86 - 0
docs/src/components/timeline.tsx

@@ -0,0 +1,86 @@
+import React from 'react';
+import Icon from '@mdi/react';
+import { mdiCheckboxMarkedCircleOutline } from '@mdi/js';
+import useIsBrowser from '@docusaurus/useIsBrowser';
+
+export interface Item {
+  icon: string;
+  title: string;
+  description?: string;
+  release: string;
+  tag?: string;
+  date: Date;
+}
+
+interface Props {
+  items: Item[];
+}
+
+export default function Timeline({ items }: Props): JSX.Element {
+  const isBrowser = useIsBrowser();
+
+  return (
+    <ul className="flex flex-col pl-4">
+      {items.map((item, index) => {
+        const isFirst = index === 0;
+        const isLast = index === items.length - 1;
+
+        const classNames: string[] = [];
+
+        if (isFirst) {
+          classNames.push('');
+        }
+
+        if (isLast) {
+          classNames.push('rounded rounded-b-full');
+        }
+
+        return (
+          <li key={index} className="flex min-h-24 w-[700px] max-w-[90vw]">
+            <div className="md:flex justify-start w-36 mr-8 items-center dark:text-immich-dark-primary text-immich-primary hidden">
+              {isBrowser ? item.date.toLocaleDateString(navigator.language) : ''}
+            </div>
+            <div className={`${isFirst && 'relative top-[50%]'} ${isLast && 'relative bottom-[50%]'}`}>
+              <div
+                className={`h-full border-solid border-4 border-immich-primary dark:border-immich-dark-primary ${
+                  isFirst && 'rounded rounded-t-full'
+                } ${isLast && 'rounded rounded-b-full'}`}
+              ></div>
+            </div>
+            <div className="z-10 flex items-center bg-immich-primary dark:bg-immich-dark-primary border-2 border-solid rounded-full dark:text-black text-white relative top-[50%] left-[-3px] translate-y-[-50%] translate-x-[-50%] w-8 h-8 shadow-lg ">
+              <Icon path={mdiCheckboxMarkedCircleOutline} size={1.25} />
+            </div>
+            <section className=" dark:bg-immich-dark-gray bg-immich-gray dark:border-0 border-gray-200 border border-solid rounded-2xl flex flex-col w-full gap-2 p-4 ml-4 my-2 hover:bg-immich-primary/10 dark:hover:bg-immich-dark-primary/10 transition-all">
+              <div className="m-0 text-lg flex w-full items-center justify-between gap-2">
+                <p className="m-0 items-start flex gap-2">
+                  <Icon path={item.icon} size={1} />
+                  <span>{item.title}</span>
+                </p>
+
+                <span className="dark:text-immich-dark-primary text-immich-primary">
+                  {item.tag ? (
+                    <a
+                      href={`https://github.com/immich-app/immich/releases/tag/${item.tag}`}
+                      target="_blank"
+                      rel="noopener"
+                    >
+                      [{item.release}]{' '}
+                    </a>
+                  ) : (
+                    <span>
+                      [{item.release} {isBrowser ? item.date.toLocaleDateString(navigator.language) : ''}]
+                    </span>
+                  )}
+                </span>
+              </div>
+              <div className="md:hidden text-xs">
+                Release Date - {isBrowser ? item.date.toLocaleDateString(navigator.language) : ''}
+              </div>
+              <p className="m-0 text-sm text-gray-600 dark:text-gray-300">{item.description}</p>
+            </section>
+          </li>
+        );
+      })}
+    </ul>
+  );
+}

+ 4 - 0
docs/src/css/custom.css

@@ -40,3 +40,7 @@ button {
   --ifm-background-color: #000000;
   --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
 }
+
+.navbar__brand .navbar__title {
+  @apply font-immich-title text-2xl font-normal text-immich-primary dark:text-immich-dark-primary;
+}

+ 12 - 9
docs/src/pages/index.tsx

@@ -6,28 +6,31 @@ function HomepageHeader() {
   return (
     <header>
       <section className="text-center m-6 p-12 border border-red-400 rounded-[50px] bg-gray-100 dark:bg-immich-dark-gray">
-        <h1 className="md:text-6xl font-bold mb-10 font-immich-title text-immich-primary dark:text-immich-dark-primary">
-          IMMICH
+        <img src="img/immich-logo.svg" className="md:h-24 h-12 mb-2" alt="Immich logo" />
+        <h1 className="md:text-6xl font-immich-title mb-10 text-immich-primary dark:text-immich-dark-primary uppercase">
+          Immich
         </h1>
         <div className="font-thin sm:text-base md:text-2xl my-12 sm:leading-tight">
-          <p>SELF-HOSTED BACKUP SOLUTION </p>
-          <p>FOR PHOTOS AND VIDEOS</p>
-          <p>ON MOBILE DEVICE</p>
+          <p className="mb-1 uppercase">
+            Self-hosted backup solution <span className="block"></span>
+            for photos and videos <span className="block"></span>
+            on mobile device
+          </p>
         </div>
 
         <div className="flex flex-col sm:flex-row place-items-center place-content-center mt-9 mb-16 gap-4 ">
           <Link
-            className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary dark:bg-immich-dark-primary rounded-full no-underline hover:no-underline text-white hover:text-gray-50 dark:text-immich-dark-bg font-bold"
+            className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary dark:bg-immich-dark-primary rounded-full no-underline hover:no-underline text-white hover:text-gray-50 dark:text-immich-dark-bg font-bold uppercase"
             to="docs/overview/introduction"
           >
-            GET STARTED
+            Get started
           </Link>
 
           <Link
-            className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary/10 dark:bg-gray-300  rounded-full hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold"
+            className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary/10 dark:bg-gray-300  rounded-full hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
             to="https://demo.immich.app/"
           >
-            DEMO PORTAL
+            Demo portal
           </Link>
         </div>
 

+ 0 - 7
docs/src/pages/markdown-page.md

@@ -1,7 +0,0 @@
----
-title: Markdown page example
----
-
-# Markdown page example
-
-You don't need React to write simple standalone pages.

+ 509 - 0
docs/src/pages/milestones.tsx

@@ -0,0 +1,509 @@
+import {
+  mdiAccountGroup,
+  mdiAndroid,
+  mdiAppleIos,
+  mdiArchiveOutline,
+  mdiBookSearchOutline,
+  mdiCheckAll,
+  mdiCheckboxMarked,
+  mdiCollage,
+  mdiDevices,
+  mdiFaceMan,
+  mdiFaceManOutline,
+  mdiFile,
+  mdiFileSearch,
+  mdiFolder,
+  mdiHeart,
+  mdiImage,
+  mdiImageAlbum,
+  mdiImageMultipleOutline,
+  mdiImageSearch,
+  mdiKeyboardSettingsOutline,
+  mdiMagnify,
+  mdiMap,
+  mdiMaterialDesign,
+  mdiMerge,
+  mdiMonitor,
+  mdiMotionPlayOutline,
+  mdiPanVertical,
+  mdiPartyPopper,
+  mdiRaw,
+  mdiRotate360,
+  mdiSecurity,
+  mdiServer,
+  mdiShareAll,
+  mdiShareCircle,
+  mdiStar,
+  mdiTag,
+  mdiText,
+  mdiThemeLightDark,
+  mdiTrashCanOutline,
+  mdiVideo,
+  mdiWeb,
+} from '@mdi/js';
+import Layout from '@theme/Layout';
+import React from 'react';
+import Timeline, { Item } from '../components/timeline';
+
+const items: Item[] = [
+  {
+    icon: mdiTrashCanOutline,
+    title: 'Trash Feature',
+    description: 'Trash, restore from trash, and automatically empty the recycle bin after 30 days.',
+    release: 'v1.82.0',
+    tag: 'v1.82.0',
+    date: new Date(2023, 9, 17),
+  },
+  {
+    icon: mdiBookSearchOutline,
+    title: 'External Libraries',
+    description: 'Automatically import media into Immich based on imports paths and ignore patterns.',
+    release: 'v1.79.0',
+    tag: 'v1.79.0',
+    date: new Date(2023, 8, 21),
+  },
+  {
+    icon: mdiMap,
+    title: 'Map View (Mobile)',
+    description: 'Heat map implementation in the mobile app.',
+    release: 'v1.76.0',
+    tag: 'v1.76.0',
+    date: new Date(2023, 7, 29),
+  },
+  {
+    icon: mdiFile,
+    title: 'Configuration File',
+    description: 'Auto-configure an Immich installation via a configuration file.',
+    release: 'v1.75.0',
+    tag: 'v1.75.0',
+    date: new Date(2023, 7, 26),
+  },
+  {
+    icon: mdiMonitor,
+    title: 'Slideshow Mode (Web)',
+    description: 'Start a full-screen slideshow from an Album on the web.',
+    release: 'v1.75.0',
+    tag: 'v1.75.0',
+    date: new Date(2023, 7, 26),
+  },
+  {
+    icon: mdiServer,
+    title: 'Hardware Transcoding',
+    description: 'Support hardware acceleration (QuickSync, VAAPI, and Nvidia) for video transcoding.',
+    release: 'v1.72.0',
+    tag: 'v1.72.0',
+    date: new Date(2023, 7, 6),
+  },
+  {
+    icon: mdiImageAlbum,
+    title: 'View Albums via Time Buckets',
+    description: 'Upgrade albums to use time buckets, an optimized virtual viewport.',
+    release: 'v1.72.0',
+    tag: 'v1.72.0',
+    date: new Date(2023, 7, 6),
+  },
+  {
+    icon: mdiImageAlbum,
+    title: 'Album Description',
+    description: 'Save an album description.',
+    release: 'v1.72.0',
+    tag: 'v1.72.0',
+    date: new Date(2023, 7, 6),
+  },
+  {
+    icon: mdiRotate360,
+    title: '360° Photos (Web)',
+    description: 'View 360° Photos on the web.',
+    release: 'v1.71.0',
+    tag: 'v1.71.0',
+    date: new Date(2023, 6, 29),
+  },
+  {
+    icon: mdiMotionPlayOutline,
+    title: 'Android Motion Photos',
+    description: 'Add support for Android Motion Photos.',
+    release: 'v1.69.0',
+    tag: 'v1.69.0',
+    date: new Date(2023, 6, 23),
+  },
+  {
+    icon: mdiFaceManOutline,
+    title: 'Show/Hide Faces',
+    description: 'Add the options to show or hide faces.',
+    release: 'v1.68.0',
+    tag: 'v1.68.0',
+    date: new Date(2023, 6, 20),
+  },
+  {
+    icon: mdiMerge,
+    title: 'Merge Faces',
+    description: 'Add the ability to merge multiple faces together.',
+    release: 'v1.67.0',
+    tag: 'v1.67.0',
+    date: new Date(2023, 6, 14),
+  },
+  {
+    icon: mdiImage,
+    title: 'Feature Photo',
+    description: 'Add the option to change the feature photo for a person.',
+    release: 'v1.66.0',
+    tag: 'v1.66.0',
+    date: new Date(2023, 6, 4),
+  },
+  {
+    icon: mdiKeyboardSettingsOutline,
+    title: 'Multi-Select via SHIFT',
+    description: 'Add the option to multi-select while holding SHIFT.',
+    release: 'v1.66.0',
+    tag: 'v1.66.0',
+    date: new Date(2023, 6, 4),
+  },
+  {
+    icon: mdiImageMultipleOutline,
+    title: 'Memories (Mobile)',
+    description: 'View "On this day..." memories in the mobile app.',
+    release: 'v1.65.0',
+    tag: 'v1.65.0',
+    date: new Date(2023, 5, 30),
+  },
+  {
+    icon: mdiFaceMan,
+    title: 'Facial Recognition (Mobile)',
+    description: 'View detected faces in the mobile app.',
+    release: 'v1.63.0',
+    tag: 'v1.63.0',
+    date: new Date(2023, 5, 24),
+  },
+  {
+    icon: mdiImageMultipleOutline,
+    title: 'Memories (Web)',
+    description: 'View pictures taken in past years on this day on the web.',
+    release: 'v1.61.0',
+    tag: 'v1.61.0',
+    date: new Date(2023, 5, 16),
+  },
+  {
+    icon: mdiCollage,
+    title: 'Justified Layout (Web)',
+    description: 'Implement justified layout (collage) on the web.',
+    release: 'v1.61.0',
+    tag: 'v1.61.0',
+    date: new Date(2023, 5, 16),
+  },
+  {
+    icon: mdiRaw,
+    title: 'RAW File Formats',
+    description: 'Support for RAW file formats.',
+    release: 'v1.61.0',
+    tag: 'v1.61.0',
+    date: new Date(2023, 5, 16),
+  },
+  {
+    icon: mdiShareAll,
+    title: 'Partner Sharing (Mobile)',
+    description: 'View shared partner photos in the mobile app.',
+    release: 'v1.58.0',
+    tag: 'v1.58.0',
+    date: new Date(2023, 4, 28),
+  },
+  {
+    icon: mdiFile,
+    title: 'XMP Sidecar',
+    description: 'Attach XMP Sidecar files to assets.',
+    release: 'v1.58.0',
+    tag: 'v1.58.0',
+    date: new Date(2023, 4, 28),
+  },
+  {
+    icon: mdiFolder,
+    title: 'Custom Storage Label',
+    description: 'Replace the user UUID in the storage template with a custom label.',
+    release: 'v1.57.0',
+    tag: 'v1.57.0',
+    date: new Date(2023, 4, 23),
+  },
+  {
+    icon: mdiShareCircle,
+    title: 'Partner Sharing',
+    description: 'Share your entire collection with another user.',
+    release: 'v1.56.0',
+    tag: 'v1.56.0',
+    date: new Date(2023, 4, 18),
+  },
+  {
+    icon: mdiFaceMan,
+    title: 'Facial Recognition',
+    description: 'Detect faces in pictures and cluster them together as people, which can be named.',
+    release: 'v1.56.0',
+    tag: 'v1.56.0',
+    date: new Date(2023, 4, 18),
+  },
+  {
+    icon: mdiMap,
+    title: 'Map View (Web)',
+    description: 'View a global map, with clusters of photos based on corresponding GPS data.',
+    release: 'v1.55.0',
+    tag: 'v1.55.0',
+    date: new Date(2023, 4, 9),
+  },
+  {
+    icon: mdiDevices,
+    title: 'Manage Auth Devices',
+    description: 'Manage logged-in devices and revoke access from User Settings.',
+    release: 'v1.55.0',
+    tag: 'v1.55.0',
+    date: new Date(2023, 4, 9),
+  },
+  {
+    icon: mdiStar,
+    description: 'Reach 10K Starts on GitHub!',
+    title: '10,000 Stars',
+    release: 'v1.54.0',
+    tag: 'v1.54.0',
+    date: new Date(2023, 3, 18),
+  },
+  {
+    icon: mdiText,
+    title: 'Asset Descriptions',
+    description: 'Save an asset description',
+    release: 'v1.54.0',
+    tag: 'v1.54.0',
+    date: new Date(2023, 3, 18),
+  },
+  {
+    icon: mdiArchiveOutline,
+    title: 'Archiving',
+    description: 'Remove assets from the main timeline by archiving them.',
+    release: 'v1.54.0',
+    tag: 'v1.54.0',
+    date: new Date(2023, 3, 18),
+  },
+  {
+    icon: mdiDevices,
+    title: 'Responsive Web App',
+    description: 'Optimize the web app for small screen.',
+    release: 'v1.54.0',
+    tag: 'v1.54.0',
+    date: new Date(2023, 3, 18),
+  },
+  {
+    icon: mdiFileSearch,
+    title: 'Search By Metadata',
+    description: 'Search images by filename, description, tagged people, make, model, and other metadata.',
+    release: 'v1.52.0',
+    tag: 'v1.52.0',
+    date: new Date(2023, 2, 29),
+  },
+  {
+    icon: mdiImageSearch,
+    title: 'CLIP Search',
+    description: 'Search images with free-form text like "Sunset at the beach".',
+    release: 'v1.51.0',
+    tag: 'v1.51.0',
+    date: new Date(2023, 2, 20),
+  },
+  {
+    icon: mdiMagnify,
+    title: 'Explore Page',
+    description: 'View tagged places, object, and people.',
+    release: 'v1.51.0',
+    tag: 'v1.51.0',
+    date: new Date(2023, 2, 20),
+  },
+  {
+    icon: mdiAppleIos,
+    title: 'iOS Background Uploads',
+    description: 'Automatically backup pictures in the background on iOS.',
+    release: 'v1.48.0',
+    tag: 'v1.48.0',
+    date: new Date(2023, 1, 21),
+  },
+  {
+    icon: mdiMotionPlayOutline,
+    title: 'Auto-Link Live Photos',
+    description: 'Automatically link live photos, even when uploaded as separate files.',
+    release: 'v1.48.0',
+    tag: 'v1.48.0',
+    date: new Date(2023, 2, 21),
+  },
+  {
+    icon: mdiMaterialDesign,
+    title: 'Material Design 3 (Mobile)',
+    description: 'Upgrade the mobile app to Material Design 3.',
+    release: 'v1.47.0',
+    tag: 'v1.47.0',
+    date: new Date(2023, 1, 13),
+  },
+  {
+    icon: mdiHeart,
+    title: 'Favorites (Mobile)',
+    description: 'Show favorites on the mobile app.',
+    release: 'v1.46.0',
+    tag: 'v1.46.0',
+    date: new Date(2023, 1, 9),
+  },
+  {
+    icon: mdiPartyPopper,
+    title: 'Immich Turns 1',
+    description: 'Immich is officially one year old.',
+    release: 'v1.43.0',
+    tag: 'v1.43.0',
+    date: new Date(2023, 0, 27),
+  },
+  {
+    icon: mdiHeart,
+    title: 'Favorites Page (Web)',
+    description: 'Favorite and view favorites on the web.',
+    release: 'v1.43.0',
+    tag: 'v1.43.0',
+    date: new Date(2023, 0, 27),
+  },
+  {
+    icon: mdiShareCircle,
+    title: 'Public Share Links',
+    description: 'Share photos and albums publicly via a shared link.',
+    release: 'v1.41.0',
+    tag: 'v1.41.1_64-dev',
+    date: new Date(2023, 0, 10),
+  },
+  {
+    icon: mdiFolder,
+    title: 'User-Defined Storage Structure',
+    description: 'Support custom storage structures.',
+    release: 'v1.39.0',
+    tag: 'v1.39.0_61-dev',
+    date: new Date(2022, 11, 19),
+  },
+  {
+    icon: mdiMotionPlayOutline,
+    title: 'iOS Live Photos',
+    description: 'Backup and display iOS Live Photos.',
+    release: 'v1.36.0',
+    tag: 'v1.36.0_55-dev',
+    date: new Date(2022, 10, 20),
+  },
+  {
+    icon: mdiSecurity,
+    title: 'OAuth Integration',
+    description: 'Support OAuth2 and OIDC capable identity providers.',
+    release: 'v1.36.0',
+    tag: 'v1.36.0_55-dev',
+    date: new Date(2022, 10, 20),
+  },
+  {
+    icon: mdiWeb,
+    title: 'Documentation Site',
+    description: 'Release an official documentation website.',
+    release: 'v1.33.1',
+    tag: 'v1.33.0_52-dev',
+    date: new Date(2022, 9, 26),
+  },
+  {
+    icon: mdiThemeLightDark,
+    title: 'Dark Mode (Web)',
+    description: 'Dark mode on the web.',
+    release: 'v1.32.0',
+    tag: ' v1.32.0_50-dev',
+    date: new Date(2022, 9, 14),
+  },
+  {
+    icon: mdiPanVertical,
+    title: 'Virtual Scrollbar (Web)',
+    description: 'View the main timeline with a virtual scrollbar, allowing to jump to any point in time, instantly.',
+    release: 'v1.27.0',
+    tag: 'v1.27.0_37-dev',
+    date: new Date(2022, 8, 6),
+  },
+  {
+    icon: mdiCheckAll,
+    title: 'Checksum Duplication Check',
+    description: 'Enforce per user sha1 checksum uniqueness.',
+    release: 'v1.27.0',
+    tag: 'v1.27.0_37-dev',
+    date: new Date(2022, 8, 6),
+  },
+  {
+    icon: mdiAndroid,
+    title: 'Android Background Backup',
+    description: 'Automatic backup in the background on Android.',
+    release: 'v1.24.0',
+    tag: 'v1.24.0_34-dev',
+    date: new Date(2022, 7, 19),
+  },
+  {
+    icon: mdiAccountGroup,
+    title: 'Admin Portal',
+    description: 'Manage users and admin settings from the web.',
+    release: 'v1.10.0',
+    tag: 'v1.10.0_15-dev',
+    date: new Date(2022, 4, 29),
+  },
+  {
+    icon: mdiShareCircle,
+    title: 'Album Sharing',
+    description: 'Share albums with other users.',
+    release: 'v1.7.0',
+    tag: 'v1.7.0_11-dev ',
+    date: new Date(2022, 3, 24),
+  },
+  {
+    icon: mdiTag,
+    title: 'Image Tagging',
+    description: 'Tag images with custom values.',
+    release: 'v1.7.0',
+    tag: 'v1.7.0_11-dev ',
+    date: new Date(2022, 3, 24),
+  },
+  {
+    icon: mdiImage,
+    title: 'View Exif',
+    description: 'View metadata about assets.',
+    release: 'v1.3.0',
+    tag: 'V1.3.0-dev ',
+    date: new Date(2022, 2, 22),
+  },
+  {
+    icon: mdiCheckboxMarked,
+    title: 'Multi Select',
+    description: 'Select and execute actions on multiple assets at the same time.',
+    release: 'v1.2.0',
+    tag: 'V0.2-dev ',
+    date: new Date(2022, 1, 8),
+  },
+  {
+    icon: mdiVideo,
+    title: 'Video Player',
+    description: 'Play videos in the web and on mobile.',
+    release: 'v1.2.0',
+    tag: 'v0.2-dev ',
+    date: new Date(2022, 1, 8),
+  },
+  {
+    icon: mdiPartyPopper,
+    title: 'First Commit',
+    description: 'First commit on GitHub, Immich is born.',
+    release: 'v1.0.0',
+    date: new Date(2022, 2, 3),
+  },
+];
+
+export default function MilestonePage(): JSX.Element {
+  return (
+    <Layout title="Milestones" description="History of Immich">
+      <section className="my-8">
+        <h1 className="md:text-6xl text-center mb-10 text-immich-primary dark:text-immich-dark-primary">
+          Major Milestones
+        </h1>
+        <p className="text-center text-xl">
+          A list of project achievements and milestones, <br />
+          by release date.
+        </p>
+        <div className="flex row justify-around mt-8">
+          <div className="flex max-w-full ">
+            <Timeline items={items} />
+          </div>
+        </div>
+      </section>
+    </Layout>
+  );
+}

+ 98 - 0
docs/static/img/immich-logo.svg

@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="svg2781" xmlns:svg="http://www.w3.org/2000/svg"
+	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 564.2 553.5"
+	 style="enable-background:new 0 0 564.2 553.5;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:#4081EF;stroke:#512D8C;stroke-miterlimit:10;}
+	.st1{fill:#31A452;stroke:#512D8C;stroke-miterlimit:10;}
+	.st2{fill:#DE7FB3;stroke:#512D8C;stroke-miterlimit:10;}
+	.st3{fill:#FFB800;stroke:#512D8C;stroke-miterlimit:10;}
+	.st4{fill:#E64132;stroke:#512D8C;stroke-miterlimit:10;}
+	.st5{fill:#F2F5FB;stroke:#512D8C;stroke-miterlimit:10;}
+</style>
+<path class="st0" d="M210.5,549.6c-2.2-0.2-5.5-1-9.7-2.2c-52.4-15.7-99-46.5-133.8-88.5c-8.8-10.7-17.2-22.4-19.4-27.5
+	c-8.1-18.1-6.3-38.7,4.8-55.4c5-7.5,13.2-15,20.5-18.7c1.2-0.6,54.1-20,55.8-20.4c0.5-0.1,0.5,0.2-0.3,2.1c-0.7,1.7-1,3.1-1.1,5.5
+	l-0.1,3.2l2.8,5.8c8.7,17.9,19.2,32.7,33.2,46.4c6.3,6.2,7.8,7.6,13.8,12.3c22.7,18.1,52,30.7,79.9,34.3c2.5,0.3,5,0.8,5.7,1
+	c2.8,0.9,7.7-0.8,11-3.7l1.8-1.6l-0.2,4.8c-0.1,2.7-0.6,15.4-1,28.3c-0.6,20.3-0.8,24-1.5,27.5c-3.9,20.7-18.6,37.5-38.4,44.1
+	c-4.6,1.5-8,2.2-13.1,2.7C216.6,550.1,215.3,550,210.5,549.6z"/>
+<path class="st1" d="M339.8,549.4c-4-0.4-9.4-1.6-13.2-2.9c-3.4-1.2-10-4.4-12.5-6.1c-10.9-7.4-19-17.9-23.1-30
+	c-2.2-6.7-2.3-7.5-3.3-36.9c-0.5-14.9-0.9-27.9-0.9-28.9l0-1.9l2.3,1.8c2.6,2,6.6,3.4,8.5,3.1c0.6-0.1,3-0.5,5.3-0.8
+	c37.7-5.3,71.2-22.2,97.4-49.1c12.2-12.5,21.4-25.5,29.9-42.4l3.5-7l0-3.6c0-3.1-0.1-3.8-1-5.7c-0.5-1.2-0.9-2.1-0.9-2.2
+	c0.2-0.2,55.3,20.1,56.9,20.9c2.6,1.3,6.6,4.1,9.9,7c9.2,7.7,16.1,19.4,18.8,31.8c0.7,3.1,0.8,4.8,0.8,11.3c0,8.6-0.5,11.7-2.9,18.7
+	c-1.7,5-2.9,7.2-7.1,13.1c-7.6,11-15.3,20.5-25.2,31.2c-32.8,35.4-76.5,62.5-123.4,76.3C351.6,549.6,347.2,550.1,339.8,549.4z"/>
+<path class="st2" d="M255.6,438c-25.9-4.2-50.7-14.9-71.7-31c-5.2-4-8.7-7.1-14.1-12.4c-12.7-12.5-21.9-24.9-30.5-41.4
+	c-2.3-4.4-2.4-4.7-2.4-7.1c0-8.8,8.5-15.2,16.9-12.7c5.6,1.7,9.6,6.8,9.7,12.2c0,2.6-0.8,4.6-2.6,6.2c-1.2,1.1-3.2,1.9-4.6,1.9
+	c-1.2,0-3.3-0.8-4.3-1.6c-2.1-1.8-2-1,0.4,3.2c19.3,33.8,52.3,59.1,90,69.1c5.7,1.5,11.5,2.7,11.8,2.4c0.1-0.1-0.4-0.8-1.3-1.6
+	c-5.1-4.5-2.3-11.7,5-12.8c5.4-0.8,11.4,2.7,13.9,8c0.8,1.7,1,2.5,1,5.3s-0.1,3.5-1,5.3c-2,4.3-6.8,7.9-10.3,7.8
+	C260.6,438.7,257.9,438.3,255.6,438z"/>
+<path class="st0" d="M297.6,438.2c-3.4-1.3-6.4-4.3-7.8-8.1c-1.1-2.9-0.9-7.3,0.5-10.2c2.6-5.3,8.7-8.5,14.4-7.5
+	c2.9,0.5,4.7,1.9,6,4.3c0.8,1.6,1,2.2,0.8,3.6c-0.3,2.2-0.9,3.3-2.7,4.8c-0.8,0.7-1.4,1.4-1.3,1.5c0.5,0.5,13.4-2.7,21.3-5.4
+	c33.6-11.3,62.5-35.1,80.4-66.1c2.5-4.4,2.6-5,0.5-3.2c-2.8,2.4-7,1.9-9.6-1c-4-4.6-0.7-13.8,6.1-16.9c2-0.9,2.7-1,5.5-1
+	c2.9,0,3.5,0.1,5.6,1.1c4.4,2.1,7.4,6.4,7.8,11c0.2,2.2,0.1,2.3-2.2,6.9c-23,45.9-67,78.1-117.2,85.9
+	C300.2,438.8,299.4,438.9,297.6,438.2z"/>
+<path class="st1" d="M211.1,398.5c-4.7-0.9-8.7-2.7-12.9-5.9c-10.8-8.1-13.5-22.3-6.6-33.7c0.7-1.2,1.1-2.2,1-2.4
+	c-0.2-0.2-1.2-0.6-2.3-1.1c-7.6-3-13-10.6-13.5-19.1c-0.5-7.4,3.1-15,9-19.4c1-0.7,2.2-1.5,2.6-1.8c0.8-0.4,68.9-22.7,69.4-22.7
+	c0.2,0,0.7,0.7,1.2,1.5c0.5,0.8,1.6,2.3,2.4,3.3c1.2,1.4,1.5,1.9,1.2,2.3c-0.2,0.3-6.9,9.5-14.8,20.5
+	c-15.9,21.9-15.5,21.3-13.4,23.4c1.3,1.3,2.9,1.4,4.4,0.3c0.6-0.4,7.5-9.7,15.5-20.7c11.2-15.4,14.6-19.9,15-19.7
+	c0.9,0.4,5.5,1.9,6.6,2.1l1,0.2l0,35.3c0,39.7,0,38.8-2.5,44c-2.6,5.3-7.2,9.3-12.7,11.2c-3.7,1.3-6.8,1.6-10.2,1
+	c-5.5-0.9-9.8-3.2-13.7-7.4l-2.2-2.4l-0.6,0.9c-3,4.3-8.6,8.1-14,9.5C218.2,398.6,213.2,398.9,211.1,398.5z"/>
+<path class="st3" d="M342.9,398.5c-5.5-0.9-9.9-3.2-14.3-7.6l-3.2-3.2l-0.7,1c-2.3,3.3-6.8,6.5-11.1,7.9c-3.7,1.2-9.2,1.4-12.6,0.3
+	c-7.1-2.1-12.7-7.4-15.2-14.3l-0.9-2.6v-37.1v-37.1l1.8-0.4c1-0.2,2.7-0.8,3.9-1.2c1.1-0.5,2.1-0.8,2.2-0.7c0.1,0.1,6.5,9,14.4,19.9
+	c7.8,10.9,14.7,20.1,15.2,20.5c2.2,1.9,5.4,0.4,5.4-2.6c0-1.4-1-2.9-13.8-20.5c-7.6-10.5-14.2-19.6-14.7-20.4l-0.9-1.3l1.4-1.7
+	c0.8-0.9,1.9-2.5,2.5-3.4l1-1.6l34.4,11.2c18.9,6.2,35.1,11.6,35.9,12.1c6.8,4,11.1,11.3,11.1,19.1c0,4.1-0.5,6.4-2.4,10.2
+	c-2,4.1-5.5,7.6-9.6,9.7c-1.6,0.8-3.2,1.5-3.4,1.5c-1,0-0.9,0.7,0.3,2.6c2.8,4.3,4,8.5,3.9,13.7c0,8.1-3.7,15.2-10.6,20.3
+	C356.4,397.6,349.5,399.5,342.9,398.5z"/>
+<path class="st2" d="M53.9,341.9c-0.5-0.1-2.3-0.4-3.9-0.7c-15.6-2.6-30.4-12.6-38.8-26.2c-3.5-5.7-6.4-13.2-7.8-19.9
+	c-1.2-6.1-0.8-28.1,0.8-43.1c4.5-43,19-84.3,42.2-120.7c6.5-10.2,14.9-21.5,18.2-24.6c17.8-16.6,43.1-20.5,64.8-10
+	c4.3,2.1,8.8,5.1,12.7,8.6c2.8,2.4,5.8,6.1,20.9,25.5c9.7,12.5,17.8,22.8,17.9,23c0.2,0.2-0.9,0.4-3.2,0.4c-2.5,0-4.1,0.2-5.7,0.7
+	c-2.1,0.7-2.6,1.1-7.9,6.3c-8.2,8.1-14.4,15.3-20.3,23.9c-15.5,22.2-25.4,47.7-28.8,74.8c-2.2,16.9-1.6,37.5,1.6,52.3
+	c0.3,1.4,0.5,2.8,0.4,3c-0.1,0.2,0.2,1.3,0.8,2.4c1.1,2.4,4.3,5.7,6.5,6.8l1.5,0.8l-1.2,0.4c-0.7,0.2-13.1,3.8-27.6,8
+	c-16.4,4.7-27.7,7.8-29.8,8.1C64.1,342.1,56.1,342.3,53.9,341.9z"/>
+<path class="st3" d="M494.7,341.7c-2.1-0.3-33.8-9.1-56.5-15.8l-2.5-0.7l1.6-0.8c3.4-1.7,7.2-6.6,7.3-9.6c0-0.7,0.4-3.3,0.8-5.8
+	c3.9-22.7,3.1-46.1-2.5-68.4c-6.4-25.5-18.6-49.2-35.8-69.1c-4.6-5.3-14.8-15.4-16.4-16.1c-2.4-1.1-5.1-1.6-8-1.4l-2.7,0.2l1.2-1.5
+	c0.7-0.8,8.5-10.8,17.5-22.3c8.9-11.5,17.2-21.8,18.5-23.1c2.6-2.7,7-6.2,10.3-8.2c19.3-11.6,43-11.1,61.6,1.2
+	c5.4,3.6,8.2,6.2,12.3,11.7c26.4,34.5,44,73.7,52.3,116.2c3.4,17.6,4.9,33.3,5,52.4c0,13-0.2,14.8-2.5,21.8
+	C547.8,328.6,521.7,345.2,494.7,341.7z"/>
+<path class="st4" d="M133.9,318.5c-2-0.5-4.6-1.9-6-3.3c-2.5-2.4-3.1-3.5-3.7-7.3c-4.4-27.3-2.2-54,6.7-79.3
+	c5.3-15.1,13.5-30.5,23-43.1c5.8-7.8,16.6-19.5,19-20.7c4.7-2.4,11.3-1.2,15.2,2.7c5.4,5.4,5.2,13.9-0.3,19.1
+	c-4.3,4-9.4,4.4-12.6,0.9c-1.7-1.9-2.2-3.9-1.7-6.4c0.2-1.1,0.3-2,0.2-2.2c-0.3-0.3-3.6,3.3-8.3,9.1c-17.6,21.8-28.5,48-31.9,76.5
+	c-1.1,9.3-1,26.4,0.1,34.6c0.3,1.8,0.8,1.9,1.4,0.1c0.9-2.6,4-4.7,6.8-4.7c3,0,5.9,2.2,7.5,5.7c0.6,1.3,0.8,2.3,0.8,5.2
+	c0,3.3-0.1,3.8-1.1,5.7c-1.4,2.7-4.6,5.7-7.1,6.6C139.4,318.6,135.8,318.9,133.9,318.5z"/>
+<path class="st1" d="M422.6,318.5c-3.7-0.6-7.7-3.6-9.4-7.1c-3.8-7.5,0.1-16.9,6.9-16.9c3.1,0,5.8,2,6.9,5.2
+	c0.4,1.2,0.5,1.3,0.7,0.7c1.3-3.7,1.7-26.4,0.6-35.7c-3.6-29.6-14.5-55.3-33-77.9c-5.5-6.7-8.4-9.4-7.1-6.6c0.7,1.4,0.5,4.3-0.3,5.9
+	c-0.9,1.7-3.2,3.5-5,3.8c-3.2,0.6-7.9-1.6-10.2-4.8c-6.5-8.8-0.5-21.2,10.4-21.4c4.6-0.1,5.2,0.3,11.2,6.4
+	c12.1,12.3,21.1,24.9,28.8,40.3c13.2,26.3,18.6,54.9,16.1,84.5c-0.5,5.6-2,15.7-2.6,17.1c-1.3,2.8-4.8,5.5-8.4,6.5
+	C425.9,318.9,425.1,318.9,422.6,318.5z"/>
+<path class="st0" d="M178.2,307.2c-6-1.3-12.2-6.2-14.9-11.7c-3.4-7-3.1-15.1,0.9-21.6c0.7-1.2,1.2-2.3,1.1-2.4
+	c-0.1-0.1-1.1-0.6-2.1-1c-3.9-1.5-8.1-4.8-10.7-8.3c-4.6-6.2-6.1-14.6-3.9-22.1c2.9-10.3,9.4-16.8,19.1-19.3c2.8-0.7,9-0.8,11.7,0
+	c1.1,0.3,2.2,0.5,2.4,0.5c0.2,0,0.3-0.7,0.3-1.5c0-2.9,0.8-5.8,2.4-9.2c5.2-10.8,18.1-15.5,29-10.5c2.7,1.2,6.2,3.8,7.8,5.8
+	c0.7,0.8,10.3,14,21.5,29.4l20.3,27.9l-1.5,1.8c-0.8,1-1.9,2.6-2.5,3.5c-0.6,1-1.2,1.7-1.5,1.6c-4.5-1.7-46.7-15-47.7-15
+	c-1.9,0-3.1,1.3-3.1,3.2c0,1,0.2,1.7,0.8,2.3c0.6,0.6,7.8,3.1,24.5,8.5l23.7,7.7l-0.1,4.3l-0.1,4.3L223,295.9
+	c-18,5.9-33.9,10.9-35.2,11.2C184.7,307.8,181.2,307.8,178.2,307.2z"/>
+<path class="st4" d="M372.5,306.8c-1.8-0.5-17.5-5.6-35-11.3l-31.8-10.4l1-4.3v-4.3l22.6-7.7c15-4.9,24-8,24.6-8.5
+	c0.7-0.6,0.9-1.1,0.9-2.2c0-2-1.2-3.3-3.1-3.3c-0.9,0-10.5,2.9-24.7,7.5c-12.8,4.1-23.4,7.5-23.6,7.5c-0.1,0-0.7-0.8-1.3-1.9
+	c-0.6-1-1.6-2.5-2.2-3.2c-0.7-0.7-1.2-1.5-1.2-1.6c0-0.2,9.6-13.5,21.4-29.6c18.9-26,21.6-29.6,23.6-31.1c5.7-4.4,13.1-5.8,19.7-3.9
+	c9,2.7,16.1,11.6,16.1,20.3c0,2.3-0.1,2.3,3.1,1.5c4.7-1.1,11.5-0.5,16,1.5c4.6,2,9,6,11.5,10.2c2.1,3.6,3.9,9.4,4.2,13.2
+	c0.3,5.2-1.1,10.7-4,15.3c-2.6,4.1-7.8,8.3-12.1,9.8c-0.9,0.3-1.7,0.8-1.7,1c0,0.2,0.4,1,0.9,1.7c2.4,3.6,3.6,7.7,3.5,12.7
+	c0,5.8-2.1,10.7-6.4,15.1c-4,4.1-8.9,6.3-14.9,6.5C376.3,307.7,375.3,307.6,372.5,306.8z"/>
+<path class="st5" d="M276.2,298.9c-6.1-1.6-11.4-6.8-13.2-12.9c-0.7-2.4-0.7-7.5,0-9.9c1.7-5.8,6.6-10.8,12.3-12.5
+	c2.7-0.8,7.2-0.9,10-0.2c6.2,1.6,11.6,7.1,13.2,13.3c1.6,6-0.3,12.6-5,17.3C288.9,298.6,282.2,300.5,276.2,298.9z"/>
+<path class="st2" d="M248.3,229.8c-13.3-18.3-21.2-29.6-22-31.1c-1.4-3-1.9-5.5-1.9-9.4c0-14.1,13.1-24.4,27.1-21.4
+	c1.4,0.3,2.6,0.5,2.7,0.5s0.3-1.3,0.4-2.8c0.8-10.7,8.4-19.6,18.9-22.4c3.9-1,10.6-1,14.5,0c8.9,2.3,15.9,9.3,18.2,18.2
+	c0.4,1.5,0.7,3.7,0.7,4.9c0,1.2,0.1,2.1,0.3,2.1s1.5-0.3,3-0.6c7.4-1.6,15.2,0.7,20.5,6c4.3,4.3,6.6,9.6,6.6,15.6
+	c0,4-0.6,6.5-2.4,10c-0.6,1.2-10.4,15-21.7,30.7c-17.8,24.5-20.8,28.5-21.4,28.3c-0.4-0.1-1.9-0.6-3.4-1.1c-1.5-0.5-2.9-0.9-3.3-0.9
+	c-0.7,0-0.7-0.8-0.3-25.5v-25.5l-1.4-0.9c-1-1.1-2.5-1.5-3.8-0.9c-2,0.8-2-0.5-1.8,27.2v25.8h-1.2c-0.5-0.2-2.4,0.3-4,0.9
+	s-3.1,1.1-3.2,1.1C269.2,258.5,259.8,245.6,248.3,229.8z"/>
+<path class="st3" d="M210.9,164.8c-4.1-0.9-7.7-3.6-9.6-7.4c-1.4-2.8-1.7-7.3-0.5-10.3c1.7-4.5,3.9-6.1,15.6-11.2
+	c15.8-7,31.4-11.1,49.2-12.9c7.3-0.8,23.2-0.8,30.6,0c17.4,1.8,33.3,6,49.1,13c7.3,3.2,12.5,6.1,13.6,7.5c4.3,5.6,3.8,12.7-1.1,17.6
+	c-5.1,5.1-12.9,5.4-18.1,0.7c-2-1.8-3-3.5-3.4-5.6c-0.7-4,2.9-8.1,7.3-8.2c1.4,0,1.5-0.1,1.1-0.5c-0.3-0.3-2.2-1.2-4.3-2.1
+	c-33.2-14.5-70.5-16.4-105-5.4c-7.5,2.4-19,7.2-18.6,7.7c0.1,0.2,0.8,0.3,1.6,0.3c5.6,0,9.1,6.2,6.1,10.8
+	C221.6,163.3,215.9,165.9,210.9,164.8z"/>
+<path class="st4" d="M174.7,123.4c-8.9-13.1-16.8-25.1-17.5-26.6c-1.6-3.3-3.6-9.2-4.4-13c-2.6-12.5-0.9-25.8,5-37.5
+	c4.2-8.3,11.2-16.3,18.6-21.3c5-3.4,6.1-3.9,12.8-6.3c23.1-8.2,47.2-13.1,73.4-15c7.5-0.6,28.5-0.6,36.3,0
+	c25.5,1.8,50.6,6.9,73,14.8c6.4,2.2,8.2,3.1,13.1,6.5c9.8,6.6,18.1,17.5,22,29.2c2.2,6.5,2.7,10,2.7,17.9c0,7.9-0.5,11.3-2.7,17.9
+	c-2.3,6.8-3.7,9.1-20.3,33.6l-16.1,23.8l-0.4-2.2c-0.2-1.2-0.9-3-1.4-4c-1-1.8-4.4-5.6-4.7-5.2c-0.1,0.1-1.2-0.4-2.4-1.1
+	c-9.1-5.2-21.9-10.5-33.2-13.9c-37-11-77.2-8.8-113,6.1c-4.9,2.1-17.7,8.4-19.2,9.5c-2.2,1.6-5.1,6.8-5.1,9c0,0.4-0.1,1-0.3,1.2
+	C191,147,184.7,138,174.7,123.4z"/>
+</svg>

+ 3 - 1
docs/tsconfig.json

@@ -1,7 +1,9 @@
 {
   // This file is not used in compilation. It is here just for a nice editor experience.
   "extends": "@tsconfig/docusaurus/tsconfig.json",
+
   "compilerOptions": {
-    "baseUrl": "."
+    "baseUrl": ".",
+    "module": "Node16"
   }
 }

+ 7 - 5
machine-learning/Dockerfile

@@ -1,4 +1,4 @@
-FROM python:3.11.4-bullseye@sha256:5b401676aff858495a5c9c726c60b8b73fe52833e9e16eccdb59e93d52741727 as builder
+FROM python:3.11-bookworm as builder
 
 ENV PYTHONDONTWRITEBYTECODE=1 \
   PYTHONUNBUFFERED=1 \
@@ -10,12 +10,13 @@ RUN poetry config installer.max-workers 10 && \
 RUN python -m venv /opt/venv
 ENV VIRTUAL_ENV="/opt/venv" PATH="/opt/venv/bin:${PATH}"
 
-COPY poetry.lock pyproject.toml ./
+COPY poetry.lock pyproject.toml requirements.txt ./
 RUN poetry install --sync --no-interaction --no-ansi --no-root --only main
+RUN pip install --no-deps -r requirements.txt
 
-FROM python:3.11.4-slim-bullseye@sha256:91d194f58f50594cda71dcd2e8fdefd90e7ecc57d07823813b67c8521e565dcd
+FROM python:3.11-slim-bookworm
 
-RUN apt-get update && apt-get install -y --no-install-recommends tini && rm -rf /var/lib/apt/lists/*
+RUN apt-get update && apt-get install -y --no-install-recommends tini libmimalloc2.0 && rm -rf /var/lib/apt/lists/*
 
 WORKDIR /usr/src/app
 ENV NODE_ENV=production \
@@ -26,6 +27,7 @@ ENV NODE_ENV=production \
   PYTHONPATH=/usr/src
 
 COPY --from=builder /opt/venv /opt/venv
+COPY start.sh log_conf.json ./
 COPY app .
 ENTRYPOINT ["tini", "--"]
-CMD ["python", "-m", "app.main"]
+CMD ["./start.sh"]

+ 4 - 2
machine-learning/README.md

@@ -17,6 +17,8 @@ Be sure to commit the `poetry.lock` and `pyproject.toml` files to reflect any ch
 
 To measure inference throughput and latency, you can use [Locust](https://locust.io/) using the provided `locustfile.py`.
 Locust works by querying the model endpoints and aggregating their statistics, meaning the app must be deployed.
-You can run `load_test.sh` to automatically deploy the app locally and start Locust, optionally adjusting its env variables as needed.
+You can change the models or adjust options like score thresholds through the Locust UI.
 
-Alternatively, for more custom testing, you may also run `locust` directly: see the [documentation](https://docs.locust.io/en/stable/index.html). Note that in Locust's jargon, concurrency is measured in `users`, and each user runs one task at a time. To achieve a particular per-endpoint concurrency, multiply that number by the number of endpoints to be queried. For example, if there are 3 endpoints and you want each of them to receive 8 requests at a time, you should set the number of users to 24.
+To get started, you can simply run `locust --web-host 127.0.0.1` and open `localhost:8089` in a browser to access the UI. See the [Locust documentation](https://docs.locust.io/en/stable/index.html) for more info on running Locust. 
+
+Note that in Locust's jargon, concurrency is measured in `users`, and each user runs one task at a time. To achieve a particular per-endpoint concurrency, multiply that number by the number of endpoints to be queried. For example, if there are 3 endpoints and you want each of them to receive 8 requests at a time, you should set the number of users to 24.

+ 22 - 0
machine-learning/README_fr_FR.md

@@ -0,0 +1,22 @@
+# Immich Apprentissage machine
+
+- Classification d'images
+- Embarquement de CLIP
+- Reconnaissance faciale
+
+# Mise en place
+
+Ce projet utilise [Poetry](https://python-poetry.org/docs/#installation), donc soyez certain de l'installer en premier.
+Exécuter `poetry install --no-root --with dev` installera tout ce dont vous avez besoin dans un environnement virtuel isolé.
+
+Pour ajouter ou supprimer des dépendances, vous pouvez utiliser les commandes `poetry add $PACKAGE_NAME` et `poetry remove $PACKAGE_NAME` respectivement.
+Soyez sûr de commit les fichiers `poetry.lock` et `pyproject.toml` pour refléter les changements de dépendances.
+
+
+# Test de charge
+
+Pour mesurer le débit d'inférence et la latence, vous pouvez utiliser [Locust](https://locust.io/) avec le fichier fourni `locustfile.py`.
+Locust fonctionne en interrogeant les endpoints des modèles et en aggrégeant leurs statistiques, signifiant que l'application doit être déployée.
+Vous pouvez exécuter `load_test.sh` pour automatiquement déployer l'application localement et démarrer Locust, en ajustant si besoin ses variables d'environnement.
+
+En alternative, pour réaliser plus de tests customisés, vous pourriez aussi exécuter `locust` directement : voir la [documentation](https://docs.locust.io/en/stable/index.html). Notez que dans le jargon de Locust, la concurrence est mesurée en `users` et que chaque user exécute une tâche après l'autre. Pour parvenir à une concurrence par endpoint, multipliez ce nombre par le nombre d'endpoints à interroger. Par exemple, s'il y a 3 endpoints et que vous voulez que chacun d'entre eux reçoive 8 requêtes à la fois, vous devrez mettre ce nombre d'users à 24.

+ 46 - 9
machine-learning/app/config.py

@@ -1,32 +1,69 @@
+import logging
+import os
 from pathlib import Path
 
+import gunicorn
+import starlette
 from pydantic import BaseSettings
+from rich.console import Console
+from rich.logging import RichHandler
 
 from .schemas import ModelType
 
 
 class Settings(BaseSettings):
     cache_folder: str = "/cache"
-    classification_model: str = "microsoft/resnet-50"
-    clip_image_model: str = "clip-ViT-B-32"
-    clip_text_model: str = "clip-ViT-B-32"
-    facial_recognition_model: str = "buffalo_l"
-    min_tag_score: float = 0.9
-    eager_startup: bool = True
     model_ttl: int = 0
     host: str = "0.0.0.0"
     port: int = 3003
     workers: int = 1
-    min_face_score: float = 0.7
     test_full: bool = False
+    request_threads: int = os.cpu_count() or 4
+    model_inter_op_threads: int = 1
+    model_intra_op_threads: int = 2
 
-    class Config(BaseSettings.Config):
+    class Config:
         env_prefix = "MACHINE_LEARNING_"
         case_sensitive = False
 
 
+class LogSettings(BaseSettings):
+    log_level: str = "info"
+    no_color: bool = False
+
+    class Config:
+        case_sensitive = False
+
+
+_clean_name = str.maketrans(":\\/", "___", ".")
+
+
 def get_cache_dir(model_name: str, model_type: ModelType) -> Path:
-    return Path(settings.cache_folder, model_type.value, model_name)
+    return Path(settings.cache_folder) / model_type.value / model_name.translate(_clean_name)
 
 
+LOG_LEVELS: dict[str, int] = {
+    "critical": logging.ERROR,
+    "error": logging.ERROR,
+    "warning": logging.WARNING,
+    "warn": logging.WARNING,
+    "info": logging.INFO,
+    "log": logging.INFO,
+    "debug": logging.DEBUG,
+    "verbose": logging.DEBUG,
+}
+
 settings = Settings()
+log_settings = LogSettings()
+
+
+class CustomRichHandler(RichHandler):
+    def __init__(self) -> None:
+        console = Console(color_system="standard", no_color=log_settings.no_color)
+        super().__init__(
+            show_path=False, omit_repeated_times=False, console=console, tracebacks_suppress=[gunicorn, starlette]
+        )
+
+
+log = logging.getLogger("gunicorn.access")
+log.setLevel(LOG_LEVELS.get(log_settings.log_level.lower(), logging.INFO))

+ 6 - 86
machine-learning/app/conftest.py

@@ -1,4 +1,4 @@
-from types import SimpleNamespace
+import json
 from typing import Any, Iterator, TypeAlias
 from unittest import mock
 
@@ -22,91 +22,6 @@ def cv_image(pil_image: Image.Image) -> ndarray:
     return np.asarray(pil_image)[:, :, ::-1]  # PIL uses RGB while cv2 uses BGR
 
 
-@pytest.fixture
-def mock_classifier_pipeline() -> Iterator[mock.Mock]:
-    with mock.patch("app.models.image_classification.pipeline") as model:
-        classifier_preds = [
-            {"label": "that's an image alright", "score": 0.8},
-            {"label": "well it ends with .jpg", "score": 0.1},
-            {"label": "idk, im just seeing bytes", "score": 0.05},
-            {"label": "not sure", "score": 0.04},
-            {"label": "probably a virus", "score": 0.01},
-        ]
-
-        def forward(
-            inputs: Image.Image | list[Image.Image], **kwargs: Any
-        ) -> list[dict[str, Any]] | list[list[dict[str, Any]]]:
-            if isinstance(inputs, list) and not all([isinstance(img, Image.Image) for img in inputs]):
-                raise TypeError
-            elif not isinstance(inputs, Image.Image):
-                raise TypeError
-
-            if isinstance(inputs, list):
-                return [classifier_preds] * len(inputs)
-
-            return classifier_preds
-
-        model.return_value = forward
-        yield model
-
-
-@pytest.fixture
-def mock_st() -> Iterator[mock.Mock]:
-    with mock.patch("app.models.clip.SentenceTransformer") as model:
-        embedding = np.random.rand(512).astype(np.float32)
-
-        def encode(inputs: Image.Image | list[Image.Image], **kwargs: Any) -> ndarray | list[ndarray]:
-            #  mypy complains unless isinstance(inputs, list) is used explicitly
-            img_batch = isinstance(inputs, list) and all([isinstance(inst, Image.Image) for inst in inputs])
-            text_batch = isinstance(inputs, list) and all([isinstance(inst, str) for inst in inputs])
-            if isinstance(inputs, list) and not any([img_batch, text_batch]):
-                raise TypeError
-
-            if isinstance(inputs, list):
-                return np.stack([embedding] * len(inputs))
-
-            return embedding
-
-        mocked = mock.Mock()
-        mocked.encode = encode
-        model.return_value = mocked
-        yield model
-
-
-@pytest.fixture
-def mock_faceanalysis() -> Iterator[mock.Mock]:
-    with mock.patch("app.models.facial_recognition.FaceAnalysis") as model:
-        face_preds = [
-            SimpleNamespace(  # this is so these fields can be accessed through dot notation
-                **{
-                    "bbox": np.random.rand(4).astype(np.float32),
-                    "kps": np.random.rand(5, 2).astype(np.float32),
-                    "det_score": np.array([0.67]).astype(np.float32),
-                    "normed_embedding": np.random.rand(512).astype(np.float32),
-                }
-            ),
-            SimpleNamespace(
-                **{
-                    "bbox": np.random.rand(4).astype(np.float32),
-                    "kps": np.random.rand(5, 2).astype(np.float32),
-                    "det_score": np.array([0.4]).astype(np.float32),
-                    "normed_embedding": np.random.rand(512).astype(np.float32),
-                }
-            ),
-        ]
-
-        def get(image: np.ndarray[int, np.dtype[np.float32]], **kwargs: Any) -> list[SimpleNamespace]:
-            if not isinstance(image, np.ndarray):
-                raise TypeError
-
-            return face_preds
-
-        mocked = mock.Mock()
-        mocked.get = get
-        model.return_value = mocked
-        yield model
-
-
 @pytest.fixture
 def mock_get_model() -> Iterator[mock.Mock]:
     with mock.patch("app.models.cache.InferenceModel.from_model_type", autospec=True) as mocked:
@@ -117,3 +32,8 @@ def mock_get_model() -> Iterator[mock.Mock]:
 def deployed_app() -> TestClient:
     init_state()
     return TestClient(app)
+
+
+@pytest.fixture(scope="session")
+def responses() -> dict[str, Any]:
+    return json.load(open("responses.json", "r"))

+ 83 - 98
machine-learning/app/main.py

@@ -1,62 +1,46 @@
-import os
-from io import BytesIO
+import asyncio
+import threading
+from concurrent.futures import ThreadPoolExecutor
 from typing import Any
+from zipfile import BadZipFile
 
-import cv2
-import numpy as np
-import uvicorn
-from fastapi import Body, Depends, FastAPI
-from PIL import Image
+import orjson
+from fastapi import FastAPI, Form, HTTPException, UploadFile
+from fastapi.responses import ORJSONResponse
+from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile  # type: ignore
+from starlette.formparsers import MultiPartParser
 
-from .config import settings
-from .models.base import InferenceModel
+from app.models.base import InferenceModel
+
+from .config import log, settings
 from .models.cache import ModelCache
 from .schemas import (
-    EmbeddingResponse,
-    FaceResponse,
     MessageResponse,
     ModelType,
-    TagResponse,
-    TextModelRequest,
     TextResponse,
 )
 
+MultiPartParser.max_file_size = 2**24  # spools to disk if payload is 16 MiB or larger
 app = FastAPI()
 
 
 def init_state() -> None:
     app.state.model_cache = ModelCache(ttl=settings.model_ttl, revalidate=settings.model_ttl > 0)
-
-
-async def load_models() -> None:
-    models = [
-        (settings.classification_model, ModelType.IMAGE_CLASSIFICATION),
-        (settings.clip_image_model, ModelType.CLIP),
-        (settings.clip_text_model, ModelType.CLIP),
-        (settings.facial_recognition_model, ModelType.FACIAL_RECOGNITION),
-    ]
-
-    # Get all models
-    for model_name, model_type in models:
-        if settings.eager_startup:
-            await app.state.model_cache.get(model_name, model_type)
-        else:
-            InferenceModel.from_model_type(model_type, model_name)
+    log.info(
+        (
+            "Created in-memory cache with unloading "
+            f"{f'after {settings.model_ttl}s of inactivity' if settings.model_ttl > 0 else 'disabled'}."
+        )
+    )
+    # asyncio is a huge bottleneck for performance, so we use a thread pool to run blocking code
+    app.state.thread_pool = ThreadPoolExecutor(settings.request_threads) if settings.request_threads > 0 else None
+    app.state.locks = {model_type: threading.Lock() for model_type in ModelType}
+    log.info(f"Initialized request thread pool with {settings.request_threads} threads.")
 
 
 @app.on_event("startup")
 async def startup_event() -> None:
     init_state()
-    await load_models()
-
-
-def dep_pil_image(byte_image: bytes = Body(...)) -> Image.Image:
-    return Image.open(BytesIO(byte_image))
-
-
-def dep_cv_image(byte_image: bytes = Body(...)) -> cv2.Mat:
-    byte_image_np = np.frombuffer(byte_image, np.uint8)
-    return cv2.imdecode(byte_image_np, cv2.IMREAD_COLOR)
 
 
 @app.get("/", response_model=MessageResponse)
@@ -69,62 +53,63 @@ def ping() -> str:
     return "pong"
 
 
-@app.post(
-    "/image-classifier/tag-image",
-    response_model=TagResponse,
-    status_code=200,
-)
-async def image_classification(
-    image: Image.Image = Depends(dep_pil_image),
-) -> list[str]:
-    model = await app.state.model_cache.get(settings.classification_model, ModelType.IMAGE_CLASSIFICATION)
-    labels = model.predict(image)
-    return labels
-
-
-@app.post(
-    "/sentence-transformer/encode-image",
-    response_model=EmbeddingResponse,
-    status_code=200,
-)
-async def clip_encode_image(
-    image: Image.Image = Depends(dep_pil_image),
-) -> list[float]:
-    model = await app.state.model_cache.get(settings.clip_image_model, ModelType.CLIP)
-    embedding = model.predict(image)
-    return embedding
-
-
-@app.post(
-    "/sentence-transformer/encode-text",
-    response_model=EmbeddingResponse,
-    status_code=200,
-)
-async def clip_encode_text(payload: TextModelRequest) -> list[float]:
-    model = await app.state.model_cache.get(settings.clip_text_model, ModelType.CLIP)
-    embedding = model.predict(payload.text)
-    return embedding
-
-
-@app.post(
-    "/facial-recognition/detect-faces",
-    response_model=FaceResponse,
-    status_code=200,
-)
-async def facial_recognition(
-    image: cv2.Mat = Depends(dep_cv_image),
-) -> list[dict[str, Any]]:
-    model = await app.state.model_cache.get(settings.facial_recognition_model, ModelType.FACIAL_RECOGNITION)
-    faces = model.predict(image)
-    return faces
-
-
-if __name__ == "__main__":
-    is_dev = os.getenv("NODE_ENV") == "development"
-    uvicorn.run(
-        "app.main:app",
-        host=settings.host,
-        port=settings.port,
-        reload=is_dev,
-        workers=settings.workers,
-    )
+@app.post("/predict")
+async def predict(
+    model_name: str = Form(alias="modelName"),
+    model_type: ModelType = Form(alias="modelType"),
+    options: str = Form(default="{}"),
+    text: str | None = Form(default=None),
+    image: UploadFile | None = None,
+) -> Any:
+    if image is not None:
+        inputs: str | bytes = await image.read()
+    elif text is not None:
+        inputs = text
+    else:
+        raise HTTPException(400, "Either image or text must be provided")
+    try:
+        kwargs = orjson.loads(options)
+    except orjson.JSONDecodeError:
+        raise HTTPException(400, f"Invalid options JSON: {options}")
+
+    model = await load(await app.state.model_cache.get(model_name, model_type, **kwargs))
+    model.configure(**kwargs)
+    outputs = await run(model, inputs)
+    return ORJSONResponse(outputs)
+
+
+async def run(model: InferenceModel, inputs: Any) -> Any:
+    if app.state.thread_pool is None:
+        return model.predict(inputs)
+
+    return await asyncio.get_running_loop().run_in_executor(app.state.thread_pool, model.predict, inputs)
+
+
+async def load(model: InferenceModel) -> InferenceModel:
+    if model.loaded:
+        return model
+
+    def _load() -> None:
+        with app.state.locks[model.model_type]:
+            model.load()
+
+    loop = asyncio.get_running_loop()
+    try:
+        if app.state.thread_pool is None:
+            model.load()
+        else:
+            await loop.run_in_executor(app.state.thread_pool, _load)
+        return model
+    except (OSError, InvalidProtobuf, BadZipFile, NoSuchFile):
+        log.warn(
+            (
+                f"Failed to load {model.model_type.replace('_', ' ')} model '{model.model_name}'."
+                "Clearing cache and retrying."
+            )
+        )
+        model.clear_cache()
+        if app.state.thread_pool is None:
+            model.load()
+        else:
+            await loop.run_in_executor(app.state.thread_pool, _load)
+        return model

+ 1 - 1
machine-learning/app/models/__init__.py

@@ -1,3 +1,3 @@
-from .clip import CLIPSTEncoder
+from .clip import CLIPEncoder
 from .facial_recognition import FaceRecognizer
 from .image_classification import ImageClassifier

+ 95 - 12
machine-learning/app/models/base.py

@@ -1,35 +1,89 @@
 from __future__ import annotations
 
+import pickle
 from abc import ABC, abstractmethod
 from pathlib import Path
 from shutil import rmtree
 from typing import Any
 
-from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf  # type: ignore
+import onnxruntime as ort
 
-from ..config import get_cache_dir
+from ..config import get_cache_dir, log, settings
 from ..schemas import ModelType
 
 
 class InferenceModel(ABC):
     _model_type: ModelType
 
-    def __init__(self, model_name: str, cache_dir: Path | str | None = None, **model_kwargs: Any) -> None:
+    def __init__(
+        self,
+        model_name: str,
+        cache_dir: Path | str | None = None,
+        inter_op_num_threads: int = settings.model_inter_op_threads,
+        intra_op_num_threads: int = settings.model_intra_op_threads,
+        **model_kwargs: Any,
+    ) -> None:
         self.model_name = model_name
+        self.loaded = False
         self._cache_dir = Path(cache_dir) if cache_dir is not None else get_cache_dir(model_name, self.model_type)
+        self.providers = model_kwargs.pop("providers", ["CPUExecutionProvider"])
+        #  don't pre-allocate more memory than needed
+        self.provider_options = model_kwargs.pop(
+            "provider_options", [{"arena_extend_strategy": "kSameAsRequested"}] * len(self.providers)
+        )
+        log.debug(
+            (
+                f"Setting '{self.model_name}' execution providers to {self.providers}"
+                "in descending order of preference"
+            ),
+        )
+        log.debug(f"Setting execution provider options to {self.provider_options}")
+        self.sess_options = PicklableSessionOptions()
+        # avoid thread contention between models
+        if inter_op_num_threads > 1:
+            self.sess_options.execution_mode = ort.ExecutionMode.ORT_PARALLEL
+
+        log.debug(f"Setting execution_mode to {self.sess_options.execution_mode.name}")
+        log.debug(f"Setting inter_op_num_threads to {inter_op_num_threads}")
+        log.debug(f"Setting intra_op_num_threads to {intra_op_num_threads}")
+        self.sess_options.inter_op_num_threads = inter_op_num_threads
+        self.sess_options.intra_op_num_threads = intra_op_num_threads
+        self.sess_options.enable_cpu_mem_arena = False
+
+    def download(self) -> None:
+        if not self.cached:
+            log.info(
+                (f"Downloading {self.model_type.replace('-', ' ')} model '{self.model_name}'." "This may take a while.")
+            )
+            self._download()
+
+    def load(self) -> None:
+        if self.loaded:
+            return
+        self.download()
+        log.info(f"Loading {self.model_type.replace('-', ' ')} model '{self.model_name}'")
+        self._load()
+        self.loaded = True
 
-        try:
-            self.load(**model_kwargs)
-        except (OSError, InvalidProtobuf):
-            self.clear_cache()
-            self.load(**model_kwargs)
+    def predict(self, inputs: Any, **model_kwargs: Any) -> Any:
+        self.load()
+        if model_kwargs:
+            self.configure(**model_kwargs)
+        return self._predict(inputs)
 
     @abstractmethod
-    def load(self, **model_kwargs: Any) -> None:
+    def _predict(self, inputs: Any) -> Any:
         ...
 
+    def configure(self, **model_kwargs: Any) -> None:
+        pass
+
     @abstractmethod
-    def predict(self, inputs: Any) -> Any:
+    def _download(self) -> None:
+        ...
+
+    @abstractmethod
+    def _load(self) -> None:
         ...
 
     @property
@@ -44,6 +98,10 @@ class InferenceModel(ABC):
     def cache_dir(self, cache_dir: Path) -> None:
         self._cache_dir = cache_dir
 
+    @property
+    def cached(self) -> bool:
+        return self.cache_dir.exists() and any(self.cache_dir.iterdir())
+
     @classmethod
     def from_model_type(cls, model_type: ModelType, model_name: str, **model_kwargs: Any) -> InferenceModel:
         subclasses = {subclass._model_type: subclass for subclass in cls.__subclasses__()}
@@ -54,8 +112,33 @@ class InferenceModel(ABC):
 
     def clear_cache(self) -> None:
         if not self.cache_dir.exists():
+            log.warn(
+                f"Attempted to clear cache for model '{self.model_name}' but cache directory does not exist.",
+            )
             return
-        elif not rmtree.avoids_symlink_attacks:
+        if not rmtree.avoids_symlink_attacks:
             raise RuntimeError("Attempted to clear cache, but rmtree is not safe on this platform.")
 
-        rmtree(self.cache_dir)
+        if self.cache_dir.is_dir():
+            log.info(f"Cleared cache directory for model '{self.model_name}'.")
+            rmtree(self.cache_dir)
+        else:
+            log.warn(
+                (
+                    f"Encountered file instead of directory at cache path "
+                    f"for '{self.model_name}'. Removing file and replacing with a directory."
+                ),
+            )
+            self.cache_dir.unlink()
+        self.cache_dir.mkdir(parents=True, exist_ok=True)
+
+
+# HF deep copies configs, so we need to make session options picklable
+class PicklableSessionOptions(ort.SessionOptions):
+    def __getstate__(self) -> bytes:
+        return pickle.dumps([(attr, getattr(self, attr)) for attr in dir(self) if not callable(getattr(self, attr))])
+
+    def __setstate__(self, state: Any) -> None:
+        self.__init__()  # type: ignore
+        for attr, val in pickle.loads(state):
+            setattr(self, attr, val)

+ 2 - 2
machine-learning/app/models/cache.py

@@ -17,7 +17,7 @@ class ModelCache:
         revalidate: bool = False,
         timeout: int | None = None,
         profiling: bool = False,
-    ):
+    ) -> None:
         """
         Args:
             ttl: Unloads model after this duration. Disabled if None. Defaults to None.
@@ -46,7 +46,7 @@ class ModelCache:
             model: The requested model.
         """
 
-        key = self.cache.build_key(model_name, model_type.value)
+        key = f"{model_name}{model_type.value}{model_kwargs.get('mode', '')}"
         async with OptimisticLock(self.cache, key) as lock:
             model = await self.cache.get(key)
             if model is None:

+ 122 - 12
machine-learning/app/models/clip.py

@@ -1,22 +1,132 @@
-from pathlib import Path
-from typing import Any
+import os
+import zipfile
+from io import BytesIO
+from typing import Any, Literal
 
-from PIL.Image import Image
-from sentence_transformers import SentenceTransformer
+import onnxruntime as ort
+import torch
+from clip_server.model.clip import BICUBIC, _convert_image_to_rgb
+from clip_server.model.clip_onnx import _MODELS, _S3_BUCKET_V2, CLIPOnnxModel, download_model
+from clip_server.model.pretrained_models import _VISUAL_MODEL_IMAGE_SIZE
+from clip_server.model.tokenization import Tokenizer
+from PIL import Image
+from torchvision.transforms import CenterCrop, Compose, Normalize, Resize, ToTensor
 
+from ..config import log
 from ..schemas import ModelType
 from .base import InferenceModel
 
 
-class CLIPSTEncoder(InferenceModel):
+class CLIPEncoder(InferenceModel):
     _model_type = ModelType.CLIP
 
-    def load(self, **model_kwargs: Any) -> None:
-        self.model = SentenceTransformer(
-            self.model_name,
-            cache_folder=self.cache_dir.as_posix(),
-            **model_kwargs,
+    def __init__(
+        self,
+        model_name: str,
+        cache_dir: str | None = None,
+        mode: Literal["text", "vision"] | None = None,
+        **model_kwargs: Any,
+    ) -> None:
+        if mode is not None and mode not in ("text", "vision"):
+            raise ValueError(f"Mode must be 'text', 'vision', or omitted; got '{mode}'")
+        if model_name not in _MODELS:
+            raise ValueError(f"Unknown model name {model_name}.")
+        self.mode = mode
+        super().__init__(model_name, cache_dir, **model_kwargs)
+
+    def _download(self) -> None:
+        models: tuple[tuple[str, str], tuple[str, str]] = _MODELS[self.model_name]
+        text_onnx_path = self.cache_dir / "textual.onnx"
+        vision_onnx_path = self.cache_dir / "visual.onnx"
+
+        if not text_onnx_path.is_file():
+            self._download_model(*models[0])
+
+        if not vision_onnx_path.is_file():
+            self._download_model(*models[1])
+
+    def _load(self) -> None:
+        if self.mode == "text" or self.mode is None:
+            log.debug(f"Loading clip text model '{self.model_name}'")
+            self.text_model = ort.InferenceSession(
+                self.cache_dir / "textual.onnx",
+                sess_options=self.sess_options,
+                providers=self.providers,
+                provider_options=self.provider_options,
+            )
+            self.text_outputs = [output.name for output in self.text_model.get_outputs()]
+            self.tokenizer = Tokenizer(self.model_name)
+
+        if self.mode == "vision" or self.mode is None:
+            log.debug(f"Loading clip vision model '{self.model_name}'")
+            self.vision_model = ort.InferenceSession(
+                self.cache_dir / "visual.onnx",
+                sess_options=self.sess_options,
+                providers=self.providers,
+                provider_options=self.provider_options,
+            )
+            self.vision_outputs = [output.name for output in self.vision_model.get_outputs()]
+
+            image_size = _VISUAL_MODEL_IMAGE_SIZE[CLIPOnnxModel.get_model_name(self.model_name)]
+            self.transform = _transform_pil_image(image_size)
+
+    def _predict(self, image_or_text: Image.Image | str) -> list[float]:
+        if isinstance(image_or_text, bytes):
+            image_or_text = Image.open(BytesIO(image_or_text))
+
+        match image_or_text:
+            case Image.Image():
+                if self.mode == "text":
+                    raise TypeError("Cannot encode image as text-only model")
+                pixel_values = self.transform(image_or_text)
+                assert isinstance(pixel_values, torch.Tensor)
+                pixel_values = torch.unsqueeze(pixel_values, 0).numpy()
+                outputs = self.vision_model.run(self.vision_outputs, {"pixel_values": pixel_values})
+            case str():
+                if self.mode == "vision":
+                    raise TypeError("Cannot encode text as vision-only model")
+                text_inputs: dict[str, torch.Tensor] = self.tokenizer(image_or_text)
+                inputs = {
+                    "input_ids": text_inputs["input_ids"].int().numpy(),
+                    "attention_mask": text_inputs["attention_mask"].int().numpy(),
+                }
+                outputs = self.text_model.run(self.text_outputs, inputs)
+            case _:
+                raise TypeError(f"Expected Image or str, but got: {type(image_or_text)}")
+
+        return outputs[0][0].tolist()
+
+    def _download_model(self, model_name: str, model_md5: str) -> bool:
+        # downloading logic is adapted from clip-server's CLIPOnnxModel class
+        download_model(
+            url=_S3_BUCKET_V2 + model_name,
+            target_folder=self.cache_dir.as_posix(),
+            md5sum=model_md5,
+            with_resume=True,
         )
+        file = self.cache_dir / model_name.split("/")[1]
+        if file.suffix == ".zip":
+            with zipfile.ZipFile(file, "r") as zip_ref:
+                zip_ref.extractall(self.cache_dir)
+            os.remove(file)
+        return True
+
+    @property
+    def cached(self) -> bool:
+        return (self.cache_dir / "textual.onnx").is_file() and (self.cache_dir / "visual.onnx").is_file()
+
 
-    def predict(self, image_or_text: Image | str) -> list[float]:
-        return self.model.encode(image_or_text).tolist()
+# same as `_transform_blob` without `_blob2image`
+def _transform_pil_image(n_px: int) -> Compose:
+    return Compose(
+        [
+            Resize(n_px, interpolation=BICUBIC),
+            CenterCrop(n_px),
+            _convert_image_to_rgb,
+            ToTensor(),
+            Normalize(
+                (0.48145466, 0.4578275, 0.40821073),
+                (0.26862954, 0.26130258, 0.27577711),
+            ),
+        ]
+    )

+ 72 - 24
machine-learning/app/models/facial_recognition.py

@@ -1,10 +1,14 @@
+import zipfile
 from pathlib import Path
 from typing import Any
 
 import cv2
-from insightface.app import FaceAnalysis
+import numpy as np
+import onnxruntime as ort
+from insightface.model_zoo import ArcFaceONNX, RetinaFace
+from insightface.utils.face_align import norm_crop
+from insightface.utils.storage import BASE_REPO_URL, download_file
 
-from ..config import settings
 from ..schemas import ModelType
 from .base import InferenceModel
 
@@ -15,46 +19,90 @@ class FaceRecognizer(InferenceModel):
     def __init__(
         self,
         model_name: str,
-        min_score: float = settings.min_face_score,
+        min_score: float = 0.7,
         cache_dir: Path | str | None = None,
         **model_kwargs: Any,
     ) -> None:
-        self.min_score = min_score
+        self.min_score = model_kwargs.pop("minScore", min_score)
         super().__init__(model_name, cache_dir, **model_kwargs)
 
-    def load(self, **model_kwargs: Any) -> None:
-        self.model = FaceAnalysis(
-            name=self.model_name,
-            root=self.cache_dir.as_posix(),
-            allowed_modules=["detection", "recognition"],
-            **model_kwargs,
+    def _download(self) -> None:
+        zip_file = self.cache_dir / f"{self.model_name}.zip"
+        download_file(f"{BASE_REPO_URL}/{self.model_name}.zip", zip_file)
+        with zipfile.ZipFile(zip_file, "r") as zip:
+            members = zip.namelist()
+            det_file = next(model for model in members if model.startswith("det_"))
+            rec_file = next(model for model in members if model.startswith("w600k_"))
+            zip.extractall(self.cache_dir, members=[det_file, rec_file])
+        zip_file.unlink()
+
+    def _load(self) -> None:
+        try:
+            det_file = next(self.cache_dir.glob("det_*.onnx"))
+            rec_file = next(self.cache_dir.glob("w600k_*.onnx"))
+        except StopIteration:
+            raise FileNotFoundError("Facial recognition models not found in cache directory")
+
+        self.det_model = RetinaFace(
+            session=ort.InferenceSession(
+                det_file.as_posix(),
+                sess_options=self.sess_options,
+                providers=self.providers,
+                provider_options=self.provider_options,
+            ),
+        )
+        self.rec_model = ArcFaceONNX(
+            rec_file.as_posix(),
+            session=ort.InferenceSession(
+                rec_file.as_posix(),
+                sess_options=self.sess_options,
+                providers=self.providers,
+                provider_options=self.provider_options,
+            ),
         )
-        self.model.prepare(
+
+        self.det_model.prepare(
             ctx_id=0,
             det_thresh=self.min_score,
-            det_size=(640, 640),
+            input_size=(640, 640),
         )
+        self.rec_model.prepare(ctx_id=0)
 
-    def predict(self, image: cv2.Mat) -> list[dict[str, Any]]:
-        height, width, _ = image.shape
-        results = []
-        faces = self.model.get(image)
+    def _predict(self, image: np.ndarray[int, np.dtype[Any]] | bytes) -> list[dict[str, Any]]:
+        if isinstance(image, bytes):
+            image = cv2.imdecode(np.frombuffer(image, np.uint8), cv2.IMREAD_COLOR)
+        bboxes, kpss = self.det_model.detect(image)
+        if bboxes.size == 0:
+            return []
+        assert isinstance(image, np.ndarray) and isinstance(kpss, np.ndarray)
 
-        for face in faces:
-            x1, y1, x2, y2 = face.bbox
+        scores = bboxes[:, 4].tolist()
+        bboxes = bboxes[:, :4].round().tolist()
 
+        results = []
+        height, width, _ = image.shape
+        for (x1, y1, x2, y2), score, kps in zip(bboxes, scores, kpss):
+            cropped_img = norm_crop(image, kps)
+            embedding = self.rec_model.get_feat(cropped_img)[0].tolist()
             results.append(
                 {
                     "imageWidth": width,
                     "imageHeight": height,
                     "boundingBox": {
-                        "x1": round(x1),
-                        "y1": round(y1),
-                        "x2": round(x2),
-                        "y2": round(y2),
+                        "x1": x1,
+                        "y1": y1,
+                        "x2": x2,
+                        "y2": y2,
                     },
-                    "score": face.det_score.item(),
-                    "embedding": face.normed_embedding.tolist(),
+                    "score": score,
+                    "embedding": embedding,
                 }
             )
         return results
+
+    @property
+    def cached(self) -> bool:
+        return self.cache_dir.is_dir() and any(self.cache_dir.glob("*.onnx"))
+
+    def configure(self, **model_kwargs: Any) -> None:
+        self.det_model.det_thresh = model_kwargs.pop("minScore", self.det_model.det_thresh)

+ 50 - 11
machine-learning/app/models/image_classification.py

@@ -1,10 +1,14 @@
+from io import BytesIO
 from pathlib import Path
 from typing import Any
 
-from PIL.Image import Image
-from transformers.pipelines import pipeline
+from huggingface_hub import snapshot_download
+from optimum.onnxruntime import ORTModelForImageClassification
+from optimum.pipelines import pipeline
+from PIL import Image
+from transformers import AutoImageProcessor
 
-from ..config import settings
+from ..config import log
 from ..schemas import ModelType
 from .base import InferenceModel
 
@@ -15,22 +19,57 @@ class ImageClassifier(InferenceModel):
     def __init__(
         self,
         model_name: str,
-        min_score: float = settings.min_tag_score,
+        min_score: float = 0.9,
         cache_dir: Path | str | None = None,
         **model_kwargs: Any,
     ) -> None:
-        self.min_score = min_score
+        self.min_score = model_kwargs.pop("minScore", min_score)
         super().__init__(model_name, cache_dir, **model_kwargs)
 
-    def load(self, **model_kwargs: Any) -> None:
-        self.model = pipeline(
-            self.model_type.value,
-            self.model_name,
-            model_kwargs={"cache_dir": self.cache_dir, **model_kwargs},
+    def _download(self) -> None:
+        snapshot_download(
+            cache_dir=self.cache_dir,
+            repo_id=self.model_name,
+            allow_patterns=["*.bin", "*.json", "*.txt"],
+            local_dir=self.cache_dir,
+            local_dir_use_symlinks=True,
         )
 
-    def predict(self, image: Image) -> list[str]:
+    def _load(self) -> None:
+        processor = AutoImageProcessor.from_pretrained(self.cache_dir, cache_dir=self.cache_dir)
+        model_path = self.cache_dir / "model.onnx"
+        model_kwargs = {
+            "cache_dir": self.cache_dir,
+            "provider": self.providers[0],
+            "provider_options": self.provider_options[0],
+            "session_options": self.sess_options,
+        }
+
+        if model_path.exists():
+            model = ORTModelForImageClassification.from_pretrained(self.cache_dir, **model_kwargs)
+            self.model = pipeline(self.model_type.value, model, feature_extractor=processor)
+        else:
+            log.info(
+                (
+                    f"ONNX model not found in cache directory for '{self.model_name}'."
+                    "Exporting optimized model for future use."
+                ),
+            )
+            self.sess_options.optimized_model_filepath = model_path.as_posix()
+            self.model = pipeline(
+                self.model_type.value,
+                self.model_name,
+                model_kwargs=model_kwargs,
+                feature_extractor=processor,
+            )
+
+    def _predict(self, image: Image.Image | bytes) -> list[str]:
+        if isinstance(image, bytes):
+            image = Image.open(BytesIO(image))
         predictions: list[dict[str, Any]] = self.model(image)  # type: ignore
         tags = [tag for pred in predictions for tag in pred["label"].split(", ") if pred["score"] >= self.min_score]
 
         return tags
+
+    def configure(self, **model_kwargs: Any) -> None:
+        self.min_score = model_kwargs.pop("minScore", self.min_score)

+ 2 - 30
machine-learning/app/schemas.py

@@ -1,4 +1,4 @@
-from enum import Enum
+from enum import StrEnum
 
 from pydantic import BaseModel
 
@@ -20,18 +20,6 @@ class MessageResponse(BaseModel):
     message: str
 
 
-class TagResponse(BaseModel):
-    __root__: list[str]
-
-
-class Embedding(BaseModel):
-    __root__: list[float]
-
-
-class EmbeddingResponse(BaseModel):
-    __root__: Embedding
-
-
 class BoundingBox(BaseModel):
     x1: int
     y1: int
@@ -39,23 +27,7 @@ class BoundingBox(BaseModel):
     y2: int
 
 
-class Face(BaseModel):
-    image_width: int
-    image_height: int
-    bounding_box: BoundingBox
-    score: float
-    embedding: Embedding
-
-    class Config:
-        alias_generator = to_lower_camel
-        allow_population_by_field_name = True
-
-
-class FaceResponse(BaseModel):
-    __root__: list[Face]
-
-
-class ModelType(Enum):
+class ModelType(StrEnum):
     IMAGE_CLASSIFICATION = "image-classification"
     CLIP = "clip"
     FACIAL_RECOGNITION = "facial-recognition"

+ 111 - 55
machine-learning/app/test_main.py

@@ -1,35 +1,44 @@
+import json
+import pickle
 from io import BytesIO
-from pathlib import Path
+from typing import Any, TypeAlias
 from unittest import mock
 
 import cv2
+import numpy as np
 import pytest
 from fastapi.testclient import TestClient
 from PIL import Image
+from pytest_mock import MockerFixture
 
 from .config import settings
+from .models.base import PicklableSessionOptions
 from .models.cache import ModelCache
-from .models.clip import CLIPSTEncoder
+from .models.clip import CLIPEncoder
 from .models.facial_recognition import FaceRecognizer
 from .models.image_classification import ImageClassifier
 from .schemas import ModelType
 
+ndarray: TypeAlias = np.ndarray[int, np.dtype[np.float32]]
 
-class TestImageClassifier:
-    def test_init(self, mock_classifier_pipeline: mock.Mock) -> None:
-        cache_dir = Path("test_cache")
-        classifier = ImageClassifier("test_model_name", 0.5, cache_dir=cache_dir)
-
-        assert classifier.min_score == 0.5
-        mock_classifier_pipeline.assert_called_once_with(
-            "image-classification",
-            "test_model_name",
-            model_kwargs={"cache_dir": cache_dir},
-        )
 
-    def test_min_score(self, pil_image: Image.Image, mock_classifier_pipeline: mock.Mock) -> None:
+class TestImageClassifier:
+    classifier_preds = [
+        {"label": "that's an image alright", "score": 0.8},
+        {"label": "well it ends with .jpg", "score": 0.1},
+        {"label": "idk, im just seeing bytes", "score": 0.05},
+        {"label": "not sure", "score": 0.04},
+        {"label": "probably a virus", "score": 0.01},
+    ]
+
+    def test_min_score(self, pil_image: Image.Image, mocker: MockerFixture) -> None:
+        mocker.patch.object(ImageClassifier, "load")
         classifier = ImageClassifier("test_model_name", min_score=0.0)
-        classifier.min_score = 0.0
+        assert classifier.min_score == 0.0
+
+        classifier.model = mock.Mock()
+        classifier.model.return_value = self.classifier_preds
+
         all_labels = classifier.predict(pil_image)
         classifier.min_score = 0.5
         filtered_labels = classifier.predict(pil_image)
@@ -46,45 +55,62 @@ class TestImageClassifier:
 
 
 class TestCLIP:
-    def test_init(self, mock_st: mock.Mock) -> None:
-        CLIPSTEncoder("test_model_name", cache_dir="test_cache")
-
-        mock_st.assert_called_once_with("test_model_name", cache_folder="test_cache")
-
-    def test_basic_image(self, pil_image: Image.Image, mock_st: mock.Mock) -> None:
-        clip_encoder = CLIPSTEncoder("test_model_name", cache_dir="test_cache")
+    embedding = np.random.rand(512).astype(np.float32)
+
+    def test_basic_image(self, pil_image: Image.Image, mocker: MockerFixture) -> None:
+        mocker.patch.object(CLIPEncoder, "download")
+        mocked = mocker.patch("app.models.clip.ort.InferenceSession", autospec=True)
+        mocked.return_value.run.return_value = [[self.embedding]]
+        clip_encoder = CLIPEncoder("ViT-B-32::openai", cache_dir="test_cache", mode="vision")
+        assert clip_encoder.mode == "vision"
         embedding = clip_encoder.predict(pil_image)
 
         assert isinstance(embedding, list)
         assert len(embedding) == 512
         assert all([isinstance(num, float) for num in embedding])
-        mock_st.assert_called_once()
-
-    def test_basic_text(self, mock_st: mock.Mock) -> None:
-        clip_encoder = CLIPSTEncoder("test_model_name", cache_dir="test_cache")
+        clip_encoder.vision_model.run.assert_called_once()
+
+    def test_basic_text(self, mocker: MockerFixture) -> None:
+        mocker.patch.object(CLIPEncoder, "download")
+        mocked = mocker.patch("app.models.clip.ort.InferenceSession", autospec=True)
+        mocked.return_value.run.return_value = [[self.embedding]]
+        clip_encoder = CLIPEncoder("ViT-B-32::openai", cache_dir="test_cache", mode="text")
+        assert clip_encoder.mode == "text"
         embedding = clip_encoder.predict("test search query")
 
         assert isinstance(embedding, list)
         assert len(embedding) == 512
         assert all([isinstance(num, float) for num in embedding])
-        mock_st.assert_called_once()
+        clip_encoder.text_model.run.assert_called_once()
 
 
 class TestFaceRecognition:
-    def test_init(self, mock_faceanalysis: mock.Mock) -> None:
-        FaceRecognizer("test_model_name", cache_dir="test_cache")
+    def test_set_min_score(self, mocker: MockerFixture) -> None:
+        mocker.patch.object(FaceRecognizer, "load")
+        face_recognizer = FaceRecognizer("test_model_name", cache_dir="test_cache", min_score=0.5)
 
-        mock_faceanalysis.assert_called_once_with(
-            name="test_model_name",
-            root="test_cache",
-            allowed_modules=["detection", "recognition"],
-        )
+        assert face_recognizer.min_score == 0.5
 
-    def test_basic(self, cv_image: cv2.Mat, mock_faceanalysis: mock.Mock) -> None:
+    def test_basic(self, cv_image: cv2.Mat, mocker: MockerFixture) -> None:
+        mocker.patch.object(FaceRecognizer, "load")
         face_recognizer = FaceRecognizer("test_model_name", min_score=0.0, cache_dir="test_cache")
+
+        det_model = mock.Mock()
+        num_faces = 2
+        bbox = np.random.rand(num_faces, 4).astype(np.float32)
+        score = np.array([[0.67]] * num_faces).astype(np.float32)
+        kpss = np.random.rand(num_faces, 5, 2).astype(np.float32)
+        det_model.detect.return_value = (np.concatenate([bbox, score], axis=-1), kpss)
+        face_recognizer.det_model = det_model
+
+        rec_model = mock.Mock()
+        embedding = np.random.rand(num_faces, 512).astype(np.float32)
+        rec_model.get_feat.return_value = embedding
+        face_recognizer.rec_model = rec_model
+
         faces = face_recognizer.predict(cv_image)
 
-        assert len(faces) == 2
+        assert len(faces) == num_faces
         for face in faces:
             assert face["imageHeight"] == 800
             assert face["imageWidth"] == 600
@@ -92,7 +118,8 @@ class TestFaceRecognition:
             assert len(face["embedding"]) == 512
             assert all([isinstance(num, float) for num in face["embedding"]])
 
-        mock_faceanalysis.assert_called_once()
+        det_model.detect.assert_called_once()
+        assert rec_model.get_feat.call_count == num_faces
 
 
 @pytest.mark.asyncio
@@ -142,42 +169,71 @@ class TestCache:
     reason="More time-consuming since it deploys the app and loads models.",
 )
 class TestEndpoints:
-    def test_tagging_endpoint(self, pil_image: Image.Image, deployed_app: TestClient) -> None:
+    def test_tagging_endpoint(
+        self, pil_image: Image.Image, responses: dict[str, Any], deployed_app: TestClient
+    ) -> None:
         byte_image = BytesIO()
         pil_image.save(byte_image, format="jpeg")
-        headers = {"Content-Type": "image/jpg"}
         response = deployed_app.post(
-            "http://localhost:3003/image-classifier/tag-image",
-            content=byte_image.getvalue(),
-            headers=headers,
+            "http://localhost:3003/predict",
+            data={
+                "modelName": "microsoft/resnet-50",
+                "modelType": "image-classification",
+                "options": json.dumps({"minScore": 0.0}),
+            },
+            files={"image": byte_image.getvalue()},
         )
         assert response.status_code == 200
+        assert response.json() == responses["image-classification"]
 
-    def test_clip_image_endpoint(self, pil_image: Image.Image, deployed_app: TestClient) -> None:
+    def test_clip_image_endpoint(
+        self, pil_image: Image.Image, responses: dict[str, Any], deployed_app: TestClient
+    ) -> None:
         byte_image = BytesIO()
         pil_image.save(byte_image, format="jpeg")
-        headers = {"Content-Type": "image/jpg"}
         response = deployed_app.post(
-            "http://localhost:3003/sentence-transformer/encode-image",
-            content=byte_image.getvalue(),
-            headers=headers,
+            "http://localhost:3003/predict",
+            data={"modelName": "ViT-B-32::openai", "modelType": "clip", "options": json.dumps({"mode": "vision"})},
+            files={"image": byte_image.getvalue()},
         )
         assert response.status_code == 200
+        assert response.json() == responses["clip"]["image"]
 
-    def test_clip_text_endpoint(self, deployed_app: TestClient) -> None:
+    def test_clip_text_endpoint(self, responses: dict[str, Any], deployed_app: TestClient) -> None:
         response = deployed_app.post(
-            "http://localhost:3003/sentence-transformer/encode-text",
-            json={"text": "test search query"},
+            "http://localhost:3003/predict",
+            data={
+                "modelName": "ViT-B-32::openai",
+                "modelType": "clip",
+                "text": "test search query",
+                "options": json.dumps({"mode": "text"}),
+            },
         )
         assert response.status_code == 200
+        assert response.json() == responses["clip"]["text"]
 
-    def test_face_endpoint(self, pil_image: Image.Image, deployed_app: TestClient) -> None:
+    def test_face_endpoint(self, pil_image: Image.Image, responses: dict[str, Any], deployed_app: TestClient) -> None:
         byte_image = BytesIO()
         pil_image.save(byte_image, format="jpeg")
-        headers = {"Content-Type": "image/jpg"}
+
         response = deployed_app.post(
-            "http://localhost:3003/facial-recognition/detect-faces",
-            content=byte_image.getvalue(),
-            headers=headers,
+            "http://localhost:3003/predict",
+            data={
+                "modelName": "buffalo_l",
+                "modelType": "facial-recognition",
+                "options": json.dumps({"minScore": 0.034}),
+            },
+            files={"image": byte_image.getvalue()},
         )
         assert response.status_code == 200
+        assert response.json() == responses["facial-recognition"]
+
+
+def test_sess_options() -> None:
+    sess_options = PicklableSessionOptions()
+    sess_options.intra_op_num_threads = 1
+    sess_options.inter_op_num_threads = 1
+    pickled = pickle.dumps(sess_options)
+    unpickled = pickle.loads(pickled)
+    assert unpickled.intra_op_num_threads == 1
+    assert unpickled.inter_op_num_threads == 1

+ 0 - 24
machine-learning/load_test.sh

@@ -1,24 +0,0 @@
-export MACHINE_LEARNING_CACHE_FOLDER=/tmp/model_cache
-export MACHINE_LEARNING_MIN_FACE_SCORE=0.034 # returns 1 face per request; setting this to 0 blows up the number of faces to the thousands
-export MACHINE_LEARNING_MIN_TAG_SCORE=0.0
-export PID_FILE=/tmp/locust_pid
-export LOG_FILE=/tmp/gunicorn.log
-export HEADLESS=false
-export HOST=127.0.0.1:3003
-export CONCURRENCY=4
-export NUM_ENDPOINTS=3
-export PYTHONPATH=app
-
-gunicorn app.main:app --worker-class uvicorn.workers.UvicornWorker \
-    --bind $HOST --daemon --error-logfile $LOG_FILE --pid $PID_FILE
-while true ; do
-    echo "Loading models..."
-    sleep 5
-    if cat $LOG_FILE | grep -q -E "startup complete"; then break; fi
-done
-
-# "users" are assigned only one task, so multiply concurrency by the number of tasks
-locust --host http://$HOST --web-host 127.0.0.1 \
-    --run-time 120s --users $(($CONCURRENCY * $NUM_ENDPOINTS)) $(if $HEADLESS; then echo "--headless"; fi)
-
-if [[ -e $PID_FILE ]]; then kill $(cat $PID_FILE); fi

+ 62 - 22
machine-learning/locustfile.py

@@ -1,13 +1,32 @@
 from io import BytesIO
+import json
+from typing import Any
 
 from locust import HttpUser, events, task
+from locust.env import Environment
 from PIL import Image
+from argparse import ArgumentParser
+byte_image = BytesIO()
+
+
+@events.init_command_line_parser.add_listener
+def _(parser: ArgumentParser) -> None:
+    parser.add_argument("--tag-model", type=str, default="microsoft/resnet-50")
+    parser.add_argument("--clip-model", type=str, default="ViT-B-32::openai")
+    parser.add_argument("--face-model", type=str, default="buffalo_l")
+    parser.add_argument("--tag-min-score", type=int, default=0.0, 
+                        help="Returns all tags at or above this score. The default returns all tags.")
+    parser.add_argument("--face-min-score", type=int, default=0.034, 
+                        help=("Returns all faces at or above this score. The default returns 1 face per request; "
+                              "setting this to 0 blows up the number of faces to the thousands."))
+    parser.add_argument("--image-size", type=int, default=1000)
 
 
 @events.test_start.add_listener
-def on_test_start(environment, **kwargs):
+def on_test_start(environment: Environment, **kwargs: Any) -> None:
     global byte_image
-    image = Image.new("RGB", (1000, 1000))
+    assert environment.parsed_options is not None
+    image = Image.new("RGB", (environment.parsed_options.image_size, environment.parsed_options.image_size))
     byte_image = BytesIO()
     image.save(byte_image, format="jpeg")
 
@@ -19,34 +38,55 @@ class InferenceLoadTest(HttpUser):
     headers: dict[str, str] = {"Content-Type": "image/jpg"}
 
     # re-use the image across all instances in a process
-    def on_start(self):
+    def on_start(self) -> None:
         global byte_image
         self.data = byte_image.getvalue()
 
 
-class ClassificationLoadTest(InferenceLoadTest):
+class ClassificationFormDataLoadTest(InferenceLoadTest):
+    @task
+    def classify(self) -> None:
+        data = [
+            ("modelName", self.environment.parsed_options.clip_model),
+            ("modelType", "clip"),
+            ("options", json.dumps({"minScore": self.environment.parsed_options.tag_min_score})),
+        ]
+        files = {"image": self.data}
+        self.client.post("/predict", data=data, files=files)
+
+
+class CLIPTextFormDataLoadTest(InferenceLoadTest):
     @task
-    def classify(self):
-        self.client.post(
-            "/image-classifier/tag-image", data=self.data, headers=self.headers
-        )
+    def encode_text(self) -> None:
+        data = [
+            ("modelName", self.environment.parsed_options.clip_model),
+            ("modelType", "clip"),
+            ("options", json.dumps({"mode": "text"})),
+            ("text", "test search query")
+        ]
+        self.client.post("/predict", data=data)
 
 
-class CLIPLoadTest(InferenceLoadTest):
+class CLIPVisionFormDataLoadTest(InferenceLoadTest):
     @task
-    def encode_image(self):
-        self.client.post(
-            "/sentence-transformer/encode-image",
-            data=self.data,
-            headers=self.headers,
-        )
+    def encode_image(self) -> None:
+        data = [
+            ("modelName", self.environment.parsed_options.clip_model),
+            ("modelType", "clip"),
+            ("options", json.dumps({"mode": "vision"})),
+        ]
+        files = {"image": self.data}
+        self.client.post("/predict", data=data, files=files)
 
 
-class RecognitionLoadTest(InferenceLoadTest):
+class RecognitionFormDataLoadTest(InferenceLoadTest):
     @task
-    def recognize(self):
-        self.client.post(
-            "/facial-recognition/detect-faces",
-            data=self.data,
-            headers=self.headers,
-        )
+    def recognize(self) -> None:
+        data = [
+            ("modelName", self.environment.parsed_options.face_model),
+            ("modelType", "facial-recognition"),
+            ("options", json.dumps({"minScore": self.environment.parsed_options.face_min_score})),
+        ]
+        files = {"image": self.data}
+            
+        self.client.post("/predict", data=data, files=files)

+ 17 - 0
machine-learning/log_conf.json

@@ -0,0 +1,17 @@
+{
+  "version": 1,
+  "disable_existing_loggers": true,
+  "formatters": { "rich": { "show_path": false, "omit_repeated_times": false } },
+  "handlers": {
+    "console": {
+      "class": "app.config.CustomRichHandler",
+      "formatter": "rich",
+      "level": "INFO"
+    }
+  },
+  "loggers": {
+    "gunicorn.access": { "propagate": true },
+    "gunicorn.error": { "propagate": true }
+  },
+  "root": { "handlers": ["console"] }
+}

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 579 - 274
machine-learning/poetry.lock


+ 30 - 6
machine-learning/pyproject.toml

@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "machine-learning"
-version = "1.71.0"
+version = "1.82.1"
 description = ""
 authors = ["Hau Tran <alex.tran1502@gmail.com>"]
 readme = "README.md"
@@ -13,7 +13,6 @@ torch = [
     {markers = "platform_machine == 'amd64' or platform_machine == 'x86_64'", version = "=2.0.1", source = "pytorch-cpu"}
 ]
 transformers = "^4.29.2"
-sentence-transformers = "^2.2.2"
 onnxruntime = "^1.15.0"
 insightface = "^0.7.3"
 opencv-python-headless = "^4.7.0.72"
@@ -22,17 +21,30 @@ fastapi = "^0.95.2"
 uvicorn = {extras = ["standard"], version = "^0.22.0"}
 pydantic = "^1.10.8"
 aiocache = "^0.12.1"
+optimum = "^1.9.1"
+torchvision = [
+    {markers = "platform_machine == 'arm64' or platform_machine == 'aarch64'", version = "=0.15.2", source = "pypi"},
+    {markers = "platform_machine == 'amd64' or platform_machine == 'x86_64'", version = "=0.15.2", source = "pytorch-cpu"}
+]
+rich = "^13.4.2"
+ftfy = "^6.1.1"
+setuptools = "^68.0.0"
+open-clip-torch = "^2.20.0"
+python-multipart = "^0.0.6"
+orjson = "^3.9.5"
+safetensors = "0.3.2"
+gunicorn = "^21.1.0"
 
 [tool.poetry.group.dev.dependencies]
 mypy = "^1.3.0"
 black = "^23.3.0"
 pytest = "^7.3.1"
 locust = "^2.15.1"
-gunicorn = "^20.1.0"
 httpx = "^0.24.1"
 pytest-asyncio = "^0.21.0"
 pytest-cov = "^4.1.0"
 ruff = "^0.0.272"
+pytest-mock = "^3.11.1"
 
 [[tool.poetry.source]]
 name = "pytorch-cpu"
@@ -60,10 +72,22 @@ warn_untyped_fields = true
 
 [[tool.mypy.overrides]]
 module = [
-    "transformers.pipelines",
+    "huggingface_hub",
+    "transformers",
+    "gunicorn",
     "cv2",
-    "insightface.app",
-    "sentence_transformers",
+    "insightface.model_zoo",
+    "insightface.utils.face_align",
+    "insightface.utils.storage",
+    "onnxruntime",
+    "optimum",
+    "optimum.pipelines",
+    "optimum.onnxruntime",
+    "clip_server.model.clip",
+    "clip_server.model.clip_onnx",
+    "clip_server.model.pretrained_models",
+    "clip_server.model.tokenization",
+    "torchvision.transforms",
     "aiocache.backends.memory",
     "aiocache.lock",
     "aiocache.plugins"

+ 2 - 0
machine-learning/requirements.txt

@@ -0,0 +1,2 @@
+# requirements to be installed with `--no-deps` flag
+clip-server==0.8.*

+ 1570 - 0
machine-learning/responses.json

@@ -0,0 +1,1570 @@
+{
+    "image-classification": [
+        "matchstick",
+        "nematode",
+        "nematode worm",
+        "roundworm",
+        "theater curtain",
+        "theatre curtain",
+        "spotlight",
+        "spot",
+        "digital clock"
+    ],
+    "clip": {
+        "image": [
+            -0.1503497064113617,
+            -0.26338839530944824,
+            -0.5655120611190796,
+            -0.07222450524568558,
+            0.1557869017124176,
+            -0.04308490827679634,
+            -0.3871212303638458,
+            1.2720786333084106,
+            0.28359371423721313,
+            0.2737857401371002,
+            0.5060482025146484,
+            -0.1557128131389618,
+            0.3533468246459961,
+            0.01474827527999878,
+            -0.1428389996290207,
+            0.11168161034584045,
+            0.10963180661201477,
+            0.1525699496269226,
+            0.03198016434907913,
+            0.37112998962402344,
+            0.3841392397880554,
+            0.1560487300157547,
+            -0.08011418581008911,
+            -0.0414440855383873,
+            -0.11277894675731659,
+            -0.19827546179294586,
+            -0.4201951026916504,
+            -0.047886259853839874,
+            0.35359475016593933,
+            -0.32785624265670776,
+            -0.18916064500808716,
+            0.16513843834400177,
+            -0.28110021352767944,
+            -0.02913704514503479,
+            -0.5625120401382446,
+            -0.27407437562942505,
+            0.1405377984046936,
+            -0.18804830312728882,
+            0.05606943368911743,
+            -1.8098962306976318,
+            -0.30152440071105957,
+            0.36267736554145813,
+            -0.1972821056842804,
+            0.4708936810493469,
+            -0.2627987265586853,
+            0.17974340915679932,
+            -0.23960985243320465,
+            -0.12328865379095078,
+            -0.09735478460788727,
+            -0.23208953440189362,
+            -0.11159719526767731,
+            -0.40541020035743713,
+            0.3979860842227936,
+            0.03492492437362671,
+            0.1079457551240921,
+            0.12264229357242584,
+            0.19060258567333221,
+            -0.06760812550783157,
+            -0.14953204989433289,
+            0.020657464861869812,
+            -0.045544713735580444,
+            -0.49547797441482544,
+            0.16501721739768982,
+            0.409220427274704,
+            0.025506392121315002,
+            -0.07141813635826111,
+            -0.08364877104759216,
+            -0.402360737323761,
+            0.01254647970199585,
+            -0.3270309269428253,
+            -0.5074016451835632,
+            -0.14843346178531647,
+            0.2588506042957306,
+            0.19633343815803528,
+            -0.3371100425720215,
+            -0.20139610767364502,
+            0.1256970763206482,
+            0.2077842354774475,
+            0.01762661337852478,
+            -0.09415242820978165,
+            -0.06982693821191788,
+            0.05645954608917236,
+            0.2230844497680664,
+            -0.3787960410118103,
+            0.2884414792060852,
+            0.15585792064666748,
+            0.2103164792060852,
+            0.5253552794456482,
+            0.11847981810569763,
+            -0.4132201671600342,
+            -0.340353399515152,
+            0.08250808715820312,
+            -9.104710578918457,
+            0.5367923974990845,
+            0.3082265555858612,
+            -0.054788097739219666,
+            0.38076674938201904,
+            0.10743528604507446,
+            -0.4294043183326721,
+            -0.6330970525741577,
+            -0.2239609658718109,
+            0.14761009812355042,
+            0.10569937527179718,
+            -0.03754069656133652,
+            -0.13122299313545227,
+            0.052014321088790894,
+            -1.317906141281128,
+            0.3992273807525635,
+            0.33764201402664185,
+            -0.1352805197238922,
+            -0.05533941090106964,
+            -0.12124986946582794,
+            -0.21593835949897766,
+            -0.18228507041931152,
+            -0.22175437211990356,
+            -0.3678016662597656,
+            -0.1543227732181549,
+            -0.22483907639980316,
+            -0.19485993683338165,
+            0.6772430539131165,
+            -0.38898220658302307,
+            0.05267711728811264,
+            -0.1736181378364563,
+            0.09440051019191742,
+            0.05043143033981323,
+            -0.08631592988967896,
+            -0.27284130454063416,
+            -0.06817375123500824,
+            0.17827574908733368,
+            0.23594903945922852,
+            0.0936334878206253,
+            0.2405415028333664,
+            -0.03946849703788757,
+            1.2118183374404907,
+            0.09601867198944092,
+            0.3550981879234314,
+            -0.2727612555027008,
+            -0.18505775928497314,
+            -0.09403584897518158,
+            -0.047489557415246964,
+            0.18727253377437592,
+            -0.6417330503463745,
+            0.15188869833946228,
+            0.39904549717903137,
+            -0.3706473708152771,
+            0.30436980724334717,
+            -0.20777952671051025,
+            0.21207189559936523,
+            0.15178178250789642,
+            0.08812069892883301,
+            0.07701482623815536,
+            -0.015871748328208923,
+            0.4365525543689728,
+            0.07790355384349823,
+            0.41923996806144714,
+            0.04688695818185806,
+            0.11905840039253235,
+            -0.009424615651369095,
+            -0.11237604916095734,
+            0.233009934425354,
+            -0.1393250972032547,
+            -0.13983336091041565,
+            0.0062601566314697266,
+            -0.17631873488426208,
+            0.2863244414329529,
+            0.2513056695461273,
+            -0.18979927897453308,
+            -0.1352718472480774,
+            0.20460893213748932,
+            -0.12186478078365326,
+            -0.09860621392726898,
+            -0.008292056620121002,
+            0.1744387447834015,
+            -0.04108913987874985,
+            0.21673311293125153,
+            -0.12838992476463318,
+            -0.14950336515903473,
+            -0.11025911569595337,
+            -0.050624407827854156,
+            0.09065848588943481,
+            0.12539921700954437,
+            0.06310015916824341,
+            -0.03992995619773865,
+            0.11594507843255997,
+            -0.0679289698600769,
+            -0.3390244245529175,
+            0.37619641423225403,
+            -0.08030971884727478,
+            -0.16285991668701172,
+            0.09049184620380402,
+            0.19502219557762146,
+            -0.2555782198905945,
+            -0.3483888506889343,
+            0.17943069338798523,
+            0.4208906292915344,
+            -0.21696600317955017,
+            0.09876695275306702,
+            0.24968630075454712,
+            -0.25556057691574097,
+            0.08045558631420135,
+            0.1918676197528839,
+            -0.0391634926199913,
+            0.2760481834411621,
+            -0.052914783358573914,
+            0.2551308572292328,
+            -0.17824983596801758,
+            -0.10600590705871582,
+            -0.07306042313575745,
+            0.5268251299858093,
+            0.3004921078681946,
+            0.11021347343921661,
+            0.6560711860656738,
+            0.3629128336906433,
+            -0.20159263908863068,
+            0.3353482186794281,
+            0.16813652217388153,
+            0.3684561848640442,
+            0.2572426199913025,
+            -0.1394597291946411,
+            -0.1081065982580185,
+            0.3119319677352905,
+            0.1547945886850357,
+            -0.1011475920677185,
+            0.12471853196620941,
+            0.05174969136714935,
+            0.11287996917963028,
+            0.13873223960399628,
+            -0.08381669968366623,
+            0.08320209383964539,
+            0.0005587935447692871,
+            -0.1399155706167221,
+            0.26964786648750305,
+            -0.10338637977838516,
+            -0.635022759437561,
+            0.07535472512245178,
+            0.38028770685195923,
+            -0.08784236013889313,
+            0.012145690619945526,
+            -0.4892919957637787,
+            -0.33344197273254395,
+            0.2501947581768036,
+            0.3406282663345337,
+            -0.29875123500823975,
+            0.10239893198013306,
+            -0.4501486122608185,
+            -0.5407907962799072,
+            0.02152668684720993,
+            -0.10010775923728943,
+            0.3343956470489502,
+            0.043100178241729736,
+            -0.28547170758247375,
+            -0.2451920360326767,
+            0.11399783194065094,
+            -0.06306293606758118,
+            0.0514407679438591,
+            0.1943231225013733,
+            -0.0092984139919281,
+            -0.25484439730644226,
+            -0.16108255088329315,
+            -0.07067693769931793,
+            -0.00940198078751564,
+            -0.4148368835449219,
+            -0.033262044191360474,
+            0.4516279101371765,
+            -0.24985359609127045,
+            0.20121824741363525,
+            0.4106118083000183,
+            -0.5621019005775452,
+            0.046541422605514526,
+            0.11314355581998825,
+            0.3270842730998993,
+            0.2676352858543396,
+            0.12272027134895325,
+            0.23055218160152435,
+            -0.3493749797344208,
+            0.10709720849990845,
+            0.44768550992012024,
+            -0.04293309152126312,
+            -0.03936107084155083,
+            0.2270624190568924,
+            0.16066348552703857,
+            0.35009339451789856,
+            0.04158636927604675,
+            0.03541511297225952,
+            -0.11349448561668396,
+            0.03199382871389389,
+            -0.0008175931870937347,
+            -0.317646861076355,
+            0.025708124041557312,
+            -0.3693602383136749,
+            0.5752753019332886,
+            0.34967079758644104,
+            -0.46220043301582336,
+            0.356049120426178,
+            -0.07281075417995453,
+            0.3589980900287628,
+            -0.2318757176399231,
+            -0.13105618953704834,
+            0.2065689116716385,
+            -0.06275947391986847,
+            0.2042674571275711,
+            -0.4130946397781372,
+            -0.1220964565873146,
+            0.5112582445144653,
+            -0.3460087478160858,
+            -0.8238317966461182,
+            0.14678490161895752,
+            0.04015420377254486,
+            -0.006557643413543701,
+            -0.18147070705890656,
+            -0.29465460777282715,
+            0.2962910532951355,
+            1.2113488912582397,
+            -0.14589866995811462,
+            0.01400098204612732,
+            0.5417146682739258,
+            0.24798265099525452,
+            0.07022618502378464,
+            0.16047419607639313,
+            0.19315579533576965,
+            0.046383827924728394,
+            1.2587014436721802,
+            0.13661745190620422,
+            0.15565162897109985,
+            0.12895506620407104,
+            0.257668673992157,
+            -0.2156587839126587,
+            0.24780665338039398,
+            0.1337527483701706,
+            1.296618938446045,
+            -0.012920260429382324,
+            0.21750850975513458,
+            0.020899254828691483,
+            -0.4345206022262573,
+            0.23699785768985748,
+            -0.057529620826244354,
+            -0.1588137149810791,
+            0.2551633417606354,
+            0.22220531105995178,
+            0.3276950418949127,
+            -0.2745910882949829,
+            0.03471551090478897,
+            0.3112860321998596,
+            0.2810869812965393,
+            0.2718833386898041,
+            0.01436258852481842,
+            0.21428215503692627,
+            -0.17697292566299438,
+            0.17256298661231995,
+            -0.22692933678627014,
+            0.5784951448440552,
+            -0.025022784247994423,
+            0.4769775867462158,
+            -0.08030113577842712,
+            -0.1672212779521942,
+            -0.6855964064598083,
+            -0.41609156131744385,
+            -0.26269420981407166,
+            -0.07503907382488251,
+            0.19233737885951996,
+            -0.06759896874427795,
+            -0.09529343247413635,
+            0.09245525300502777,
+            0.08412420749664307,
+            -0.606180727481842,
+            -0.42773720622062683,
+            0.17781633138656616,
+            -0.014700733125209808,
+            0.1649937480688095,
+            0.06545217335224152,
+            0.056644488126039505,
+            -0.282379686832428,
+            0.13707347214221954,
+            0.20983807742595673,
+            0.22233684360980988,
+            0.22664642333984375,
+            -0.007336974143981934,
+            -0.017350323498249054,
+            -0.5800395011901855,
+            -0.3650877773761749,
+            -0.43565335869789124,
+            -0.11023098230361938,
+            0.14169475436210632,
+            -0.2186550498008728,
+            0.08632096648216248,
+            -0.2775184214115143,
+            0.16572964191436768,
+            -0.054821647703647614,
+            0.012959688901901245,
+            0.10601806640625,
+            -0.5677923560142517,
+            -0.07860828936100006,
+            -0.11558187007904053,
+            -0.2668137848377228,
+            0.37440529465675354,
+            0.2550721764564514,
+            0.4321855306625366,
+            -0.17515484988689423,
+            -0.7251265048980713,
+            -0.09783211350440979,
+            0.03180219605565071,
+            0.017610028386116028,
+            0.4321219027042389,
+            0.10047803819179535,
+            -0.20737037062644958,
+            -0.029400426894426346,
+            0.16525790095329285,
+            0.03198770433664322,
+            0.8583027124404907,
+            -0.2614802122116089,
+            -0.33115634322166443,
+            0.10226110368967056,
+            0.12969331443309784,
+            0.0962774008512497,
+            0.1938648819923401,
+            0.019921928644180298,
+            -0.24156862497329712,
+            -0.07179004698991776,
+            0.12483114004135132,
+            -0.11993065476417542,
+            0.17780695855617523,
+            -0.3194235563278198,
+            0.2734241485595703,
+            -0.5535119771957397,
+            0.29743289947509766,
+            0.05474330484867096,
+            -0.18467886745929718,
+            0.08001690357923508,
+            -0.5903282761573792,
+            -0.0422833077609539,
+            -0.11614792048931122,
+            -0.2760723829269409,
+            0.04588329792022705,
+            0.024796903133392334,
+            0.0228412002325058,
+            -0.10622230172157288,
+            -0.0740782767534256,
+            -0.012539714574813843,
+            -0.14146174490451813,
+            -0.06914753466844559,
+            -0.007321890443563461,
+            0.35990434885025024,
+            0.173700213432312,
+            -0.1448882520198822,
+            -0.06778709590435028,
+            0.06621548533439636,
+            -0.26644161343574524,
+            -0.04929056763648987,
+            -0.12463732063770294,
+            -0.1825450360774994,
+            -0.050098665058612823,
+            -0.34152570366859436,
+            -0.04286198690533638,
+            -0.34003931283950806,
+            0.2556508779525757,
+            -0.017066851258277893,
+            -0.2992037534713745,
+            -0.49942725896835327,
+            0.18731364607810974,
+            -0.2803362011909485,
+            0.2351609766483307,
+            0.04899322986602783,
+            -0.23118168115615845,
+            -0.020409684628248215,
+            0.4137369692325592,
+            0.041882023215293884,
+            0.8543367385864258,
+            -0.0407162606716156,
+            0.055834680795669556,
+            -0.3278890550136566,
+            -0.2651934027671814,
+            0.42500773072242737,
+            -0.007330574095249176,
+            -0.34944894909858704,
+            -0.05894789099693298,
+            -0.11741754412651062,
+            0.32538557052612305,
+            -0.09227307885885239,
+            -0.2109876275062561,
+            0.32647180557250977,
+            -0.0870003029704094,
+            -0.10214601457118988,
+            0.03182033449411392,
+            0.7124476432800293,
+            -0.18646913766860962,
+            -0.30009108781814575,
+            0.6758496761322021,
+            -0.15808522701263428,
+            0.06233774125576019,
+            -0.37928515672683716,
+            0.1257198303937912,
+            0.09383836388587952,
+            0.03652769327163696,
+            0.28418007493019104,
+            0.10384587943553925,
+            -0.3028501868247986,
+            0.3513643443584442,
+            -0.16712747514247894,
+            -0.27849292755126953,
+            -0.42919832468032837,
+            -0.1720525026321411,
+            -0.18900787830352783,
+            -0.07925420254468918,
+            0.15841886401176453,
+            0.6488138437271118,
+            0.09101241081953049,
+            -0.02837720513343811,
+            -0.3493013381958008,
+            -0.13481371104717255,
+            0.7722870111465454,
+            -0.3643395006656647,
+            -0.29339152574539185
+        ],
+        "text": [
+            -0.051369935274124146,
+            -0.010725006461143494,
+            -0.11009600013494492,
+            -0.08671483397483826,
+            -0.1376112848520279,
+            0.1834997683763504,
+            -0.13518013060092926,
+            -1.2175710201263428,
+            0.21137484908103943,
+            -0.18747025728225708,
+            -0.04556228220462799,
+            0.2627124488353729,
+            0.026277093216776848,
+            0.022868335247039795,
+            0.3758852779865265,
+            -0.050838619470596313,
+            0.29562997817993164,
+            0.20151537656784058,
+            -0.02015306055545807,
+            0.17147132754325867,
+            0.14357797801494598,
+            -0.08850984275341034,
+            0.02603408694267273,
+            -0.2109367996454239,
+            -0.08127906918525696,
+            0.1460433006286621,
+            -0.1448330283164978,
+            0.19058109819889069,
+            -0.16784363985061646,
+            0.05917847529053688,
+            -0.08631782233715057,
+            -0.16270577907562256,
+            0.16088467836380005,
+            0.07714083790779114,
+            0.03789868205785751,
+            0.1956929862499237,
+            0.0517326295375824,
+            -0.005512930452823639,
+            0.047351937741041183,
+            -0.1909642219543457,
+            0.010126054286956787,
+            -0.27198368310928345,
+            -0.08384807407855988,
+            0.19902220368385315,
+            -0.03116871416568756,
+            -0.06306224316358566,
+            -0.13961419463157654,
+            0.0525326132774353,
+            -0.008902296423912048,
+            0.04049867391586304,
+            -0.0951114073395729,
+            -0.10472963750362396,
+            0.15850012004375458,
+            0.24902492761611938,
+            0.26900243759155273,
+            0.09727084636688232,
+            0.01945263147354126,
+            0.005587100982666016,
+            -0.0347808301448822,
+            0.3930657207965851,
+            0.22567930817604065,
+            -0.2227778136730194,
+            0.051797881722450256,
+            -0.14941716194152832,
+            0.06470682471990585,
+            -0.09520977735519409,
+            -0.07954283058643341,
+            0.13018378615379333,
+            0.25618842244148254,
+            -0.25645968317985535,
+            0.060703642666339874,
+            -0.14322586357593536,
+            -0.18528185784816742,
+            0.06203678250312805,
+            -0.03347042202949524,
+            -0.0051424503326416016,
+            -0.5088537335395813,
+            -0.1840534806251526,
+            0.1848325878381729,
+            0.052688274532556534,
+            -0.1965670883655548,
+            -0.20634984970092773,
+            0.04999275505542755,
+            0.2836085557937622,
+            0.07079711556434631,
+            0.44851911067962646,
+            0.1325448453426361,
+            -0.41299372911453247,
+            0.3724905252456665,
+            -0.14482314884662628,
+            -0.16094544529914856,
+            0.0793130099773407,
+            -2.0048556327819824,
+            0.3593715727329254,
+            0.0009432807564735413,
+            0.09033863246440887,
+            -0.13612447679042816,
+            0.20753170549869537,
+            0.056115202605724335,
+            -0.13530956208705902,
+            0.20019665360450745,
+            0.20947805047035217,
+            0.011267989873886108,
+            -0.09066569805145264,
+            -0.0007635504007339478,
+            0.0008731484413146973,
+            -0.44167017936706543,
+            -0.3381350040435791,
+            0.06586254388093948,
+            -0.16046567261219025,
+            0.13803477585315704,
+            0.22680139541625977,
+            0.06841648370027542,
+            -0.04588864743709564,
+            -0.15522271394729614,
+            -0.2671688497066498,
+            -0.20172488689422607,
+            -0.11681345105171204,
+            -0.2891874313354492,
+            0.10894495993852615,
+            0.016581878066062927,
+            0.08065705746412277,
+            0.03441505879163742,
+            -0.040672171860933304,
+            0.23110319674015045,
+            -0.037525858730077744,
+            -0.08831262588500977,
+            -0.08008699119091034,
+            0.09568070620298386,
+            0.18096476793289185,
+            0.06148066371679306,
+            -0.367189884185791,
+            -0.185734823346138,
+            7.826099395751953,
+            -0.13802570104599,
+            0.2800956070423126,
+            -0.22982153296470642,
+            -0.09386709332466125,
+            -0.15627573430538177,
+            0.037662118673324585,
+            -0.13656215369701385,
+            -0.07198184728622437,
+            -0.322614461183548,
+            -0.006186550483107567,
+            -0.46435683965682983,
+            0.12803569436073303,
+            -0.11408974975347519,
+            -0.006137289106845856,
+            -0.14130638539791107,
+            -0.07269007712602615,
+            -0.02471087872982025,
+            -0.01719208061695099,
+            0.04794128239154816,
+            0.015703098848462105,
+            -0.011346891522407532,
+            -0.2799241840839386,
+            0.04372157156467438,
+            0.1896992325782776,
+            -0.13235250115394592,
+            0.2291410267353058,
+            -0.1392405480146408,
+            -0.34844085574150085,
+            -0.12091708183288574,
+            -0.06026161462068558,
+            -0.013828547671437263,
+            0.11202779412269592,
+            0.277866005897522,
+            -0.4359501302242279,
+            -0.06519348919391632,
+            -0.008573257364332676,
+            -0.14178775250911713,
+            -0.11654964089393616,
+            0.27719753980636597,
+            -0.017636612057685852,
+            0.05240386724472046,
+            -0.32324519753456116,
+            0.0938805490732193,
+            0.004964321851730347,
+            0.32904016971588135,
+            0.08840127289295197,
+            -0.22231429815292358,
+            0.1611260622739792,
+            -0.25504690408706665,
+            0.3107917904853821,
+            -0.15648266673088074,
+            -0.14989043772220612,
+            0.14963313937187195,
+            0.23006673157215118,
+            0.2181217074394226,
+            -0.21481798589229584,
+            0.01277482882142067,
+            -0.5298252105712891,
+            -0.38569486141204834,
+            -0.15905818343162537,
+            0.38914966583251953,
+            -0.05173046141862869,
+            0.016740625724196434,
+            -0.016509413719177246,
+            -0.250088095664978,
+            -0.18937590718269348,
+            0.4233970642089844,
+            -0.18640199303627014,
+            -0.36222168803215027,
+            0.13211572170257568,
+            -0.12458311021327972,
+            -0.17373305559158325,
+            -0.012691006064414978,
+            0.2151109278202057,
+            0.07149481773376465,
+            -0.3380594253540039,
+            0.23961129784584045,
+            0.17417001724243164,
+            0.21425491571426392,
+            -0.3403988480567932,
+            -0.18938428163528442,
+            0.26312142610549927,
+            -0.18937340378761292,
+            -0.2695082128047943,
+            0.3464704751968384,
+            -0.026021957397460938,
+            0.16364264488220215,
+            -0.18288151919841766,
+            0.04950743168592453,
+            0.1870364248752594,
+            0.05565011501312256,
+            0.04865076020359993,
+            0.15473288297653198,
+            0.07724197208881378,
+            -0.08260702341794968,
+            -0.23326924443244934,
+            0.39012813568115234,
+            0.30722934007644653,
+            -0.2548608183860779,
+            -0.10277849435806274,
+            0.31726837158203125,
+            -0.04298326373100281,
+            -0.24891462922096252,
+            -0.041203320026397705,
+            -0.10855470597743988,
+            -0.0399850457906723,
+            0.016369149088859558,
+            0.2801763117313385,
+            0.22338557243347168,
+            -0.2106330692768097,
+            -0.19607603549957275,
+            0.40184202790260315,
+            0.013695113360881805,
+            -0.024221263825893402,
+            0.44003885984420776,
+            -0.10847768187522888,
+            -0.24203643202781677,
+            0.26467961072921753,
+            -0.05966983735561371,
+            0.12467685341835022,
+            -0.21156349778175354,
+            0.3611948788166046,
+            0.04894602298736572,
+            -0.09056273102760315,
+            0.01707540452480316,
+            0.076592355966568,
+            0.027092672884464264,
+            -0.41262251138687134,
+            0.13822166621685028,
+            0.09504467248916626,
+            0.002575445920228958,
+            -0.3847529888153076,
+            0.18197567760944366,
+            0.2379690706729889,
+            -0.05150367692112923,
+            0.2653059959411621,
+            -0.15765774250030518,
+            0.1880730241537094,
+            0.021861106157302856,
+            -0.11092785745859146,
+            0.08940362930297852,
+            0.3101727366447449,
+            0.014927387237548828,
+            -0.2911876440048218,
+            -0.16078510880470276,
+            -0.06823819875717163,
+            0.19686979055404663,
+            0.2373345047235489,
+            0.08917825669050217,
+            0.060556091368198395,
+            0.017128556966781616,
+            0.04221831634640694,
+            0.07160300761461258,
+            0.114939846098423,
+            0.07517443597316742,
+            0.1443810611963272,
+            -0.060701385140419006,
+            0.09209010750055313,
+            -0.01475987583398819,
+            -0.0017274729907512665,
+            -0.06395074725151062,
+            -0.13522477447986603,
+            -0.3005772829055786,
+            -0.25623619556427,
+            0.012192284688353539,
+            0.17816416919231415,
+            -0.08423683047294617,
+            0.1499529331922531,
+            -0.0016323104500770569,
+            -0.3038976788520813,
+            -0.011781338602304459,
+            0.17713011801242828,
+            0.2583661377429962,
+            -0.21670076251029968,
+            -0.1923849880695343,
+            -0.21187667548656464,
+            -0.027309250086545944,
+            -0.07633239030838013,
+            7.819303512573242,
+            0.16145764291286469,
+            -0.30060699582099915,
+            -0.1365492194890976,
+            -0.0063630640506744385,
+            -0.1362326741218567,
+            -0.16472479701042175,
+            0.17343278229236603,
+            0.24356240034103394,
+            0.6252191066741943,
+            -0.09942090511322021,
+            -0.18946921825408936,
+            -0.3351835608482361,
+            0.35568395256996155,
+            0.11762797087430954,
+            -0.3656516671180725,
+            0.19118718802928925,
+            -3.4465088844299316,
+            0.22562159597873688,
+            -0.2150695025920868,
+            -0.06499379128217697,
+            0.046910446137189865,
+            -0.05743652582168579,
+            -0.33147257566452026,
+            0.2564307153224945,
+            0.09259609133005142,
+            -0.08864793181419373,
+            -0.11425864696502686,
+            -0.19367149472236633,
+            -0.06630873680114746,
+            -0.007083814591169357,
+            -0.05304165184497833,
+            0.2017112821340561,
+            0.10328061133623123,
+            -0.04912707582116127,
+            -0.060788594186306,
+            0.0784585103392601,
+            0.28052055835723877,
+            -0.33164721727371216,
+            0.017689572647213936,
+            -0.001561986282467842,
+            0.39189884066581726,
+            -0.12148138135671616,
+            0.1046065166592598,
+            0.021570973098278046,
+            -0.012959085404872894,
+            0.2276451140642166,
+            -0.15909601747989655,
+            0.09318748116493225,
+            0.03624418377876282,
+            0.41286107897758484,
+            0.4439883232116699,
+            -0.574947714805603,
+            0.2063872516155243,
+            -0.11515115946531296,
+            0.15398114919662476,
+            -0.10527925938367844,
+            0.08131930232048035,
+            -0.10869307070970535,
+            -0.012484684586524963,
+            0.12625205516815186,
+            0.2636316418647766,
+            -0.07193168997764587,
+            0.08365315198898315,
+            0.07778038084506989,
+            -0.08492550998926163,
+            -0.31494593620300293,
+            -0.30747660994529724,
+            -0.12434972077608109,
+            -0.14759579300880432,
+            -0.0856187641620636,
+            -0.015103459358215332,
+            0.21642933785915375,
+            0.24999216198921204,
+            -0.25810444355010986,
+            -0.10437635332345963,
+            -0.09068337082862854,
+            0.015814702957868576,
+            -0.13024312257766724,
+            -0.031961895525455475,
+            -0.050593845546245575,
+            0.47274336218833923,
+            -0.24634651839733124,
+            0.2797456383705139,
+            -0.2669164538383484,
+            -0.18765005469322205,
+            -0.12668517231941223,
+            -0.07038992643356323,
+            -0.15561681985855103,
+            0.16797593235969543,
+            0.08408385515213013,
+            0.05721508711576462,
+            0.055883586406707764,
+            0.07930487394332886,
+            0.06364040076732635,
+            0.2569333016872406,
+            0.14447829127311707,
+            -0.051580414175987244,
+            0.06861788034439087,
+            0.02721872180700302,
+            0.06561324000358582,
+            0.12471484392881393,
+            0.1832738071680069,
+            -0.16585436463356018,
+            -0.08117838203907013,
+            -0.06088650971651077,
+            -0.2081316113471985,
+            0.1322343945503235,
+            0.01742127537727356,
+            -0.17162840068340302,
+            0.0042439959943294525,
+            -0.13668203353881836,
+            -0.07343199849128723,
+            0.2740647494792938,
+            0.1601387858390808,
+            -0.12330605089664459,
+            -0.3267219662666321,
+            -0.3382628858089447,
+            0.3690100908279419,
+            0.18473923206329346,
+            -0.17084713280200958,
+            -0.05962555855512619,
+            -0.0792207270860672,
+            -0.06036840379238129,
+            -0.3774709701538086,
+            -0.05851760879158974,
+            0.3206423819065094,
+            0.0651409924030304,
+            -0.058551669120788574,
+            -0.36781901121139526,
+            -0.24176156520843506,
+            -0.4543174207210541,
+            -0.08731119334697723,
+            -0.045513980090618134,
+            -0.01741885021328926,
+            0.034853145480155945,
+            -0.03033122420310974,
+            -0.5135809183120728,
+            -0.3117552399635315,
+            0.11883510649204254,
+            0.03182804584503174,
+            0.304332971572876,
+            -0.018284954130649567,
+            0.00013580918312072754,
+            -0.30424895882606506,
+            0.14760465919971466,
+            -0.21293771266937256,
+            0.23776817321777344,
+            -0.24130550026893616,
+            0.05505291372537613,
+            0.0050969719886779785,
+            -0.02879290282726288,
+            0.030616000294685364,
+            -0.05236539989709854,
+            0.0355415940284729,
+            -0.10839346051216125,
+            -0.15302366018295288,
+            0.17155241966247559,
+            0.024070631712675095,
+            0.09996841847896576,
+            0.3937240242958069,
+            0.11562097072601318,
+            -0.03887242078781128,
+            0.08371567726135254,
+            -0.034323759377002716,
+            -0.15314003825187683,
+            0.21605932712554932,
+            0.2662332057952881,
+            0.01804041489958763,
+            -1.175376534461975,
+            -0.04393252357840538,
+            0.1724747121334076,
+            0.17443567514419556,
+            -0.03206155076622963,
+            -0.023589134216308594,
+            0.0018865615129470825,
+            0.195848286151886,
+            0.10615190118551254,
+            -0.04240237921476364,
+            -0.1930321455001831,
+            -0.4515952467918396,
+            0.06612616777420044,
+            -0.23157468438148499,
+            -0.5452786087989807,
+            0.20692341029644012,
+            -0.0644945502281189,
+            0.29818546772003174,
+            0.4354862868785858,
+            0.05778267979621887,
+            -0.0014227330684661865,
+            0.07666969299316406,
+            0.026816070079803467,
+            0.24234911799430847,
+            0.24426233768463135,
+            -0.06976135820150375,
+            -0.002162843942642212,
+            0.040565088391304016,
+            0.12837275862693787,
+            -0.28299012780189514,
+            0.33027389645576477
+        ]
+    },
+    "facial-recognition": [
+        {
+            "imageWidth": 600,
+            "imageHeight": 800,
+            "boundingBox": {
+                "x1": 690.0,
+                "y1": -89.0,
+                "x2": 833.0,
+                "y2": 96.0
+            },
+            "score": 0.03575617074966431,
+            "embedding": [
+                -0.43665632605552673,
+                -0.5930537581443787,
+                -0.12699729204177856,
+                0.3985028862953186,
+                0.18789690732955933,
+                -0.25987985730171204,
+                0.14818175137043,
+                -0.5422291159629822,
+                -0.0671021044254303,
+                -0.1319030374288559,
+                0.056408628821372986,
+                0.046094197779893875,
+                -0.14984919130802155,
+                0.04322558641433716,
+                0.023826055228710175,
+                -0.09063439071178436,
+                0.07891753315925598,
+                -0.2935708165168762,
+                -0.6277135014533997,
+                -0.2904231548309326,
+                0.18039005994796753,
+                0.21837681531906128,
+                0.17909450829029083,
+                -0.04030478745698929,
+                -0.03556056320667267,
+                -0.07568575441837311,
+                0.12771207094192505,
+                -0.13466131687164307,
+                -0.23686951398849487,
+                0.36429697275161743,
+                0.2955845892429352,
+                0.2086743414402008,
+                0.11252538859844208,
+                0.4769151210784912,
+                -0.05477480590343475,
+                0.030100278556346893,
+                -0.049531325697898865,
+                0.040458545088768005,
+                0.23517772555351257,
+                0.17130395770072937,
+                0.17269372940063477,
+                0.08591301739215851,
+                0.046999648213386536,
+                -0.17151862382888794,
+                -0.24437746405601501,
+                0.31105315685272217,
+                -0.23971444368362427,
+                -0.3174452781677246,
+                -0.026422448456287384,
+                -0.26203349232673645,
+                -0.1855347454547882,
+                -0.3104425370693207,
+                0.6385250091552734,
+                0.2749706506729126,
+                0.006675023585557938,
+                0.05378580465912819,
+                -0.20257888734340668,
+                -0.4839984178543091,
+                0.2170865386724472,
+                -0.4781228303909302,
+                -0.12367318570613861,
+                -0.09901124238967896,
+                0.1863373965024948,
+                0.3114345669746399,
+                -0.12165745347738266,
+                0.13010038435459137,
+                0.1253461092710495,
+                0.10728863626718521,
+                0.3747178912162781,
+                -0.12302650511264801,
+                -0.1263274848461151,
+                -0.1562153398990631,
+                0.260276198387146,
+                0.15841349959373474,
+                0.5164251327514648,
+                -0.31015825271606445,
+                0.24754373729228973,
+                0.10240863263607025,
+                -0.11818322539329529,
+                -0.14073267579078674,
+                0.027111530303955078,
+                0.09927573800086975,
+                -0.10066951811313629,
+                0.4808421730995178,
+                -0.042361728847026825,
+                -0.08512216061353683,
+                0.1369529813528061,
+                0.3037898540496826,
+                0.11138055473566055,
+                -0.3182139992713928,
+                -0.5708587169647217,
+                -0.14786981046199799,
+                0.49985525012016296,
+                -0.23231984674930573,
+                0.13856683671474457,
+                -0.5383139848709106,
+                -0.05995427444577217,
+                0.2796868085861206,
+                -0.3244798481464386,
+                0.16510958969593048,
+                0.5714607834815979,
+                -0.1512063443660736,
+                0.20110568404197693,
+                -0.49805426597595215,
+                -0.20088790357112885,
+                -0.046678103506565094,
+                0.2465328425168991,
+                0.02250899374485016,
+                -0.1409173160791397,
+                0.3807566463947296,
+                0.3381146490573883,
+                0.05011143907904625,
+                -0.23718394339084625,
+                -0.20052078366279602,
+                -0.1408103108406067,
+                -0.34221047163009644,
+                0.11998140811920166,
+                0.24424004554748535,
+                0.1376989781856537,
+                -0.25339990854263306,
+                -0.4108094573020935,
+                -0.28673601150512695,
+                -0.20673272013664246,
+                0.46043485403060913,
+                0.4178845286369324,
+                0.10520245134830475,
+                -0.14469142258167267,
+                0.08073662221431732,
+                -0.3737245798110962,
+                0.13030850887298584,
+                -0.08456054329872131,
+                0.21937909722328186,
+                -0.2270081490278244,
+                0.3039504289627075,
+                0.009785190224647522,
+                -0.07245694845914841,
+                0.5029141306877136,
+                -0.24968916177749634,
+                0.31788119673728943,
+                0.12665590643882751,
+                -0.0364842563867569,
+                0.21702805161476135,
+                -0.09277956187725067,
+                0.17766769230365753,
+                -0.1201881617307663,
+                0.008044496178627014,
+                -0.26986125111579895,
+                0.29888248443603516,
+                -0.2848595380783081,
+                0.30066442489624023,
+                -0.14317002892494202,
+                0.5380052328109741,
+                0.03084031492471695,
+                0.023038823157548904,
+                0.7386217713356018,
+                0.003468744456768036,
+                0.23797431588172913,
+                -0.11183349043130875,
+                0.0678468644618988,
+                -0.23546601831912994,
+                0.3935474753379822,
+                0.005377739667892456,
+                0.13494043052196503,
+                0.1370638608932495,
+                -0.02944491058588028,
+                0.14705342054367065,
+                -0.4812065362930298,
+                0.27262356877326965,
+                -0.05196662247180939,
+                -0.3097267150878906,
+                0.08714988827705383,
+                0.10841232538223267,
+                -0.11757145822048187,
+                -0.5010467767715454,
+                -0.32369980216026306,
+                -0.21964779496192932,
+                -0.19810467958450317,
+                0.14780977368354797,
+                -0.04624304920434952,
+                0.24638010561466217,
+                -0.0671030580997467,
+                -0.31719157099723816,
+                0.269559383392334,
+                0.37117093801498413,
+                -0.3964727520942688,
+                0.21541666984558105,
+                -0.12243526428937912,
+                -0.5392556190490723,
+                0.0464024543762207,
+                0.3657010495662689,
+                -0.042127206921577454,
+                -0.03063909336924553,
+                0.2190942019224167,
+                0.16005609929561615,
+                -0.03320079296827316,
+                -0.0949995294213295,
+                0.33176088333129883,
+                0.2253836989402771,
+                -0.016216054558753967,
+                -0.4241701662540436,
+                0.5294063091278076,
+                -0.011592432856559753,
+                -0.21875163912773132,
+                -0.06394624710083008,
+                0.24449443817138672,
+                -0.056584421545267105,
+                -0.09727923572063446,
+                -0.3978732228279114,
+                -0.11175088584423065,
+                0.08514271676540375,
+                -0.05761905014514923,
+                -0.049855880439281464,
+                0.17287252843379974,
+                0.41813868284225464,
+                -0.3043341636657715,
+                0.308758020401001,
+                -0.6604494452476501,
+                -0.13869403302669525,
+                0.07291632890701294,
+                -0.0432523638010025,
+                0.3740164041519165,
+                0.17014223337173462,
+                -0.2646957337856293,
+                -0.346534788608551,
+                0.13010692596435547,
+                0.21517504751682281,
+                0.740301251411438,
+                0.3460628092288971,
+                -0.5115481615066528,
+                0.46967509388923645,
+                -0.00984795019030571,
+                -0.13301578164100647,
+                -0.006184384226799011,
+                0.013667777180671692,
+                0.1699303388595581,
+                -0.3161454498767853,
+                0.29015013575553894,
+                0.6519798040390015,
+                0.13776443898677826,
+                0.5275151133537292,
+                0.14721794426441193,
+                -0.11468257009983063,
+                -0.05685025453567505,
+                0.21696926653385162,
+                -0.34107062220573425,
+                0.0935278832912445,
+                -0.039688196033239365,
+                -0.13109605014324188,
+                0.07406829297542572,
+                0.1509123593568802,
+                0.18835929036140442,
+                0.19146737456321716,
+                -0.38988304138183594,
+                0.4697469472885132,
+                -0.11145250499248505,
+                0.039728209376335144,
+                0.8268787264823914,
+                -0.09761662781238556,
+                -0.04332102835178375,
+                0.2700135111808777,
+                0.1207934319972992,
+                0.05877719447016716,
+                0.028245486319065094,
+                0.20692101120948792,
+                0.6844056844711304,
+                -0.3498411178588867,
+                -0.11976329982280731,
+                -0.396377295255661,
+                0.23799002170562744,
+                0.05757361650466919,
+                0.07855354994535446,
+                0.3798258602619171,
+                -0.036588408052921295,
+                0.06831938028335571,
+                0.10845135152339935,
+                -0.1865023374557495,
+                0.0892765000462532,
+                -0.27789002656936646,
+                0.31810519099235535,
+                0.4251457452774048,
+                -0.035256966948509216,
+                -0.2807217240333557,
+                0.07315991818904877,
+                0.13499341905117035,
+                -0.11333761364221573,
+                -0.0008842200040817261,
+                0.10874118655920029,
+                0.296818345785141,
+                0.008288972079753876,
+                0.24116197228431702,
+                0.01130960788577795,
+                -0.30095404386520386,
+                -0.4752867817878723,
+                0.1992175281047821,
+                -0.16108214855194092,
+                0.01783856749534607,
+                0.5126014947891235,
+                -0.08679923415184021,
+                0.3416588008403778,
+                0.3235914707183838,
+                0.2577085494995117,
+                0.2144274115562439,
+                -0.1597137153148651,
+                -0.26682955026626587,
+                0.22788375616073608,
+                -0.38956791162490845,
+                0.08458005636930466,
+                -0.15929272770881653,
+                0.2421140819787979,
+                -0.24793750047683716,
+                -0.3152828514575958,
+                0.15945720672607422,
+                -0.16866135597229004,
+                0.19472730159759521,
+                0.40839430689811707,
+                0.24238601326942444,
+                -0.2364349514245987,
+                0.2985266149044037,
+                0.12915723025798798,
+                0.32706815004348755,
+                0.5018091201782227,
+                -0.4053831696510315,
+                -0.023235589265823364,
+                -0.11315590143203735,
+                0.007632426917552948,
+                -0.22626228630542755,
+                0.28817909955978394,
+                -0.5816531181335449,
+                0.15515218675136566,
+                -0.016097508370876312,
+                -0.016345905140042305,
+                0.09585585445165634,
+                -0.010664891451597214,
+                0.1402921974658966,
+                -0.22450345754623413,
+                -0.1396103948354721,
+                -0.4073222875595093,
+                -0.2477686107158661,
+                0.12040270864963531,
+                -0.06779105961322784,
+                0.44510531425476074,
+                0.33206677436828613,
+                0.1980731040239334,
+                -0.06460791826248169,
+                -0.25242677330970764,
+                -0.1272629350423813,
+                0.4465603232383728,
+                -0.09844805300235748,
+                -0.18762269616127014,
+                0.16189783811569214,
+                0.23589575290679932,
+                -0.4479854106903076,
+                0.21351021528244019,
+                -0.33205288648605347,
+                0.28407710790634155,
+                -0.09519840776920319,
+                0.03558272495865822,
+                -0.5180788636207581,
+                -0.273823618888855,
+                0.03172869607806206,
+                0.22928576171398163,
+                0.4715774655342102,
+                -0.48383235931396484,
+                0.01422564685344696,
+                -0.0810236856341362,
+                0.1938459277153015,
+                -0.060681067407131195,
+                -0.03779950737953186,
+                -0.2875831723213196,
+                0.024652402848005295,
+                0.052711859345436096,
+                -0.22610321640968323,
+                0.46830445528030396,
+                0.2961697578430176,
+                0.14641568064689636,
+                -0.24234764277935028,
+                0.30126383900642395,
+                -0.011164642870426178,
+                0.38622337579727173,
+                -0.124844990670681,
+                0.3365071415901184,
+                0.1739971935749054,
+                -0.27030548453330994,
+                0.36919164657592773,
+                0.2617016136646271,
+                -0.15373270213603973,
+                -0.4315706789493561,
+                0.35697025060653687,
+                0.04389272257685661,
+                -0.0654754787683487,
+                0.5542906522750854,
+                0.01996980607509613,
+                0.43128183484077454,
+                -0.014291856437921524,
+                -0.33983248472213745,
+                0.3250855803489685,
+                0.21585243940353394,
+                -0.3445807695388794,
+                0.23752379417419434,
+                0.18115344643592834,
+                -0.25867414474487305,
+                -0.16033515334129333,
+                -0.16151021420955658,
+                -0.2330635040998459,
+                0.14865264296531677,
+                -0.3179035186767578,
+                0.2721554934978485,
+                -0.05992010980844498,
+                0.161936953663826,
+                0.07594355195760727,
+                -0.16281592845916748,
+                0.44893062114715576,
+                -0.4305216670036316,
+                -0.038787614554166794,
+                -0.11722588539123535,
+                0.07254093140363693,
+                -0.29970499873161316,
+                0.1654062271118164,
+                -0.15089675784111023,
+                0.12507867813110352,
+                0.4372529983520508,
+                0.13540124893188477,
+                0.1339181661605835,
+                0.013776864856481552,
+                0.2695162892341614,
+                -0.29998600482940674,
+                -0.08645695447921753,
+                0.12768305838108063,
+                0.23375648260116577,
+                -0.07325033098459244,
+                -0.0443335622549057,
+                0.047095995396375656,
+                0.09582670032978058,
+                0.2350919246673584,
+                0.18061956763267517,
+                0.3537953197956085,
+                0.1293836236000061,
+                0.33010751008987427,
+                0.18966645002365112,
+                0.07585176825523376,
+                0.005968615412712097,
+                -0.13233722746372223,
+                0.1710568368434906,
+                -0.020040705800056458,
+                -0.2805648446083069,
+                -0.09103457629680634,
+                0.19508640468120575,
+                -0.21115663647651672,
+                -0.1624927520751953,
+                0.07147711515426636,
+                -0.2013818919658661,
+                0.15193939208984375,
+                0.041464678943157196,
+                0.010748650878667831,
+                0.02909122407436371,
+                -0.22078242897987366,
+                0.06446809321641922,
+                -0.2740311324596405,
+                -0.5190434455871582,
+                -0.20539811253547668,
+                0.17622530460357666,
+                -0.28688907623291016,
+                0.03056890144944191,
+                0.29645925760269165,
+                -0.08893127739429474,
+                -0.4425870180130005,
+                0.09070369601249695,
+                0.08005643635988235,
+                0.009866252541542053,
+                -0.07386989891529083,
+                0.0668322741985321,
+                -0.34370890259742737,
+                0.23668520152568817,
+                -0.08478264510631561,
+                -0.2740020751953125,
+                -0.3166845142841339,
+                -0.11662208288908005,
+                0.20027956366539001,
+                0.3377249538898468,
+                -0.30414462089538574,
+                -0.6180194616317749,
+                0.0430230051279068,
+                -0.24733665585517883,
+                -0.20657919347286224,
+                -0.37058353424072266,
+                0.0064486004412174225,
+                0.2548515200614929,
+                0.029220985248684883,
+                -0.4174918234348297,
+                0.06511752307415009,
+                -0.37452077865600586,
+                0.2269931435585022,
+                0.22139698266983032,
+                0.28097647428512573,
+                0.10008563101291656,
+                -0.03995315730571747,
+                -0.3350542485713959,
+                0.28511708974838257,
+                0.18131467700004578,
+                -0.8796138763427734,
+                -0.04131913185119629,
+                -0.6237051486968994,
+                0.0517050176858902,
+                0.2354174554347992,
+                -0.0033704787492752075,
+                0.15842004120349884,
+                0.02000284567475319,
+                -0.22027355432510376,
+                -0.2730841040611267,
+                -0.23035141825675964,
+                -0.07705625146627426,
+                0.002099640667438507
+            ]
+        }
+    ]
+}

+ 15 - 0
machine-learning/start.sh

@@ -0,0 +1,15 @@
+#!/usr/bin/env sh
+
+export LD_PRELOAD="/usr/lib/$(arch)-linux-gnu/libmimalloc.so.2"
+
+: "${MACHINE_LEARNING_HOST:=0.0.0.0}"
+: "${MACHINE_LEARNING_PORT:=3003}"
+: "${MACHINE_LEARNING_WORKERS:=1}"
+: "${MACHINE_LEARNING_WORKER_TIMEOUT:=120}"
+
+gunicorn app.main:app \
+	-k uvicorn.workers.UvicornWorker \
+	-w $MACHINE_LEARNING_WORKERS \
+	-b $MACHINE_LEARNING_HOST:$MACHINE_LEARNING_PORT \
+	-t $MACHINE_LEARNING_WORKER_TIMEOUT \
+	--log-config-json log_conf.json

+ 1 - 1
mobile/.fvm/fvm_config.json

@@ -1,4 +1,4 @@
 {
-  "flutterSdkVersion": "3.10.5",
+  "flutterSdkVersion": "3.13.0",
   "flavors": {}
 }

+ 11 - 0
mobile/.vscode/settings.json

@@ -0,0 +1,11 @@
+{
+  "dart.flutterSdkPath": ".fvm/flutter_sdk",
+  // Remove .fvm files from search
+  "search.exclude": {
+    "**/.fvm": true
+  },
+  // Remove from file watching
+  "files.watcherExclude": {
+    "**/.fvm": true
+  }
+}

+ 6 - 1
mobile/android/app/build.gradle

@@ -52,7 +52,7 @@ android {
     defaultConfig {
         // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
         applicationId "app.alextran.immich"
-        minSdkVersion 23
+        minSdkVersion 26
         targetSdkVersion 33
         versionCode flutterVersionCode.toInteger()
         versionName flutterVersionName
@@ -96,3 +96,8 @@ dependencies {
     implementation "com.github.bumptech.glide:glide:$glide_version"
     kapt "com.github.bumptech.glide:compiler:$glide_version"
 }
+
+// This is uncommented in F-Droid build script
+//f configurations.all {
+//f     exclude group: 'com.google.android.gms'
+//f }

+ 10 - 2
mobile/android/app/src/main/AndroidManifest.xml

@@ -7,6 +7,10 @@
       android:name="io.flutter.embedding.android.EnableImpeller"
       android:value="false" />
 
+    <meta-data
+      android:name="com.google.firebase.messaging.default_notification_icon"
+      android:resource="@drawable/notification_icon" />
+
     <activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop"
       android:theme="@style/LaunchTheme"
       android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
@@ -51,8 +55,7 @@
 
 
   <uses-permission android:name="android.permission.INTERNET" />
-  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
-  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
   <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
   <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
   <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
@@ -60,11 +63,16 @@
   <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
   <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
   <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
 
   <queries>
     <intent>
       <action android:name="android.intent.action.VIEW" />
       <data android:scheme="https" />
     </intent>
+    <intent>
+      <action android:name="android.intent.action.VIEW" />
+      <data android:scheme="geo" />
+    </intent>
   </queries>
 </manifest>

BIN
mobile/android/app/src/main/res/drawable-hdpi/notification_icon.png


BIN
mobile/android/app/src/main/res/drawable-mdpi/notification_icon.png


BIN
mobile/android/app/src/main/res/drawable-xhdpi/notification_icon.png


BIN
mobile/android/app/src/main/res/drawable-xxhdpi/notification_icon.png


BIN
mobile/android/app/src/main/res/drawable-xxxhdpi/notification_icon.png


Vissa filer visades inte eftersom för många filer har ändrats