Compare commits
493 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf4ec06750 | ||
|
|
e8712e6694 | ||
|
|
ce5966c23d | ||
|
|
68f6446718 | ||
|
|
197f336b5f | ||
|
|
cd375a976e | ||
|
|
088d5addf2 | ||
|
|
2377df9dae | ||
|
|
ad5ba82f50 | ||
|
|
b6f18cbe81 | ||
|
|
87a0ba3db3 | ||
|
|
3212a47720 | ||
|
|
431536cdbb | ||
|
|
9a60578088 | ||
|
|
8dcd159bd6 | ||
|
|
2f87463170 | ||
|
|
9f56bf0ab9 | ||
|
|
603b056512 | ||
|
|
ce04e9e07a | ||
|
|
c54a188154 | ||
|
|
c77ba46d60 | ||
|
|
cc3149c520 | ||
|
|
512f672e9e | ||
|
|
b117985f66 | ||
|
|
b92a2b2a56 | ||
|
|
a6f39bc74f | ||
|
|
daad02504f | ||
|
|
8a6889529c | ||
|
|
b34cbd881a | ||
|
|
f6eaaab725 | ||
|
|
2a2c74e081 | ||
|
|
c653e0f261 | ||
|
|
f0dd1d715a | ||
|
|
d98a2a5f79 | ||
|
|
275717b8e3 | ||
|
|
51dc197b33 | ||
|
|
a42c95a781 | ||
|
|
8b5b6d0821 | ||
|
|
72dcde9e0f | ||
|
|
a08a687951 | ||
|
|
7ff68223ab | ||
|
|
c76c1d6bf8 | ||
|
|
0167407370 | ||
|
|
b49b10141e | ||
|
|
cb0e37e76e | ||
|
|
237d1c1bf4 | ||
|
|
cf71a41bae | ||
|
|
52e09b4857 | ||
|
|
aefd052888 | ||
|
|
e47a11b8ba | ||
|
|
2ad389f64e | ||
|
|
d5e19e45cd | ||
|
|
4a5654a247 | ||
|
|
d4c60eab0d | ||
|
|
0fb1d33f17 | ||
|
|
3021eca8e5 | ||
|
|
5921ec9a58 | ||
|
|
3e3598fd92 | ||
|
|
1aae29a0b8 | ||
|
|
99c6f8fb13 | ||
|
|
d4c23c8df8 | ||
|
|
62a11283af | ||
|
|
28d35bf04e | ||
|
|
dd52ff2d33 | ||
|
|
093347c7ab | ||
|
|
755649a3c8 | ||
|
|
6b25435b4f | ||
|
|
2288b022bc | ||
|
|
64e4ae7e4b | ||
|
|
c6b4bc883b | ||
|
|
50bc92aac0 | ||
|
|
36b3521be8 | ||
|
|
b05132a01a | ||
|
|
9b418642a6 | ||
|
|
013da0aa3d | ||
|
|
8dcc01b2be | ||
|
|
cf08ac7538 | ||
|
|
5ead4af2dc | ||
|
|
f2c20f60f7 | ||
|
|
e0fc6b753c | ||
|
|
ab3f82cfe4 | ||
|
|
383f11019a | ||
|
|
250f7fc55c | ||
|
|
22172a680b | ||
|
|
5156d76194 | ||
|
|
decfb9687b | ||
|
|
5e17b3199f | ||
|
|
cfec6a8fdb | ||
|
|
2ec63f7914 | ||
|
|
29182cfc9a | ||
|
|
5a7ef02387 | ||
|
|
4b59f83288 | ||
|
|
31987bc043 | ||
|
|
23f0eb6fe8 | ||
|
|
0994575bf3 | ||
|
|
f4a12acd29 | ||
|
|
335216f6dd | ||
|
|
5a9acbc05b | ||
|
|
219f99e516 | ||
|
|
1890c0ab6b | ||
|
|
a78e08bac1 | ||
|
|
634169235a | ||
|
|
45ffa65173 | ||
|
|
62cb14e4b6 | ||
|
|
3d7e9b7184 | ||
|
|
d2807b8d6a | ||
|
|
ed386dd12a | ||
|
|
dadcf49eca | ||
|
|
4a9f58bf9b | ||
|
|
9d225d3d06 | ||
|
|
268a9c4803 | ||
|
|
bddeb03fd5 | ||
|
|
f0bb50b61a | ||
|
|
7e9fc4aa97 | ||
|
|
e57c926676 | ||
|
|
f3b17d8f73 | ||
|
|
41af76bbe2 | ||
|
|
9af5e7838f | ||
|
|
5dacea6f74 | ||
|
|
18fcca2884 | ||
|
|
8222327299 | ||
|
|
eebe9bcd5f | ||
|
|
41befc0948 | ||
|
|
09bf1c9175 | ||
|
|
332a8d80f2 | ||
|
|
56eb7bf0fc | ||
|
|
99e9c2ada6 | ||
|
|
d8ecefaea5 | ||
|
|
b8d6cc1e09 | ||
|
|
f36c40bc6b | ||
|
|
83b63ca12e | ||
|
|
f57acc0802 | ||
|
|
29981b1088 | ||
|
|
43f4dac3ad | ||
|
|
2370c9ef41 | ||
|
|
ebb50476ac | ||
|
|
2ea080cacd | ||
|
|
b56f22aac3 | ||
|
|
9033e7f179 | ||
|
|
9070a361bc | ||
|
|
d8e66acd02 | ||
|
|
687d896c63 | ||
|
|
0243570c0b | ||
|
|
66ccf298ba | ||
|
|
982dcd7b8d | ||
|
|
c68702c0a7 | ||
|
|
98a7412855 | ||
|
|
104880a729 | ||
|
|
c48d4f01dc | ||
|
|
8d5bf93360 | ||
|
|
2f9d0a2404 | ||
|
|
36b21948bf | ||
|
|
4dffae3f39 | ||
|
|
35fa6397ea | ||
|
|
4a8887f37b | ||
|
|
fc93762230 | ||
|
|
ebd3f7f125 | ||
|
|
81009c17bf | ||
|
|
beb92e8ffb | ||
|
|
7b4e36e990 | ||
|
|
192e950567 | ||
|
|
126dd45751 | ||
|
|
ff331ffad9 | ||
|
|
e571880c16 | ||
|
|
e5b4d09827 | ||
|
|
81d51fbd7e | ||
|
|
02f9b40d67 | ||
|
|
260a600bbc | ||
|
|
818005fcb5 | ||
|
|
e5f704cf3b | ||
|
|
e2f1e38472 | ||
|
|
b3c82d5ba2 | ||
|
|
6d1868a6e0 | ||
|
|
98db9331d8 | ||
|
|
66e860a08e | ||
|
|
3172c341e0 | ||
|
|
8234234c48 | ||
|
|
8d5e782fc4 | ||
|
|
10d10d9021 | ||
|
|
68d6d89a3b | ||
|
|
3e73cfb71a | ||
|
|
d7e970dcea | ||
|
|
fb7249d1f6 | ||
|
|
521436dd21 | ||
|
|
c145963b02 | ||
|
|
098ab9eae5 | ||
|
|
a937efe719 | ||
|
|
b7fcec7ce3 | ||
|
|
69c23aa3ec | ||
|
|
0a22e64799 | ||
|
|
c3d6d69262 | ||
|
|
7cb78ed972 | ||
|
|
cc70f5f6a0 | ||
|
|
85efbc6984 | ||
|
|
3a44e8f8d3 | ||
|
|
9bada51d56 | ||
|
|
7bc6e9ef64 | ||
|
|
ea797c1723 | ||
|
|
f63d6d5b67 | ||
|
|
8873c9a02f | ||
|
|
ee0e131efa | ||
|
|
af5a9d9108 | ||
|
|
56cf9464af | ||
|
|
9676412875 | ||
|
|
54bea23485 | ||
|
|
3053cbd4c8 | ||
|
|
07069c3b1e | ||
|
|
c0ce81ca0e | ||
|
|
3bef456923 | ||
|
|
91e2348381 | ||
|
|
1564ed3256 | ||
|
|
b8fec26115 | ||
|
|
dd86aa9259 | ||
|
|
84e4c15ed5 | ||
|
|
25d1b3e1b1 | ||
|
|
0e63efb490 | ||
|
|
014d164d99 | ||
|
|
fc64be6603 | ||
|
|
9a7e48eaa6 | ||
|
|
e050121dbf | ||
|
|
f0a5d39625 | ||
|
|
86f5ceb80e | ||
|
|
06959a9ea5 | ||
|
|
acdc66413c | ||
|
|
816db700e1 | ||
|
|
9030b1f89f | ||
|
|
2e0c7abd65 | ||
|
|
1a633f3fca | ||
|
|
dda735ec51 | ||
|
|
f1c98ac9e6 | ||
|
|
7d07aaeba3 | ||
|
|
a0163d8df0 | ||
|
|
49ef86173f | ||
|
|
b6c6a7e403 | ||
|
|
672560f55b | ||
|
|
94cbbf3c4b | ||
|
|
40b802a5a9 | ||
|
|
a63f027bf7 | ||
|
|
1c02e1dadf | ||
|
|
63b6a71ebd | ||
|
|
0a9b632e48 | ||
|
|
7fcc5a5417 | ||
|
|
9cec6aaf46 | ||
|
|
a3206bf950 | ||
|
|
9c627920dd | ||
|
|
8a421eb778 | ||
|
|
14e681c954 | ||
|
|
095a5e0ffb | ||
|
|
b1d31a4567 | ||
|
|
b42ca61e1f | ||
|
|
197baf3473 | ||
|
|
3161eb7d16 | ||
|
|
bbbdd463fd | ||
|
|
73ad0d468f | ||
|
|
74d34b4f6c | ||
|
|
bf3b38a7f2 | ||
|
|
52d0c5fc73 | ||
|
|
fb20381f98 | ||
|
|
a678590ccd | ||
|
|
bd226e9e2c | ||
|
|
d023d5b6b4 | ||
|
|
9b30640e67 | ||
|
|
d17b24eea3 | ||
|
|
d38d0b8de0 | ||
|
|
f10b74f1e2 | ||
|
|
5c63d8f07a | ||
|
|
cb437829f3 | ||
|
|
7173af60e4 | ||
|
|
afccb37a3b | ||
|
|
c55ef7c383 | ||
|
|
47ea47ce14 | ||
|
|
fd6ade2b5d | ||
|
|
77e38abe91 | ||
|
|
5d1011b482 | ||
|
|
4b11e925d9 | ||
|
|
258b98c262 | ||
|
|
f1db257628 | ||
|
|
3edade6761 | ||
|
|
efcc66d63b | ||
|
|
ca96da22d0 | ||
|
|
85b98cf4c6 | ||
|
|
a404fb6cb5 | ||
|
|
3432b4625f | ||
|
|
b8777d7739 | ||
|
|
fb477627c7 | ||
|
|
fd78b89c92 | ||
|
|
45f9c52e7f | ||
|
|
0a24ff90bb | ||
|
|
e1eae00b35 | ||
|
|
15bfceb05a | ||
|
|
c1f4fe65bb | ||
|
|
a4a6a97aa8 | ||
|
|
608543da0b | ||
|
|
b4fa60d4fd | ||
|
|
b1467bd1da | ||
|
|
3cf0f5f11b | ||
|
|
454737ca79 | ||
|
|
26bc889f8d | ||
|
|
54775b896f | ||
|
|
9217fb4094 | ||
|
|
04d4a30471 | ||
|
|
90f9501902 | ||
|
|
f8d26bd865 | ||
|
|
816d040d81 | ||
|
|
2069293cc1 | ||
|
|
4bd77d5899 | ||
|
|
f8ff342852 | ||
|
|
67ac686704 | ||
|
|
4e5bf7ae2e | ||
|
|
b7fd5dcb4a | ||
|
|
bea287c5b3 | ||
|
|
46c716d450 | ||
|
|
9539a361e4 | ||
|
|
ca35e5557b | ||
|
|
a26ed3d1a6 | ||
|
|
c7d53a5006 | ||
|
|
41461e0d5d | ||
|
|
c0a48d7357 | ||
|
|
66cc744c22 | ||
|
|
58ae734fc2 | ||
|
|
54b2779b79 | ||
|
|
df26e12db6 | ||
|
|
343d89c032 | ||
|
|
49c2d4d115 | ||
|
|
58aefc928d | ||
|
|
70d8902737 | ||
|
|
78eeebf8e6 | ||
|
|
585330b179 | ||
|
|
5b1ac27058 | ||
|
|
bcc36d14a1 | ||
|
|
22f5e05060 | ||
|
|
e510e733cd | ||
|
|
d0a06739d8 | ||
|
|
26c43617d1 | ||
|
|
0a89c7ffc4 | ||
|
|
7097cf6319 | ||
|
|
912a13ea0d | ||
|
|
cb391342d7 | ||
|
|
305889f32b | ||
|
|
f1027d7807 | ||
|
|
2806ac6eb4 | ||
|
|
cc1fecfffd | ||
|
|
e02817362c | ||
|
|
1b0484fc46 | ||
|
|
6fe214a784 | ||
|
|
e18a9f84a4 | ||
|
|
59bb727636 | ||
|
|
20e0c03b39 | ||
|
|
6d1567cf44 | ||
|
|
dc3f53a973 | ||
|
|
dad7cf47b4 | ||
|
|
165b91b068 | ||
|
|
8211afb726 | ||
|
|
2cccef174a | ||
|
|
9bbef4a97b | ||
|
|
10c2bda3a9 | ||
|
|
cf9e04c8ec | ||
|
|
d6887117ac | ||
|
|
3b11be2859 | ||
|
|
d7f52739e8 | ||
|
|
71ea46d95e | ||
|
|
e2afc43506 | ||
|
|
6aed1180e7 | ||
|
|
476b735e3c | ||
|
|
7ad12c7f33 | ||
|
|
60729a091a | ||
|
|
d2bad1d553 | ||
|
|
3e31ad51be | ||
|
|
fbeb4664f7 | ||
|
|
4ee8a30a5a | ||
|
|
6243bce46c | ||
|
|
98b72fdb9b | ||
|
|
5e901e4d21 | ||
|
|
66490d5db4 | ||
|
|
2b839088c7 | ||
|
|
28d3d3e679 | ||
|
|
2de30e34f4 | ||
|
|
2ff71b0d27 | ||
|
|
cdb45364c3 | ||
|
|
8ba338fbe1 | ||
|
|
ce84f9c755 | ||
|
|
d1e74a28d9 | ||
|
|
78a2a9e666 | ||
|
|
53f5643994 | ||
|
|
4ee634766d | ||
|
|
bab739efbd | ||
|
|
8568ec838a | ||
|
|
4cbb18aabc | ||
|
|
3fb60aca4f | ||
|
|
19bbdebdf7 | ||
|
|
bc66b1a556 | ||
|
|
4762fd83d4 | ||
|
|
c27c12d975 | ||
|
|
0abbd85134 | ||
|
|
af1f00dff9 | ||
|
|
35b4c9d375 | ||
|
|
74da15e20d | ||
|
|
efc7fdb669 | ||
|
|
a75f368d5b | ||
|
|
a3b6095b61 | ||
|
|
7ca6f80ed2 | ||
|
|
f1b8a7ab54 | ||
|
|
079aa13edb | ||
|
|
67bac9ff59 | ||
|
|
0d80ae3a91 | ||
|
|
b1b215f083 | ||
|
|
f55c80eadf | ||
|
|
c81bb2b70a | ||
|
|
5fa9704a65 | ||
|
|
60d39a7d1f | ||
|
|
13564fbc17 | ||
|
|
77a5820c3c | ||
|
|
b790354f9a | ||
|
|
7948819e0c | ||
|
|
5cd13227ad | ||
|
|
36dc7bd924 | ||
|
|
6bd7c6c06d | ||
|
|
e9b0840f01 | ||
|
|
a8b01dc21a | ||
|
|
a815592954 | ||
|
|
f4475549d6 | ||
|
|
a6eb227330 | ||
|
|
343087e2b4 | ||
|
|
66b2ad7939 | ||
|
|
57a7103d75 | ||
|
|
23b836ffbb | ||
|
|
e54cf914d7 | ||
|
|
fa57853bd2 | ||
|
|
ddd4ec2d9e | ||
|
|
1812e8811b | ||
|
|
1d37d8cac0 | ||
|
|
19da705fcb | ||
|
|
6efc2ec9be | ||
|
|
d0c6c7cb33 | ||
|
|
b3b5f063cf | ||
|
|
3731cc4334 | ||
|
|
13df619ba9 | ||
|
|
3edb347666 | ||
|
|
c73832bd9c | ||
|
|
2f26a7edae | ||
|
|
deaf81e2a4 | ||
|
|
f1b92718d5 | ||
|
|
1f64649434 | ||
|
|
ff32506c5e | ||
|
|
68b5202730 | ||
|
|
c6abef186c | ||
|
|
e5bdf671b5 | ||
|
|
88e92332ee | ||
|
|
6da51deb83 | ||
|
|
b44f8d52ee | ||
|
|
fa03ed7dd7 | ||
|
|
5617b57b26 | ||
|
|
01210dceac | ||
|
|
e4e049d040 | ||
|
|
a405fba3bb | ||
|
|
b5844db0c7 | ||
|
|
28ab1d4551 | ||
|
|
050ee91289 | ||
|
|
5eb8d7e8b0 | ||
|
|
5e4403bb2e | ||
|
|
fb6591607f | ||
|
|
1cf3378499 | ||
|
|
a336aeb007 | ||
|
|
ee49f470b7 | ||
|
|
b9cda59172 | ||
|
|
ba71c83948 | ||
|
|
2835919931 | ||
|
|
310fab526d | ||
|
|
690b87e375 | ||
|
|
e53625b067 | ||
|
|
9e085c1071 | ||
|
|
5f9dfa9493 | ||
|
|
13051c1e5a | ||
|
|
a9cd3609dd | ||
|
|
e0a3e5a200 | ||
|
|
c587fb1df8 | ||
|
|
51cfe10c28 | ||
|
|
bc3f95c57c | ||
|
|
e368b9e50b | ||
|
|
95c75c289c | ||
|
|
23d3657ac2 | ||
|
|
74f04336bb | ||
|
|
54db2a48af | ||
|
|
cde56d5a22 | ||
|
|
3f1cf44717 | ||
|
|
2d83ac4125 | ||
|
|
7147486b6a | ||
|
|
89ddbac8bc | ||
|
|
e071b82e8a | ||
|
|
13b2b2fc4e | ||
|
|
fe9ef1a3ea | ||
|
|
afb0d0f54d | ||
|
|
26085ff82b |
12
.github/workflows/build-mobile.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/cache-cleanup.yml
vendored
@@ -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: |
|
||||
|
||||
2
.github/workflows/codeql-analysis.yml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/docker-cleanup.yml
vendored
@@ -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"
|
||||
|
||||
105
.github/workflows/docker.yml
vendored
@@ -24,28 +24,25 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- context: "server"
|
||||
image: "immich-server"
|
||||
platforms: "linux/arm/v7,linux/amd64,linux/arm64"
|
||||
- context: "web"
|
||||
image: "immich-web"
|
||||
platforms: "linux/arm/v7,linux/amd64,linux/arm64"
|
||||
platforms: "linux/amd64,linux/arm64"
|
||||
- context: "machine-learning"
|
||||
image: "immich-machine-learning"
|
||||
platforms: "linux/amd64,linux/arm64"
|
||||
- context: "nginx"
|
||||
image: "immich-proxy"
|
||||
platforms: "linux/arm/v7,linux/amd64,linux/arm64"
|
||||
platforms: "linux/amd64,linux/arm64"
|
||||
|
||||
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,91 @@ 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 }}
|
||||
# Skip pushing when PR from a fork
|
||||
push: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{matrix.image}}
|
||||
cache-to: ${{ steps.cache-target.outputs.cache-to }}
|
||||
tags: ${{ steps.metadata.outputs.tags }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
|
||||
build_and_push_server_arm_64:
|
||||
runs-on: self-hosted
|
||||
strategy:
|
||||
# Prevent a failure in one image from stopping the other builds
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- context: "server"
|
||||
image: "immich-server"
|
||||
platforms: "linux/arm64,linux/amd64"
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
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
|
||||
with:
|
||||
driver-opts: |
|
||||
image=moby/buildkit:v0.10.6
|
||||
|
||||
- name: Login to Docker Hub
|
||||
# Only push to Docker Hub when making a release
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
# Skip when PR from a fork
|
||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Generate docker image tags
|
||||
id: metadata
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
flavor: |
|
||||
# Disable latest tag
|
||||
latest=false
|
||||
images: |
|
||||
name=ghcr.io/${{ github.repository_owner }}/${{matrix.image}}
|
||||
name=altran1502/${{matrix.image}},enable=${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
# Tag with branch name
|
||||
type=ref,event=branch
|
||||
# Tag with pr-number
|
||||
type=ref,event=pr
|
||||
# Tag with git tag on release
|
||||
type=ref,event=tag
|
||||
type=raw,value=release,enable=${{ github.event_name == 'release' }}
|
||||
|
||||
- name: Determine build cache output
|
||||
id: cache-target
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
# Essentially just ignore the cache output (PR can't write to registry cache)
|
||||
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ matrix.image }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v5.0.0
|
||||
with:
|
||||
context: ${{ matrix.context }}
|
||||
platforms: ${{ matrix.platforms }}
|
||||
|
||||
5
.github/workflows/prepare-release.yml
vendored
@@ -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 }}
|
||||
|
||||
@@ -83,4 +83,5 @@ jobs:
|
||||
files: |
|
||||
docker/docker-compose.yml
|
||||
docker/example.env
|
||||
docker/hwaccel.yml
|
||||
*.apk
|
||||
|
||||
4
.github/workflows/static_analysis.yml
vendored
@@ -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
|
||||
|
||||
51
.github/workflows/test.yml
vendored
@@ -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
|
||||
@@ -188,7 +183,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 +217,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 +255,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
.gitignore
vendored
@@ -4,6 +4,7 @@
|
||||
.idea
|
||||
|
||||
docker/upload
|
||||
docker/library
|
||||
uploads
|
||||
coverage
|
||||
|
||||
|
||||
3
.gitmodules
vendored
@@ -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
|
||||
|
||||
9
Makefile
@@ -2,7 +2,10 @@ dev:
|
||||
docker-compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
||||
|
||||
dev-new:
|
||||
docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
|
||||
docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans || make dev-down
|
||||
|
||||
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
|
||||
|
||||
21
README.md
@@ -21,13 +21,19 @@
|
||||
<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
|
||||
|
||||
- ⚠️ 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
|
||||
|
||||
@@ -60,7 +66,7 @@ password: demo
|
||||
Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
|
||||
```
|
||||
|
||||
# Features
|
||||
## Features
|
||||
|
||||
| Features | Mobile | Web |
|
||||
| -------------------------------------------- | ------ | --- |
|
||||
@@ -83,14 +89,14 @@ 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 |
|
||||
| Offline support | Yes | No |
|
||||
| Read-only gallery | Yes | Yes |
|
||||
|
||||
# Support the project
|
||||
## Support the project
|
||||
|
||||
I've committed to this project, and I will not stop. I will keep updating the docs, adding new features, and fixing bugs. But I can't do it alone. So I need your help to give me additional motivation to keep going.
|
||||
|
||||
@@ -98,10 +104,15 @@ As our hosts in the [selfhosted.show - In the episode 'The-organization-must-not
|
||||
|
||||
If you feel like this is the right cause and the app is something you are seeing yourself using for a long time, please consider supporting the project with the option below.
|
||||
|
||||
## Donation
|
||||
### Donation
|
||||
|
||||
- [Monthly donation](https://github.com/sponsors/alextran1502) via GitHub Sponsors
|
||||
- [One-time donation](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
|
||||
|
||||
## Contributors
|
||||
<a href="https://github.com/alextran1502/immich/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
|
||||
</a>
|
||||
|
||||
@@ -21,6 +21,11 @@
|
||||
<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">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
|
||||
|
||||
111
README_es_ES.md
Normal file
@@ -0,0 +1,111 @@
|
||||
<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="Licencia: 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="Iniciar sesión con URL personalizada">
|
||||
</p>
|
||||
<h3 align="center">Immich: Una solución Self-Hosted de copia de seguridad de fotos y videos de alto rendimiento</h3>
|
||||
<br/>
|
||||
<a href="https://immich.app">
|
||||
<img src="design/immich-screenshots.png" title="Captura de pantalla principal">
|
||||
</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_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
|
||||
|
||||
- ⚠️ El proyecto está en **desarrollo muy activo**.
|
||||
- ⚠️ Es probable que haya errores y cambios disruptivos.
|
||||
- ⚠️ **¡No utilices la aplicación como única forma de almacenar tus fotos y videos!**
|
||||
|
||||
## Contenido
|
||||
|
||||
- [Documentación oficial](https://immich.app/docs)
|
||||
- [Hoja de ruta](https://github.com/orgs/immich-app/projects/1)
|
||||
- [Demostración](#demo)
|
||||
- [Funciones](#features)
|
||||
- [Introducción](https://immich.app/docs/overview/introduction)
|
||||
- [Instalación](https://immich.app/docs/install/requirements)
|
||||
- [Directrices para contribuir](https://immich.app/docs/overview/support-the-project)
|
||||
- [Apoya el proyecto](#support-the-project)
|
||||
|
||||
## Documentación
|
||||
|
||||
Puedes encontrar la documentación principal, incluidas las guías de instalación, en <https://immich.app/>.
|
||||
|
||||
## Demostración
|
||||
|
||||
Puedes acceder a la demostración web en <https://demo.immich.app>
|
||||
|
||||
Para la aplicación móvil, puedes usar `https://demo.immich.app/api` como `URL de la terminal del servidor`.
|
||||
|
||||
```bash title="Credenciales de la demostración"
|
||||
Las credenciales son
|
||||
correo electrónico: demo@immich.app
|
||||
contraseña: demo
|
||||
```
|
||||
|
||||
```bash
|
||||
Especificaciones: VM de nivel gratuito de Oracle - Ámsterdam - CPU ARM64 de cuatro núcleos a 2.4 GHz, 24 GB de RAM
|
||||
```
|
||||
|
||||
## Funcionalidades
|
||||
|
||||
| Funcionalidades | Móvil | Web |
|
||||
| ----------------------------------------------------- | ------ | --- |
|
||||
| Cargar y ver videos y fotos | Sí | Sí |
|
||||
| Copia de seguridad automática al abrir la aplicación | Sí | N/D |
|
||||
| Álbum(es) selectivo(s) para copia de seguridad | Sí | N/D |
|
||||
| Descargar fotos y videos al dispositivo local | Sí | Sí |
|
||||
| Soporte multiusuario | Sí | Sí |
|
||||
| Álbum y álbumes compartidos | Sí | Sí |
|
||||
| Barra de desplazamiento con función de búsqueda | Sí | Sí |
|
||||
| Soporte para formatos RAW | Sí | Sí |
|
||||
| Visualización de metadatos (EXIF, map) | Sí | Sí |
|
||||
| Búsqueda por metadatos, objetos, rostros y CLIP | Sí | Sí |
|
||||
| Funciones administrativas (gestión de usuarios) | No | Sí |
|
||||
| Copia de seguridad en segundo plano | Sí | N/D |
|
||||
| Desplazamiento virtual | Sí | Sí |
|
||||
| Soporte de OAuth | Sí | Sí |
|
||||
| Claves de API | N/D | Sí |
|
||||
| Copia de seguridad y reproducción de LivePhoto | iOS | Sí |
|
||||
| Estructura de almacenamiento definida por el usuario | Sí | Sí |
|
||||
| Compartir públicamente | No | Sí |
|
||||
| Archivar y marcar como favorito | Sí | Sí |
|
||||
| Mapa global | No | Sí |
|
||||
| Compartir con colaboradores | Sí | Sí |
|
||||
| Reconocimiento facial y agrupación | Sí | Sí |
|
||||
| Recuerdos (hace x años) | Sí | Sí |
|
||||
| Soporte sin conexión | Sí | No |
|
||||
| Galería de solo lectura | Sí | Sí |
|
||||
|
||||
## Apoya el proyecto
|
||||
|
||||
Me he comprometido con este proyecto, y no me detendré. Continuaré actualizando la documentación, agregando nuevas funcionalidades y corrigiendo errores. Pero no puedo hacerlo solo. Por eso, necesito tu ayuda para darme una motivación adicional para seguir adelante.
|
||||
|
||||
Como dijeron nuestros anfitriones en [selfhosted.show - En el episodio 'The-organization-must-not-be-name is a Hostile Actor'](https://selfhosted.show/79?t=1418), esto es una gran tarea de lo que el equipo y yo estamos haciendo. Y me encantaría poder dedicarme a esto a tiempo completo algún día, así que te pido tu ayuda para que eso sea posible.
|
||||
|
||||
Si consideras que esta es una causa justa y la aplicación es algo que te gustaría usar durante mucho tiempo, por favor, considera apoyar el proyecto con las siguientes opciones.
|
||||
|
||||
## Donación
|
||||
|
||||
- [Donación mensual](https://github.com/sponsors/alextran1502) a través de GitHub Sponsors
|
||||
- [Donación única](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) a través de GitHub Sponsors
|
||||
- [Librepay](https://liberapay.com/alex.tran1502/)
|
||||
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
|
||||
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
|
||||
113
README_fr_FR.md
Normal file
@@ -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
README_it_IT.md
Normal file
@@ -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
README_ja_JP.md
Normal file
@@ -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
README_nl_NL.md
Normal file
@@ -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
|
||||
@@ -20,6 +20,12 @@
|
||||
<p align="center">
|
||||
<a href="README.md">English</a>
|
||||
<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
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
</p>
|
||||
<h3 align="center">Immich - 高性能的自托管照片和视频备份方案</h3>
|
||||
<p align="center">
|
||||
请注意: 此README不是由Immich团队维护, 这意味着它在某一时间点不会被更新,因为我们是依靠贡献者来更新的。感谢理解。
|
||||
请注意: 此 README 不是由 Immich 团队维护, 而是依靠贡献者来更新的,这意味着它可能并不会被及时更新。感谢理解。
|
||||
</p>
|
||||
<br/>
|
||||
<a href="https://immich.app">
|
||||
@@ -24,34 +24,42 @@
|
||||
<p align="center">
|
||||
<a href="README.md">English</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>
|
||||
|
||||
|
||||
## 免责声明
|
||||
|
||||
- ⚠️ 本项目正在 **非常活跃** 的开发中。
|
||||
- ⚠️ 可能存在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="示例认证信息"
|
||||
认证信息
|
||||
@@ -60,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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
6808
cli/package-lock.json
generated
7145
cli/src/api/open-api/api.ts
generated
2
cli/src/api/open-api/base.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.70.0
|
||||
* The version of the OpenAPI document: 1.84.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
cli/src/api/open-api/common.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.70.0
|
||||
* The version of the OpenAPI document: 1.84.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
cli/src/api/open-api/configuration.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.70.0
|
||||
* The version of the OpenAPI document: 1.84.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
cli/src/api/open-api/index.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.70.0
|
||||
* The version of the OpenAPI document: 1.84.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -34,6 +34,7 @@ const other = [
|
||||
'orf',
|
||||
'ori',
|
||||
'pef',
|
||||
'psd',
|
||||
'raf',
|
||||
'raw',
|
||||
'rwl',
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
@@ -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,33 +21,21 @@ 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
|
||||
# extends:
|
||||
# file: hwaccel.yml
|
||||
# service: hwaccel
|
||||
build:
|
||||
context: ../server
|
||||
dockerfile: Dockerfile
|
||||
@@ -54,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
|
||||
@@ -87,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
|
||||
@@ -116,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
|
||||
|
||||
@@ -138,6 +155,4 @@ services:
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
model-cache:
|
||||
tsdata:
|
||||
|
||||
@@ -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,16 +30,20 @@ services:
|
||||
env_file:
|
||||
- .env
|
||||
restart: always
|
||||
|
||||
|
||||
immich-microservices:
|
||||
container_name: immich_microservices
|
||||
image: immich-microservices:latest
|
||||
# extends:
|
||||
# file: hwaccel.yml
|
||||
# service: hwaccel
|
||||
build:
|
||||
context: ../server
|
||||
dockerfile: Dockerfile
|
||||
command: ["./start-microservices.sh"]
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
@@ -65,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
|
||||
|
||||
@@ -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
|
||||
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
|
||||
env_file:
|
||||
- .env.test
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_USER: ${DB_USERNAME}
|
||||
POSTGRES_DB: ${DB_DATABASE_NAME}
|
||||
volumes:
|
||||
- /var/lib/postgresql/data
|
||||
networks:
|
||||
- immich-test-network
|
||||
- database
|
||||
|
||||
networks:
|
||||
immich-test-network:
|
||||
database:
|
||||
image: postgres:14-alpine@sha256:28407a9961e76f2d285dc6991e8e48893503cc3836a4755bbc2d40bcc272a441
|
||||
command: -c fsync=off
|
||||
environment:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: e2e_test
|
||||
logging:
|
||||
driver: none
|
||||
|
||||
@@ -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:
|
||||
@@ -18,9 +19,13 @@ services:
|
||||
immich-microservices:
|
||||
container_name: immich_microservices
|
||||
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
||||
command: [ "start.sh", "microservices" ]
|
||||
# extends:
|
||||
# file: hwaccel.yml
|
||||
# service: hwaccel
|
||||
command: ["start.sh", "microservices"]
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
@@ -51,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
|
||||
@@ -76,10 +83,6 @@ services:
|
||||
immich-proxy:
|
||||
container_name: immich_proxy
|
||||
image: ghcr.io/immich-app/immich-proxy:${IMMICH_VERSION:-release}
|
||||
environment:
|
||||
# Make sure these values get passed through from the env file
|
||||
- IMMICH_SERVER_URL
|
||||
- IMMICH_WEB_URL
|
||||
ports:
|
||||
- 2283:8080
|
||||
depends_on:
|
||||
|
||||
@@ -1,116 +1,19 @@
|
||||
###################################################################################
|
||||
# Database
|
||||
###################################################################################
|
||||
# You can find documentation for all the supported env variables at https://immich.app/docs/install/environment-variables
|
||||
|
||||
# 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
|
||||
# The location where your uploaded files are stored
|
||||
UPLOAD_LOCATION=./library
|
||||
|
||||
# The Immich version to use. You can pin this to a specific version like "v1.71.0"
|
||||
IMMICH_VERSION=release
|
||||
|
||||
# Connection secrets for postgres and typesense. You should change these to random passwords
|
||||
TYPESENSE_API_KEY=some-random-text
|
||||
DB_PASSWORD=postgres
|
||||
|
||||
# The values below this line do not need to be changed
|
||||
###################################################################################
|
||||
DB_HOSTNAME=immich_postgres
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=postgres
|
||||
DB_DATABASE_NAME=immich
|
||||
|
||||
# Optional Database settings:
|
||||
# DB_PORT=5432
|
||||
|
||||
###################################################################################
|
||||
# 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.
|
||||
###################################################################################
|
||||
|
||||
UPLOAD_LOCATION=absolute_location_on_your_machine_where_you_want_to_store_the_backup
|
||||
|
||||
|
||||
###################################################################################
|
||||
# Typesense
|
||||
###################################################################################
|
||||
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
|
||||
|
||||
###################################################################################
|
||||
# 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".
|
||||
###################################################################################
|
||||
|
||||
#IMMICH_VERSION=
|
||||
|
||||
24
docker/hwaccel-rkmpp.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
version: "3.8"
|
||||
|
||||
# Hardware acceleration for transcoding using RKMPP for Rockchip SOCs
|
||||
# This is only needed if you want to use hardware acceleration for transcoding.
|
||||
# Supported host OS is Ubuntu Jammy 22.04 with custom ffmpeg from ppa:liujianfeng1994/rockchip-multimedia
|
||||
|
||||
services:
|
||||
hwaccel:
|
||||
security_opt: # enables full access to /sys and /proc, still far better than privileged: true
|
||||
- systempaths=unconfined
|
||||
- apparmor=unconfined
|
||||
group_add:
|
||||
- video
|
||||
devices:
|
||||
- /dev/rga:/dev/rga
|
||||
- /dev/dri:/dev/dri
|
||||
- /dev/dma_heap:/dev/dma_heap
|
||||
- /dev/mpp_service:/dev/mpp_service
|
||||
volumes:
|
||||
- /usr/bin/ffmpeg:/usr/bin/ffmpeg_mpp:ro
|
||||
- /lib/aarch64-linux-gnu:/lib/ffmpeg-mpp:ro
|
||||
- /lib/aarch64-linux-gnu/libblas.so.3:/lib/ffmpeg-mpp/libblas.so.3:ro # symlink is resolved by mounting
|
||||
- /lib/aarch64-linux-gnu/liblapack.so.3:/lib/ffmpeg-mpp/liblapack.so.3:ro # symlink is resolved by mounting
|
||||
- /lib/aarch64-linux-gnu/pulseaudio/libpulsecommon-15.99.so:/lib/ffmpeg-mpp/libpulsecommon-15.99.so:ro
|
||||
23
docker/hwaccel.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
version: "3.8"
|
||||
|
||||
# Hardware acceleration for transcoding - Optional
|
||||
# This is only needed if you want to use hardware acceleration for transcoding.
|
||||
# Depending on your hardware, you should uncomment the relevant lines below.
|
||||
|
||||
services:
|
||||
hwaccel:
|
||||
# devices:
|
||||
# - /dev/dri:/dev/dri # If using Intel QuickSync or VAAPI
|
||||
# volumes:
|
||||
# - /usr/lib/wsl:/usr/lib/wsl # If using VAAPI in WSL2
|
||||
# environment:
|
||||
# - NVIDIA_DRIVER_CAPABILITIES=all # If using NVIDIA GPU
|
||||
# - LD_LIBRARY_PATH=/usr/lib/wsl/lib # If using VAAPI in WSL2
|
||||
# - LIBVA_DRIVER_NAME=d3d12 # If using VAAPI in WSL2
|
||||
# deploy: # Uncomment this section if using NVIDIA GPU
|
||||
# resources:
|
||||
# reservations:
|
||||
# devices:
|
||||
# - driver: nvidia
|
||||
# count: 1
|
||||
# capabilities: [gpu,video]
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: June 2023 update
|
||||
title: Immich Update - June 2023
|
||||
authors: [alextran]
|
||||
tags: [update]
|
||||
---
|
||||
|
||||
BIN
docs/blog/2023/07-29/images/web-shortcuts-panel.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
151
docs/blog/2023/07-29/update.mdx
Normal file
@@ -0,0 +1,151 @@
|
||||
---
|
||||
title: Immich Update - July 2023
|
||||
authors: [alextran]
|
||||
tags: [update, v1.64.0-v1.71.0]
|
||||
---
|
||||
|
||||
Hello, Immich fans, another month, another milestone. We hope you are staying cool and safe in this scorching hot summer across the globe.
|
||||
|
||||
Immich recently got some good recognition when getting to the front page of HackerNews, which helped to let more people know about the project's existence. The project will help more and more people find a solution to control the privacy of their most precious moments. And with the gain in popularity and recognition, we have gotten new users and more questions from the community than ever.
|
||||
|
||||
I want to express my gratitude to all the contributors and the community who have been tremendously helpful to new users' questions and provided technical support.
|
||||
|
||||
Below are the highlights of new features we added to the application over the past month, along with countless bug fixes and improvements across the board, from developer experience to resource optimization and UI/UX improvement. I hope you find these topics as exciting as I am.
|
||||
|
||||
## Highlights
|
||||
|
||||
- Memories feature.
|
||||
- Facial recognition improvements.
|
||||
- Improvements on multi selection behavior on the web.
|
||||
- Shortcuts for common actions on the web.
|
||||
- Support viewer for 360-panorama photos.
|
||||
|
||||
<!--truncate-->
|
||||
|
||||
---
|
||||
|
||||
### Memories feature
|
||||
|
||||
We've added the memory feature on the mobile app, so you can reminisce about your past memories.
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://youtube.com/embed/c7OTl-RqNRE"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
|
||||
### Facial recognition improvements
|
||||
|
||||
Over the past few releases, we have added many UI improvements to the facial recognition feature to help you manage the recognized people better. Some of the highlights:
|
||||
|
||||
#### Choose a new feature photo for a person.
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://youtube.com/embed/PmJp8DmSh1U"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
|
||||
#### Hide and show faces.
|
||||
|
||||
You can now select irrelevant faces to hide them. The hidden faces won’t be displayed in search results and the people section in the info panel.
|
||||
|
||||
#### Merge faces.
|
||||
|
||||
This is useful when you have multiple faces of the same person in your photos, and you want to merge them into one.
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://youtube.com/embed/-Xskhw-vpc4"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
|
||||
We also added a nifty mechanism that when naming a face, similar names will prompt you a merge face option for the convenience.
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://youtube.com/embed/XzE6wficbl4"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
|
||||
### Improvements on multi selection behavior on the web
|
||||
|
||||
We have added a new multi selection behavior on the web to help you select multiple items easier. You can now select a range of photos and videos by holding the `Shift` key.
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://youtube.com/embed/e_SiuHpVnmM"
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
|
||||
### Shortcuts for common actions on the web.
|
||||
|
||||
Some of us only navigate the world and the web with a keyboard (looking at you, Vim and Emacs users). So it would take away the sacred weapon of choice to require many clicks to perform repetitive actions. So we added quick shortcuts for the following action on the web.
|
||||
|
||||
<img
|
||||
src={require('./images/web-shortcuts-panel.png').default}
|
||||
width="100%"
|
||||
style={{ borderRadius: '25px' }}
|
||||
alt="Dot Env Example"
|
||||
/>
|
||||
|
||||
### Support viewer for 360-panorama photos.
|
||||
|
||||
Photos with the EXIF property of `ProjectionType` will now have a special viewer on the web to view all the angles of the panorama.
|
||||
|
||||
The thumbnail of the 360 degrees panoramas will have a special icon on the top right of the thumbnail
|
||||
|
||||
<img
|
||||
src="https://github.com/immich-app/immich/assets/61410067/728ca1b0-375c-4631-8081-a609843e702f"
|
||||
width="50%"
|
||||
style={{ borderRadius: '25px' }}
|
||||
alt="Dot Env Example"
|
||||
/>
|
||||
|
||||
Panorama in the detail view
|
||||
|
||||
<img
|
||||
src="https://github.com/immich-app/immich/assets/61410067/3c89dac4-395d-45fa-9bc5-98a6248fd476"
|
||||
width="50%"
|
||||
style={{ borderRadius: '25px' }}
|
||||
alt="Dot Env Example"
|
||||
/>
|
||||
|
||||
---
|
||||
|
||||
Thank you, and I am asking for your support for the project. I hope to be a full-time maintainer of Immich one day to dedicate myself to the project as my life's work for the community and my family. You can find the support channels below:
|
||||
|
||||
- Monthly donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502)
|
||||
- One-time donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502)
|
||||
- [Liberapay](https://liberapay.com/alex.tran1502/)
|
||||
- [buymeacoffee](https://www.buymeacoffee.com/altran1502)
|
||||
- Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX
|
||||
- Give a project a star - the contributors love gazing at the stars and seeing their creations shining in the sky.
|
||||
|
||||
Join our friendly [Discord](https://discord.gg/D8JsnBEuKb) to talk and discuss Immich, tech, or anything
|
||||
|
||||
Cheer!
|
||||
|
||||
Until next time!
|
||||
|
||||
Alex
|
||||
@@ -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?
|
||||
|
||||
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.
|
||||
|
||||
### Why does my uploaded photo show up with the wrong date or time in Immich?
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
As an example, the following modification of `docker-compose.yml` will set the timezone of the microservices container to be `Europe/Stockholm`
|
||||
|
||||
```
|
||||
environment:
|
||||
- TZ=Europe/Stockholm # <---- Add this line in the microservices config
|
||||
```
|
||||
|
||||
### Why are only photos and not videos being uploaded to Immich?
|
||||
|
||||
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.
|
||||
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 is Immich slow on low-memory systems like the Raspberry Pi?
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
### How to disable machine-learning and TypeSense?
|
||||
### How can I lower Immich's CPU usage?
|
||||
|
||||
:::warning
|
||||
Disabling both will result in poor search experience and typesense utilizes CLIP embeddings which are generated by machine-learning.
|
||||
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:
|
||||
|
||||
- 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.
|
||||
|
||||
### How can I disable machine learning?
|
||||
|
||||
:::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.
|
||||
:::
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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 can I disable TypeSense?
|
||||
|
||||
:::info
|
||||
Disabling Typesense will result in a poor search experience since searching is reliant on it.
|
||||
:::
|
||||
|
||||
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?
|
||||
|
||||
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.
|
||||
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>');
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 501 KiB |
@@ -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 {
|
||||
|
||||
@@ -12,22 +12,57 @@ The `immich-server` docker image comes preinstalled with an administrative CLI (
|
||||
|
||||
## How to run a command
|
||||
|
||||
To run a command, [connect](/docs/guides/docker-help.md#attach-to-a-container) to the `immich_server` container and then execute the command via `immich <command>`.
|
||||
To run a command, [connect](/docs/guides/docker-help.md#attach-to-a-container) to the `immich_server` container and then execute the command via `immich-admin <command>`.
|
||||
|
||||
## Examples
|
||||
|
||||
Reset Admin Password
|
||||
|
||||

|
||||
```
|
||||
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
|
||||
|
||||

|
||||
```
|
||||
immich-admin disable-password-login
|
||||
Password login has been disabled.
|
||||
```
|
||||
|
||||
Enabled Password Login
|
||||
|
||||

|
||||
```
|
||||
immich-admin enable-password-login
|
||||
Password login has been enabled.
|
||||
```
|
||||
|
||||
List Users
|
||||
|
||||

|
||||
```
|
||||
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
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
@@ -4,38 +4,113 @@ sidebar_position: 1
|
||||
|
||||
# Architecture
|
||||
|
||||
Immich uses a traditional client-server design, with a dedicated database for data persistence. The frontend clients communicate with backend services over HTTP using REST APIs.
|
||||
|
||||
## High Level Diagram
|
||||
|
||||

|
||||
|
||||
## Technology
|
||||
The diagram shows clients communicating with the server via REST, as well as the flow of database between backend services.
|
||||
|
||||
Immich is a full-stack [TypeScript](https://www.typescriptlang.org/) application, with a [Flutter](https://flutter.dev/) mobile app.
|
||||
## Clients
|
||||
|
||||
### Mobile
|
||||
Immich has three main clients:
|
||||
|
||||
- [Flutter](https://flutter.dev/)
|
||||
- [Riverpod](https://riverpod.dev/) for state management.
|
||||
1. Mobile app - Android, iOS
|
||||
2. Web app - Responsive website
|
||||
3. CLI - Command-line utility for bulk upload
|
||||
|
||||
### Web
|
||||
:::info
|
||||
All three clients use [OpenAPI](./open-api.md) to auto-generate rest clients for easy integration. For more information about this process, see [OpenAPI](./open-api.md).
|
||||
:::
|
||||
|
||||
- [SvelteKit](https://kit.svelte.dev/)
|
||||
- [Tailwindcss](https://tailwindcss.com/)
|
||||
### Mobile App
|
||||
|
||||
### Server
|
||||
The mobile app is written in [Flutter](https://flutter.dev/). It uses [Isar Database](https://isar.dev/) for a local database and [Riverpod](https://riverpod.dev/) for state management.
|
||||
|
||||
- [Node.js](https://nodejs.org/)
|
||||
- [Nest.js](https://nestjs.com/)
|
||||
- [TypeORM](https://typeorm.io/) for database management.
|
||||
- [Jest](https://jestjs.io/) for testing.
|
||||
- [Python](https://www.python.org/) for Machine Learning.
|
||||
### Web Client
|
||||
|
||||
### Database
|
||||
The web app is a [TypeScript](https://www.typescriptlang.org/) project that uses [SvelteKit](https://kit.svelte.dev) and [Tailwindcss](https://tailwindcss.com/).
|
||||
|
||||
- [PostgreSQL](https://www.postgresql.org/)
|
||||
- [Redis](https://redis.io/) for job queuing.
|
||||
- [Typesense](https://typesense.org/) for search.
|
||||
### CLI
|
||||
|
||||
### Web Server
|
||||
The CLI is a [TypeScript](https://www.typescriptlang.org/) project that parses command line arguments to programmatically upload/import assets to an Immich server. See [Bulk Upload](/docs/features/bulk-upload.md) for more information about its usage.
|
||||
|
||||
- [NGINX](https://www.nginx.com/) for internal communication between containers and load balancing when scaling.
|
||||
## Server
|
||||
|
||||
The Immich backend is divided into several services, which are run as individual docker containers.
|
||||
|
||||
1. `immich-server` - Handle and respond to REST API requests
|
||||
1. `immich-microservices` - Execute background jobs (thumbnail generation, metadata extraction, transcoding, etc.)
|
||||
1. `immich-machine-learning` - Execute machine learning models
|
||||
1. `postgres` - Persistent data storage
|
||||
1. `redis`- Queue management for `immich-microservices`
|
||||
1. `typesense`- Specialized database for search, specifically with vector comparison features
|
||||
|
||||
### Immich Server
|
||||
|
||||
The Immich Server is a [TypeScript](https://www.typescriptlang.org/) project written for [Node.js](https://nodejs.org/). It uses the [Nest.js](https://nestjs.com) framework, with [TypeORM](https://typeorm.io/) for database management. The server codebase also loosely follows the [Hexagonal Architecture](<https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)>). Specifically, we aim to separate technology specific implementations (`infra/`) from core business logic (`domain/`).
|
||||
|
||||
#### REST Endpoints
|
||||
|
||||
The server is a list of HTTP endpoints and associated handlers (controllers). Each controller usually implements the following CRUD operations:
|
||||
|
||||
- `POST` `/<type>` - **Create**
|
||||
- `GET` `/<type>` - **Read** (all)
|
||||
- `GET` `/<type>/:id` - **Read** (by id)
|
||||
- `PUT` `/<type>/:id` - **Updated** (by id)
|
||||
- `DELETE` `/<type>/:id` - **Delete** (by id)
|
||||
|
||||
#### DTOs
|
||||
|
||||
The server uses [Domain Transfer Objects](https://en.wikipedia.org/wiki/Data_transfer_object) as public interfaces for the inputs (query, params, and body) and outputs (response) for each endpoint. DTOs translate to [OpenAPI](./open-api.md) schemas and control the generated code used by each client.
|
||||
|
||||
### Microservices
|
||||
|
||||
The Immich Microservices image uses the same `Dockerfile` as the Immich Server, but with a different entrypoint. The Immich Microservices service mainly handles executing jobs, which include the following:
|
||||
|
||||
- Thumbnail Generation
|
||||
- Metadata Extraction
|
||||
- Video Transcoding
|
||||
- Object Tagging
|
||||
- Facial Recognition
|
||||
- Storage Template Migration
|
||||
- Search (Typesense synchronization)
|
||||
- Sidecar (see [XMP Sidecars](/docs/features/xmp-sidecars.md))
|
||||
- Background jobs (file deletion, user deletion)
|
||||
|
||||
:::info
|
||||
This list closely matches what is available on the [Administration > Jobs](/docs/administration/jobs.md) page, which provides some remote queue management capabilities.
|
||||
:::
|
||||
|
||||
### Machine Learning
|
||||
|
||||
The machine learning service is written in [Python](https://www.python.org/) and uses [FastAPI](https://fastapi.tiangolo.com/) for HTTP communication.
|
||||
|
||||
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
|
||||
|
||||
Immich persists data in Postgres, which includes information about access and authorization, users, albums, asset, sharing settings, etc.
|
||||
|
||||
:::info
|
||||
See [Database Migrations](./database-migrations.md) for more information about how to modify the database to create an index, modify a table, add a new column, etc.
|
||||
:::
|
||||
|
||||
### Redis
|
||||
|
||||
Immich uses [Redis](https://redis.com/) via [BullMQ](https://docs.bullmq.io/) to manage job queues. Some jobs trigger subsequent jobs. For example, object detection relies on thumbnail generation and automatically run after one is generated.
|
||||
|
||||
### Typesense
|
||||
|
||||
Immich synchronizes some of the Postgres data into Typesense, so it can execute vector related queries in order to implement certain features including, facial recognition and CLIP search.
|
||||
|
||||
<!-- - [NGINX](https://www.nginx.com/) for internal communication between containers and load balancing when scaling. -->
|
||||
|
||||
22
docs/docs/developer/directories.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
title: Directories
|
||||
---
|
||||
|
||||
# Repository Folder Structure
|
||||
|
||||
Our [GitHub Repository](https://github.com/immich-app/immich) is a [monorepo](https://en.wikipedia.org/wiki/Monorepo) and includes the following folders:
|
||||
|
||||
| Folder | Description |
|
||||
| :------------------ | :------------------------------------------------------------------- |
|
||||
| `.github/` | Github templates and action workflows |
|
||||
| `.vscode/` | VSCode debug launch profiles |
|
||||
| `cli/` | Source code for the work-in-progress CLI rewrite |
|
||||
| `docker/` | Docker compose resources for dev, test, production |
|
||||
| `design/` | Screenshots and logos for the README |
|
||||
| `docs/` | Source code for the [https://immich.app](https://immich.app) website |
|
||||
| `machine-learning/` | Source code for the `immich-machine-learning` docker image |
|
||||
| `misc/release/` | Scripts for version pumps and draft releases |
|
||||
| `mobile/` | Source code for the mobile app, both Android and iOS |
|
||||
| `nginx/` | Source code for the `immich-proxy` docker image |
|
||||
| `server/` | Source code for the `immich-server` docker image |
|
||||
| `web/` | Source code for the `immich-web` docker image |
|
||||
@@ -4,7 +4,7 @@ Immich uses the [OpenAPI](https://swagger.io/specification/) standard to generat
|
||||
|
||||
## Generator
|
||||
|
||||
OpenAPI is used to generate the client (Typescript, Dart) SDK. `openapi-generator-cli` can be installed [here](https://openapi-generator.tech/docs/installation/). The generated SDK is based on the `immich-openapi-specs.json` file, which is autogenerated by the server when running in development mode. The `immich-openapi-specs.json` file can be modified with `@nestjs/swagger` decorators used or referenced by controller endpoints. See the [NestJS OpenAPI docs](https://docs.nestjs.com/openapi/types-and-parameters) for more info. When you add a new endpoint or modify an existing one, you must run the command below to update the client SDK.
|
||||
OpenAPI is used to generate the client (Typescript, Dart) SDK. `openapi-generator-cli` can be installed [here](https://openapi-generator.tech/docs/installation/). The generated SDK is based on the `immich-openapi-specs.json` file, which is autogenerated by the server **when running in development mode**. The `immich-openapi-specs.json` file can be modified with `@nestjs/swagger` decorators used or referenced by controller endpoints. See the [NestJS OpenAPI docs](https://docs.nestjs.com/openapi/types-and-parameters) for more info. When you add a new endpoint or modify an existing one, you must run the server in development mode and run the command below to update the client SDK.
|
||||
|
||||
```bash
|
||||
npm run api:generate # Run from the `server/` directory
|
||||
|
||||
17
docs/docs/developer/testing.md
Normal file
@@ -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 perform 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`.
|
||||
@@ -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
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
60
docs/docs/features/hardware-transcoding.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Hardware Transcoding [Experimental]
|
||||
|
||||
This feature allows you to use a GPU or Intel Quick Sync to accelerate transcoding and reduce CPU load.
|
||||
Note that hardware transcoding is much less efficient for file sizes.
|
||||
As this is a new feature, it is still experimental and may not work on all systems.
|
||||
|
||||
## Supported APIs
|
||||
|
||||
- NVENC
|
||||
- NVIDIA GPUs
|
||||
- Quick Sync
|
||||
- Intel CPUs
|
||||
- VAAPI
|
||||
- GPUs
|
||||
|
||||
## Limitations
|
||||
|
||||
- The instructions and configurations here are specific to Docker Compose. Other container engines may require different configuration.
|
||||
- Only Linux and Windows (through WSL2) servers are supported.
|
||||
- WSL2 does not support Quick Sync.
|
||||
- Raspberry Pi is currently not supported.
|
||||
- Two-pass mode is only supported for NVENC. Other APIs will ignore this setting.
|
||||
- Only encoding is currently hardware accelerated, so the CPU is still used for software decoding.
|
||||
- This is mainly because the original video may not be hardware-decodable.
|
||||
- Hardware dependent
|
||||
- Codec support varies, but H.264 and HEVC are usually supported.
|
||||
- Notably, NVIDIA and AMD GPUs do not support VP9 encoding.
|
||||
- Newer devices tend to have higher transcoding quality.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
#### NVENC
|
||||
|
||||
- You must have the official NVIDIA driver installed on the server.
|
||||
- On Linux (except for WSL2), you also need to have [NVIDIA Container Runtime][nvcr] installed.
|
||||
|
||||
#### QSV
|
||||
|
||||
- For VP9 to work:
|
||||
- You must have a 9th gen Intel CPU or newer
|
||||
- If you have an 11th gen CPU or older, then you may need to follow [these][jellyfin-lp] instructions as Low-Power mode is required
|
||||
- Additionally, if the server specifically has an 11th gen CPU and is running kernel 5.15 (shipped with Ubuntu 22.04 LTS), then you will need to upgrade this kernel (from [Jellyfin docs][jellyfin-kernel-bug])
|
||||
|
||||
## Setup
|
||||
|
||||
1. If you do not already have it, download the latest [`hwaccel.yml`][hw-file] file and ensure it's in the same folder as the `docker-compose.yml`.
|
||||
2. Uncomment the lines that apply to your system and desired usage.
|
||||
3. In the `docker-compose.yml` under `immich-microservices`, uncomment the lines relating to the `hwaccel.yml` file.
|
||||
4. Redeploy the `immich-microservices` container with these updated settings.
|
||||
5. In the Admin page under `FFmpeg settings`, change the hardware acceleration setting to the appropriate option and save.
|
||||
|
||||
## Tips
|
||||
|
||||
- You may want to choose a slower preset than for software transcoding to maintain quality and efficiency
|
||||
- While you can use VAAPI with Nvidia GPUs and Intel CPUs, prefer the more specific APIs since they're more optimized for their respective devices
|
||||
|
||||
[hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.yml
|
||||
[nvcr]: https://github.com/NVIDIA/nvidia-container-runtime/
|
||||
[jellyfin-lp]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#configure-and-verify-lp-mode-on-linux
|
||||
[jellyfin-kernel-bug]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#known-issues-and-limitations
|
||||
170
docs/docs/features/libraries.md
Normal file
@@ -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 containing 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.
|
||||
@@ -1,4 +1,10 @@
|
||||
# Read-only Gallery [Experimental]
|
||||
# Read-only Gallery [Deprecated]
|
||||
|
||||
:::caution
|
||||
|
||||
This feature is being deprecated in favor of [Libraries](/docs/features/libraries.md).
|
||||
|
||||
:::
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -6,18 +12,6 @@ This feature enables users to use an existing gallery without uploading the asse
|
||||
|
||||
Upon syncing the file information, it will be read by Immich to generate supported files.
|
||||
|
||||
:::caution
|
||||
|
||||
This feature is still in an experimental stage. And this is an initial implementation and will receive improvements in the future.
|
||||
|
||||
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.
|
||||
|
||||
:::
|
||||
|
||||
## Usage
|
||||
|
||||
:::tip Example scenario
|
||||
|
||||
85
docs/docs/guides/database-queries.md
Normal file
@@ -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))
|
||||
@@ -1,7 +1,3 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# Docker Help
|
||||
|
||||
## Containers
|
||||
|
||||
26
docs/docs/guides/machine-learning.md
Normal file
@@ -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
docs/docs/install/config-file.md
Normal file
@@ -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.
|
||||
@@ -25,10 +25,18 @@ wget https://github.com/immich-app/immich/releases/latest/download/docker-compos
|
||||
wget -O .env https://github.com/immich-app/immich/releases/latest/download/example.env
|
||||
```
|
||||
|
||||
```bash title="(Optional) Get hwaccel.yml file"
|
||||
wget https://github.com/immich-app/immich/releases/latest/download/hwaccel.yml
|
||||
```
|
||||
|
||||
or by downloading from your browser and moving the files to the directory that you created.
|
||||
|
||||
Note: If you downloaded the files from your browser, also ensure that you rename `example.env` to `.env`.
|
||||
|
||||
:::info
|
||||
Optionally, you can use the [`hwaccel.yml`][hw-file] file to enable hardware acceleration for transcoding. See the [Hardware Transcoding](/docs/features/hardware-transcoding.md) guide for info on how to set this up.
|
||||
:::
|
||||
|
||||
### Step 2 - Populate the .env file with custom values
|
||||
|
||||
<details>
|
||||
@@ -124,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
|
||||
@@ -166,6 +173,10 @@ docker-compose up -d # or `docker compose up -d` based on your docker-compos
|
||||
For more information on how to use the application, please refer to the [Post Installation](/docs/install/post-install.mdx) guide.
|
||||
:::
|
||||
|
||||
:::tip
|
||||
Note that downloading container images might require you to authenticate to the GitHub Container Registry ([steps here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry)).
|
||||
:::
|
||||
|
||||
### Step 4 - Upgrading
|
||||
|
||||
If `IMMICH_VERSION` is set, it will need to be updated to the latest or desired version.
|
||||
@@ -182,4 +193,5 @@ Immich is currently under heavy development, which means you can expect breaking
|
||||
|
||||
[compose-file]: https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||
[env-file]: https://github.com/immich-app/immich/releases/latest/download/example.env
|
||||
[hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.yml
|
||||
[watchtower]: https://containrrr.dev/watchtower/
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
BIN
docs/docs/install/img/truenas01.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
docs/docs/install/img/truenas02.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
docs/docs/install/img/truenas03.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/docs/install/img/truenas04.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
docs/docs/install/img/truenas05.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
docs/docs/install/img/truenas06.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
docs/docs/install/img/truenas07.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
docs/docs/install/img/truenas08.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
docs/docs/install/img/truenas09.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
@@ -1,5 +1,5 @@
|
||||
---
|
||||
sidebar_position: 100
|
||||
sidebar_position: 90
|
||||
---
|
||||
|
||||
import RegisterAdminUser from '../partials/_register-admin.md';
|
||||
|
||||
192
docs/docs/install/truenas.md
Normal file
@@ -0,0 +1,192 @@
|
||||
---
|
||||
sidebar_position: 80
|
||||
---
|
||||
|
||||
# TrueNAS SCALE [Community]
|
||||
|
||||
:::note
|
||||
This is a community contribution and not officially supported by the Immich team, but included here for convenience.
|
||||
|
||||
**Please report issues to the corresponding [Github Repository](https://github.com/truenas/charts/tree/master/community/immich).**
|
||||
:::
|
||||
|
||||
Immich can easily be installed on TrueNAS SCALE via the **Community** train application.
|
||||
Consider reviewing the TrueNAS [Apps tutorial](https://www.truenas.com/docs/scale/scaletutorials/apps/) if you have not previously configured applications on your system.
|
||||
|
||||
TrueNAS SCALE makes installing and updating Immich easy, but you must use the Immich web portal and mobile app to configure accounts and access libraries.
|
||||
|
||||
## First Steps
|
||||
|
||||
The Immich app in TrueNAS SCALE installs, completes the initial configuration, then starts the Immich web portal.
|
||||
When updates become available, SCALE alerts and provides easy updates.
|
||||
|
||||
Before installing the Immich app in SCALE, review the [Environment Variables](/docs/install/environment-variables.md) documentation to see if you want to configure any during installation.
|
||||
You can configure environment variables at any time after deploying the application.
|
||||
|
||||
You can allow SCALE to create the datasets Immich requires automatically during app installation.
|
||||
Or before beginning app installation, [create the datasets](https://www.truenas.com/docs/scale/scaletutorials/storage/datasets/datasetsscale/) to use in the **Storage Configuration** section during installation.
|
||||
Immich requires seven datasets: **library**, **pgBackup**, **pgData**, **profile**, **thumbs**, **uploads**, and **video**.
|
||||
You can organize these as one parent with seven child datasets, for example `mnt/tank/immich/library`, `mnt/tank/immich/pgBackup`, and so on.
|
||||
|
||||
## Installing the Immich Application
|
||||
|
||||
To install the **Immich** application, go to **Apps**, click **Discover Apps**, either begin typing Immich into the search field or scroll down to locate the **Immich** application widget.
|
||||
|
||||
<img
|
||||
src={require('./img/truenas01.png').default}
|
||||
width="50%"
|
||||
alt="Immich App Widget"
|
||||
className="border rounded-xl"
|
||||
/>
|
||||
|
||||
Click on the widget to open the **Immich** application details screen.
|
||||
|
||||
<img
|
||||
src={require('./img/truenas02.png').default}
|
||||
width="100%"
|
||||
alt="Immich App Details Screen"
|
||||
className="border rounded-xl"
|
||||
/>
|
||||
|
||||
Click **Install** to open the Immich application configuration screen.
|
||||
|
||||
Application configuration settings are presented in several sections, each explained below.
|
||||
To find specific fields click in the **Search Input Fields** search field, scroll down to a particular section or click on the section heading on the navigation area in the upper-right corner.
|
||||
|
||||
<img
|
||||
src={require('./img/truenas03.png').default}
|
||||
width="100%"
|
||||
alt="Install Immich Screen"
|
||||
className="border rounded-xl"
|
||||
/>
|
||||
|
||||
Accept the default values in **Application Name** and **Version**.
|
||||
|
||||
Accept the default value in **Timezone** or change to match your local timezone.
|
||||
**Timezone** is only used by the Immich `exiftool` microservice if it cannot be determined from the image metadata.
|
||||
|
||||
Accept the default port in **Web Port**.
|
||||
|
||||
Immich requires seven storage datasets.
|
||||
You can allow SCALE to create them for you, or use the dataset(s) created in [First Steps](#first-steps).
|
||||
Select the storage options you want to use for **Immich Uploads Storage**, **Immich Library Storage**, **Immich Thumbs Storage**, **Immich Profile Storage**, **Immich Video Storage**, **Immich Postgres Data Storage**, **Immich Postgres Backup Storage**.
|
||||
Select **ixVolume (dataset created automatically by the system)** in **Type** to let SCALE create the dataset or select **Host Path** to use the existing datasets created on the system.
|
||||
|
||||
Accept the defaults in Resources or change the CPU and memory limits to suit your use case.
|
||||
|
||||
Click **Install**.
|
||||
The system opens the **Installed Applications** screen with the Immich app in the **Deploying** state.
|
||||
When the installation completes it changes to **Running**.
|
||||
|
||||
<img
|
||||
src={require('./img/truenas04.png').default}
|
||||
width="100%"
|
||||
alt="Immich Installed"
|
||||
className="border rounded-xl"
|
||||
/>
|
||||
|
||||
Click **Web Portal** on the **Application Info** widget to open the Immich web interface to set up your account and begin uploading photos.
|
||||
|
||||
:::tip
|
||||
For more information on how to use the application once installed, please refer to the [Post Install](/docs/install/post-install.mdx) guide.
|
||||
:::
|
||||
|
||||
## Editing Environment Variables
|
||||
|
||||
Go to the **Installed Applications** screen and select Immich from the list of installed applications.
|
||||
Click **Edit** on the **Application Info** widget to open the **Edit Immich** screen.
|
||||
The settings on the edit screen are the same as on the install screen.
|
||||
You cannot edit **Storage Configuration** paths after the initial app install.
|
||||
|
||||
Click **Update** to save changes.
|
||||
TrueNAS automatically updates, recreates, and redeploys the Immich container with the updated environment variables.
|
||||
|
||||
## Updating the App
|
||||
|
||||
When updates become available, SCALE alerts and provides easy updates.
|
||||
To update the app to the latest version, click **Update** on the **Application Info** widget from the **Installed Applications** screen.
|
||||
|
||||
Update opens an update window for the application that includes two selectable options, Images (to be updated) and Changelog. Click on the down arrow to see the options available for each.
|
||||
|
||||
Click **Upgrade** to begin the process and open a counter dialog that shows the upgrade progress. When complete, the update badge and buttons disappear and the application Update state on the Installed screen changes from Update Available to Up to date.
|
||||
|
||||
## Understanding Immich Settings in TrueNAS SCALE
|
||||
|
||||
Accept the default value or enter a name in **Application Name** field.
|
||||
In most cases use the default name, but if adding a second deployment of the application you must change this name.
|
||||
|
||||
Accept the default version number in **Version**.
|
||||
When a new version becomes available, the application has an update badge.
|
||||
The **Installed Applications** screen shows the option to update applications.
|
||||
|
||||
### Immich Configuration Settings
|
||||
|
||||
You can accept the defaults in the **Immich Configuration** settings, or enter the settings you want to use.
|
||||
|
||||
<img
|
||||
src={require('./img/truenas05.png').default}
|
||||
width="100%"
|
||||
alt="Configuration Settings"
|
||||
className="border rounded-xl"
|
||||
/>
|
||||
|
||||
Accept the default setting in **Timezone** or change to match your local timezone.
|
||||
**Timezone** is only used by the Immich `exiftool` microservice if it cannot be determined from the image metadata.
|
||||
|
||||
You can enter a **Public Login Message** to display on the login page, or leave it blank.
|
||||
|
||||
### Networking Settings
|
||||
|
||||
Accept the default port numbers in **Web Port**.
|
||||
The SCALE Immich app listens on port **30041**.
|
||||
|
||||
Refer to the TrueNAS [default port list](https://www.truenas.com/docs/references/defaultports/) for a list of assigned port numbers.
|
||||
To change the port numbers, enter a number within the range 9000-65535.
|
||||
|
||||
<img
|
||||
src={require('./img/truenas06.png').default}
|
||||
width="100%"
|
||||
alt="Networking Settings"
|
||||
className="border rounded-xl"
|
||||
/>
|
||||
|
||||
### Storage Settings
|
||||
|
||||
You can install Immich using the default setting **ixVolume (dataset created automatically by the system)** or use the host path option with datasets [created before installing the app](#first-steps).
|
||||
|
||||
<img
|
||||
src={require('./img/truenas07.png').default}
|
||||
width="100%"
|
||||
alt="Configure Storage ixVolumes"
|
||||
className="border rounded-xl"
|
||||
/>
|
||||
|
||||
Select **Host Path (Path that already exists on the system)** to browse to and select the datasets.
|
||||
|
||||
<img
|
||||
src={require('./img/truenas08.png').default}
|
||||
width="100%"
|
||||
alt="Configure Storage Host Paths"
|
||||
className="border rounded-xl"
|
||||
/>
|
||||
|
||||
### Resource Configuration Settings
|
||||
|
||||
Accept the default values in **Resources Configuration** or enter new CPU and memory values
|
||||
By default, this application is limited to use no more than 4 CPU cores and 8 Gigabytes available memory. The application might use considerably less system resources.
|
||||
|
||||
<img
|
||||
src={require('./img/truenas09.png').default}
|
||||
width="100%"
|
||||
alt="Resource Limits"
|
||||
className="border rounded-xl"
|
||||
/>
|
||||
|
||||
To customize the CPU and memory allocated to the container Immich uses, enter new CPU values as a plain integer value followed by the suffix m (milli).
|
||||
Default is 4000m.
|
||||
|
||||
Accept the default value 8Gi allocated memory or enter a new limit in bytes.
|
||||
Enter a plain integer followed by the measurement suffix, for example 129M or 123Mi.
|
||||
|
||||
Systems with compatible GPU(s) display devices in **GPU Configuration**.
|
||||
See [Managing GPUs](https://www.truenas.com/docs/scale/scaletutorials/systemsettings/advanced/managegpuscale/) for more information about allocating isolated GPU devices in TrueNAS SCALE.
|
||||
@@ -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
|
||||
|
||||
@@ -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!
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ I really like the Japanese culture, especially the books, history, and food. The
|
||||
|
||||

|
||||
|
||||
One of my favorite books is [Taikō](https://www.goodreads.com/book/show/336228.Taiko), it is a story about a prominent figure in the history of Japan, [Toyotomy Hideyoshi](https://www.britannica.com/biography/Toyotomi-Hideyoshi). He came from nothing, and through his resilience and wonderful mind, he has become one of the most powerful rulers in Japan's history. I enjoy his personality and the way he moved through life.
|
||||
One of my favorite books is [Taikō](https://www.goodreads.com/book/show/336228.Taiko), it is a story about a prominent figure in the history of Japan, [Toyotomi Hideyoshi](https://www.britannica.com/biography/Toyotomi-Hideyoshi). He came from nothing, and through his resilience and wonderful mind, he has become one of the most powerful rulers in Japan's history. I enjoy his personality and the way he moved through life.
|
||||
|
||||
The color is an adaptation of **_App-Which-Must-Not-Be-Named_**'s color scheme, with an extra color (pink) to complete the flower's fifth petal. The petal layers are the same color scheme as the main layer rotating back and forth to "bring the flower to life."
|
||||
|
||||
|
||||
@@ -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',
|
||||
}),
|
||||
|
||||
9140
docs/package-lock.json
generated
@@ -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",
|
||||
|
||||
90
docs/src/components/timeline.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
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;
|
||||
dateType: DateType;
|
||||
}
|
||||
|
||||
export enum DateType {
|
||||
RELEASE = 'Release Date',
|
||||
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 md: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}]</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="md:hidden text-xs">
|
||||
{`${item.dateType} - ${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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
title: Markdown page example
|
||||
---
|
||||
|
||||
# Markdown page example
|
||||
|
||||
You don't need React to write simple standalone pages.
|
||||
592
docs/src/pages/milestones.tsx
Normal file
@@ -0,0 +1,592 @@
|
||||
import {
|
||||
mdiAccountGroup,
|
||||
mdiAndroid,
|
||||
mdiAppleIos,
|
||||
mdiArchiveOutline,
|
||||
mdiBookSearchOutline,
|
||||
mdiCakeVariant,
|
||||
mdiCheckAll,
|
||||
mdiCheckboxMarked,
|
||||
mdiCollage,
|
||||
mdiContentCopy,
|
||||
mdiDevices,
|
||||
mdiFaceMan,
|
||||
mdiFaceManOutline,
|
||||
mdiFile,
|
||||
mdiFileSearch,
|
||||
mdiFolder,
|
||||
mdiHeart,
|
||||
mdiImage,
|
||||
mdiImageAlbum,
|
||||
mdiImageMultipleOutline,
|
||||
mdiImageSearch,
|
||||
mdiKeyboardSettingsOutline,
|
||||
mdiMagnify,
|
||||
mdiMap,
|
||||
mdiMaterialDesign,
|
||||
mdiMerge,
|
||||
mdiMonitor,
|
||||
mdiMotionPlayOutline,
|
||||
mdiPalette,
|
||||
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, { DateType, Item } from '../components/timeline';
|
||||
|
||||
const items: Item[] = [
|
||||
{
|
||||
icon: mdiStar,
|
||||
description: 'Reach 20K Stars on GitHub!',
|
||||
title: '20,000 Stars',
|
||||
release: 'v1.83.0',
|
||||
tag: 'v1.83.0',
|
||||
date: new Date(2023, 9, 28),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
icon: mdiContentCopy,
|
||||
title: 'Stack assets',
|
||||
description: 'Manual asset stacking for grouping and hiding related assets in the main timeline.',
|
||||
release: 'v1.83.0',
|
||||
tag: 'v1.83.0',
|
||||
date: new Date(2023, 9, 28),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
icon: mdiPalette,
|
||||
title: 'Custom theme',
|
||||
description: 'Apply your custom CSS for modifying fonts, colors, and styles in the web application.',
|
||||
release: 'v1.83.0',
|
||||
tag: 'v1.83.0',
|
||||
date: new Date(2023, 9, 28),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
icon: mdiImageAlbum,
|
||||
title: 'Album Description',
|
||||
description: 'Save an album description.',
|
||||
release: 'v1.72.0',
|
||||
tag: 'v1.72.0',
|
||||
date: new Date(2023, 7, 6),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
icon: mdiStar,
|
||||
description: 'Reach 10K Stars on GitHub!',
|
||||
title: '10,000 Stars',
|
||||
release: 'v1.54.0',
|
||||
tag: 'v1.54.0',
|
||||
date: new Date(2023, 3, 18),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
icon: mdiText,
|
||||
title: 'Asset Descriptions',
|
||||
description: 'Save an asset description',
|
||||
release: 'v1.54.0',
|
||||
tag: 'v1.54.0',
|
||||
date: new Date(2023, 3, 18),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
icon: mdiCakeVariant,
|
||||
title: 'Immich Turns 1',
|
||||
description: 'Immich is officially one year old.',
|
||||
release: 'v1.43.0',
|
||||
tag: 'v1.43.0',
|
||||
date: new Date(2023, 1, 3),
|
||||
dateType: DateType.DATE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
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),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
icon: mdiPartyPopper,
|
||||
title: 'First Commit',
|
||||
description: 'First commit on GitHub, Immich is born.',
|
||||
release: 'v1.0.0',
|
||||
date: new Date(2022, 1, 3),
|
||||
dateType: DateType.DATE,
|
||||
},
|
||||
];
|
||||
|
||||
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 px-2">
|
||||
Major Milestones
|
||||
</h1>
|
||||
<p className="text-center text-xl px-2">
|
||||
A list of project achievements and milestones, <br />
|
||||
by release date.
|
||||
</p>
|
||||
<div className="flex justify-around mt-8 w-full max-w-full">
|
||||
<Timeline items={items} />
|
||||
</div>
|
||||
</section>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
98
docs/static/img/immich-logo.svg
vendored
Normal file
@@ -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>
|
||||
|
After Width: | Height: | Size: 9.7 KiB |
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 \
|
||||
@@ -13,9 +13,9 @@ ENV VIRTUAL_ENV="/opt/venv" PATH="/opt/venv/bin:${PATH}"
|
||||
COPY poetry.lock pyproject.toml ./
|
||||
RUN poetry install --sync --no-interaction --no-ansi --no-root --only main
|
||||
|
||||
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 +26,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"]
|
||||
|
||||
@@ -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.
|
||||
21
machine-learning/README_es_ES.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Immich Machine Learning
|
||||
|
||||
- Clasificación de imágenes
|
||||
- Incorporación de CLIP
|
||||
- Reconocimiento facial
|
||||
|
||||
# Configuración
|
||||
|
||||
Este proyecto utiliza [Poetry](https://python-poetry.org/docs/#installation), así que asegúrate de instalarlo primero.
|
||||
Ejecutar `poetry install --no-root --with dev` instalará todo lo necesario en un entorno virtual aislado.
|
||||
|
||||
Para agregar o eliminar dependencias, puedes utilizar los comandos `poetry add $PACKAGE_NAME` y `poetry remove $PACKAGE_NAME`, respectivamente.
|
||||
Asegúrate de hacer commit de los archivos `poetry.lock` y `pyproject.toml` para reflejar cualquier cambio en las dependencias.
|
||||
|
||||
# Pruebas de carga
|
||||
|
||||
Para medir la velocidad y latencia de inferencia, puedes utilizar [Locust](https://locust.io/) con el archivo `locustfile.py` proporcionado.
|
||||
Locust funciona haciendo consultas a los puntos finales del modelo y agregando estadísticas, lo que significa que la aplicación debe estar desplegada.
|
||||
Puedes ejecutar `load_test.sh` para implementar automáticamente la aplicación localmente e iniciar Locust, ajustando opcionalmente sus variables de entorno según sea necesario.
|
||||
|
||||
Alternativamente, para pruebas más personalizadas, también puedes ejecutar `locust` directamente: consulta la [documentación](https://docs.locust.io/en/stable/index.html). Ten en cuenta que, en la jerga de Locust, la concurrencia se mide en `usuarios`, y cada usuario ejecuta una tarea a la vez. Para lograr una concurrencia específica por punto final, multiplica ese número por la cantidad de puntos finales que se desean consultar. Por ejemplo, si hay 3 puntos finales y deseas que cada uno de ellos reciba 8 solicitudes al mismo tiempo, debes configurar el número de usuarios en 24.
|
||||
22
machine-learning/README_fr_FR.md
Normal file
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
def get_cache_dir(model_name: str, model_type: ModelType) -> Path:
|
||||
return Path(settings.cache_folder, model_type.value, model_name)
|
||||
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.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))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, Iterator, TypeAlias
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterator
|
||||
from unittest import mock
|
||||
|
||||
import numpy as np
|
||||
@@ -8,8 +9,7 @@ from fastapi.testclient import TestClient
|
||||
from PIL import Image
|
||||
|
||||
from .main import app, init_state
|
||||
|
||||
ndarray: TypeAlias = np.ndarray[int, np.dtype[np.float32]]
|
||||
from .schemas import ndarray_f32
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -18,98 +18,13 @@ def pil_image() -> Image.Image:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cv_image(pil_image: Image.Image) -> ndarray:
|
||||
def cv_image(pil_image: Image.Image) -> ndarray_f32:
|
||||
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:
|
||||
with mock.patch("app.models.cache.from_model_type", autospec=True) as mocked:
|
||||
yield mocked
|
||||
|
||||
|
||||
@@ -117,3 +32,30 @@ 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"))
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def clip_model_cfg() -> dict[str, Any]:
|
||||
return {
|
||||
"embed_dim": 512,
|
||||
"vision_cfg": {"image_size": 224, "layers": 12, "width": 768, "patch_size": 32},
|
||||
"text_cfg": {"context_length": 77, "vocab_size": 49408, "width": 512, "heads": 8, "layers": 12},
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def clip_preprocess_cfg() -> dict[str, Any]:
|
||||
return {
|
||||
"size": [224, 224],
|
||||
"mode": "RGB",
|
||||
"mean": [0.48145466, 0.4578275, 0.40821073],
|
||||
"std": [0.26862954, 0.26130258, 0.27577711],
|
||||
"interpolation": "bicubic",
|
||||
"resize_mode": "shortest",
|
||||
"fill_color": 0,
|
||||
}
|
||||
|
||||
@@ -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("/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)
|
||||
|
||||
|
||||
@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
|
||||
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)
|
||||
|
||||
|
||||
@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
|
||||
async def load(model: InferenceModel) -> InferenceModel:
|
||||
if model.loaded:
|
||||
return model
|
||||
|
||||
def _load() -> None:
|
||||
with app.state.locks[model.model_type]:
|
||||
model.load()
|
||||
|
||||
@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,
|
||||
)
|
||||
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,3 +1,25 @@
|
||||
from .clip import CLIPSTEncoder
|
||||
from typing import Any
|
||||
|
||||
from app.schemas import ModelType
|
||||
|
||||
from .base import InferenceModel
|
||||
from .clip import MCLIPEncoder, OpenCLIPEncoder, is_mclip, is_openclip
|
||||
from .facial_recognition import FaceRecognizer
|
||||
from .image_classification import ImageClassifier
|
||||
|
||||
|
||||
def from_model_type(model_type: ModelType, model_name: str, **model_kwargs: Any) -> InferenceModel:
|
||||
match model_type:
|
||||
case ModelType.CLIP:
|
||||
if is_openclip(model_name):
|
||||
return OpenCLIPEncoder(model_name, **model_kwargs)
|
||||
elif is_mclip(model_name):
|
||||
return MCLIPEncoder(model_name, **model_kwargs)
|
||||
else:
|
||||
raise ValueError(f"Unknown CLIP model {model_name}")
|
||||
case ModelType.FACIAL_RECOGNITION:
|
||||
return FaceRecognizer(model_name, **model_kwargs)
|
||||
case ModelType.IMAGE_CLASSIFICATION:
|
||||
return ImageClassifier(model_name, **model_kwargs)
|
||||
case _:
|
||||
raise ValueError(f"Unknown model type {model_type}")
|
||||
|
||||
@@ -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._cache_dir = Path(cache_dir) if cache_dir is not None else get_cache_dir(model_name, self.model_type)
|
||||
self.loaded = False
|
||||
self._cache_dir = Path(cache_dir) if cache_dir is not None else None
|
||||
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
|
||||
|
||||
try:
|
||||
self.load(**model_kwargs)
|
||||
except (OSError, InvalidProtobuf):
|
||||
self.clear_cache()
|
||||
self.load(**model_kwargs)
|
||||
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
|
||||
|
||||
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 _download(self) -> None:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def predict(self, inputs: Any) -> Any:
|
||||
def _load(self) -> None:
|
||||
...
|
||||
|
||||
@property
|
||||
@@ -38,12 +92,16 @@ class InferenceModel(ABC):
|
||||
|
||||
@property
|
||||
def cache_dir(self) -> Path:
|
||||
return self._cache_dir
|
||||
return self._cache_dir if self._cache_dir is not None else get_cache_dir(self.model_name, self.model_type)
|
||||
|
||||
@cache_dir.setter
|
||||
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)
|
||||
|
||||
@@ -4,6 +4,8 @@ from aiocache.backends.memory import SimpleMemoryCache
|
||||
from aiocache.lock import OptimisticLock
|
||||
from aiocache.plugins import BasePlugin, TimingPlugin
|
||||
|
||||
from app.models import from_model_type
|
||||
|
||||
from ..schemas import ModelType
|
||||
from .base import InferenceModel
|
||||
|
||||
@@ -17,7 +19,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,11 +48,11 @@ 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:
|
||||
model = InferenceModel.from_model_type(model_type, model_name, **model_kwargs)
|
||||
model = from_model_type(model_type, model_name, **model_kwargs)
|
||||
await lock.cas(model, ttl=self.ttl)
|
||||
return model
|
||||
|
||||
|
||||
@@ -1,22 +1,222 @@
|
||||
import json
|
||||
from abc import abstractmethod
|
||||
from functools import cached_property
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
|
||||
from PIL.Image import Image
|
||||
from sentence_transformers import SentenceTransformer
|
||||
import numpy as np
|
||||
import onnxruntime as ort
|
||||
from huggingface_hub import snapshot_download
|
||||
from PIL import Image
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
from app.config import log
|
||||
from app.models.transforms import crop, get_pil_resampling, normalize, resize, to_numpy
|
||||
from app.schemas import ModelType, ndarray_f32, ndarray_i32, ndarray_i64
|
||||
|
||||
from ..schemas import ModelType
|
||||
from .base import InferenceModel
|
||||
|
||||
|
||||
class CLIPSTEncoder(InferenceModel):
|
||||
class BaseCLIPEncoder(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:
|
||||
self.mode = mode
|
||||
super().__init__(model_name, cache_dir, **model_kwargs)
|
||||
|
||||
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.textual_path.as_posix(),
|
||||
sess_options=self.sess_options,
|
||||
providers=self.providers,
|
||||
provider_options=self.provider_options,
|
||||
)
|
||||
|
||||
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.visual_path.as_posix(),
|
||||
sess_options=self.sess_options,
|
||||
providers=self.providers,
|
||||
provider_options=self.provider_options,
|
||||
)
|
||||
|
||||
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")
|
||||
|
||||
outputs = self.vision_model.run(None, self.transform(image_or_text))
|
||||
case str():
|
||||
if self.mode == "vision":
|
||||
raise TypeError("Cannot encode text as vision-only model")
|
||||
|
||||
outputs = self.text_model.run(None, self.tokenize(image_or_text))
|
||||
case _:
|
||||
raise TypeError(f"Expected Image or str, but got: {type(image_or_text)}")
|
||||
|
||||
return outputs[0][0].tolist()
|
||||
|
||||
@abstractmethod
|
||||
def tokenize(self, text: str) -> dict[str, ndarray_i32]:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def transform(self, image: Image.Image) -> dict[str, ndarray_f32]:
|
||||
pass
|
||||
|
||||
@property
|
||||
def textual_dir(self) -> Path:
|
||||
return self.cache_dir / "textual"
|
||||
|
||||
@property
|
||||
def visual_dir(self) -> Path:
|
||||
return self.cache_dir / "visual"
|
||||
|
||||
@property
|
||||
def model_cfg_path(self) -> Path:
|
||||
return self.cache_dir / "config.json"
|
||||
|
||||
@property
|
||||
def textual_path(self) -> Path:
|
||||
return self.textual_dir / "model.onnx"
|
||||
|
||||
@property
|
||||
def visual_path(self) -> Path:
|
||||
return self.visual_dir / "model.onnx"
|
||||
|
||||
@property
|
||||
def preprocess_cfg_path(self) -> Path:
|
||||
return self.visual_dir / "preprocess_cfg.json"
|
||||
|
||||
@property
|
||||
def cached(self) -> bool:
|
||||
return self.textual_path.is_file() and self.visual_path.is_file()
|
||||
|
||||
|
||||
class OpenCLIPEncoder(BaseCLIPEncoder):
|
||||
def __init__(
|
||||
self,
|
||||
model_name: str,
|
||||
cache_dir: str | None = None,
|
||||
mode: Literal["text", "vision"] | None = None,
|
||||
**model_kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(_clean_model_name(model_name), cache_dir, mode, **model_kwargs)
|
||||
|
||||
def _download(self) -> None:
|
||||
snapshot_download(
|
||||
f"immich-app/{self.model_name}",
|
||||
cache_dir=self.cache_dir,
|
||||
local_dir=self.cache_dir,
|
||||
local_dir_use_symlinks=False,
|
||||
)
|
||||
|
||||
def predict(self, image_or_text: Image | str) -> list[float]:
|
||||
return self.model.encode(image_or_text).tolist()
|
||||
def _load(self) -> None:
|
||||
super()._load()
|
||||
|
||||
self.tokenizer = AutoTokenizer.from_pretrained(self.textual_dir)
|
||||
self.sequence_length = self.model_cfg["text_cfg"]["context_length"]
|
||||
|
||||
self.size = (
|
||||
self.preprocess_cfg["size"][0] if type(self.preprocess_cfg["size"]) == list else self.preprocess_cfg["size"]
|
||||
)
|
||||
self.resampling = get_pil_resampling(self.preprocess_cfg["interpolation"])
|
||||
self.mean = np.array(self.preprocess_cfg["mean"], dtype=np.float32)
|
||||
self.std = np.array(self.preprocess_cfg["std"], dtype=np.float32)
|
||||
|
||||
def tokenize(self, text: str) -> dict[str, ndarray_i32]:
|
||||
input_ids: ndarray_i64 = self.tokenizer(
|
||||
text,
|
||||
max_length=self.sequence_length,
|
||||
return_tensors="np",
|
||||
return_attention_mask=False,
|
||||
padding="max_length",
|
||||
truncation=True,
|
||||
).input_ids
|
||||
return {"text": input_ids.astype(np.int32)}
|
||||
|
||||
def transform(self, image: Image.Image) -> dict[str, ndarray_f32]:
|
||||
image = resize(image, self.size)
|
||||
image = crop(image, self.size)
|
||||
image_np = to_numpy(image)
|
||||
image_np = normalize(image_np, self.mean, self.std)
|
||||
return {"image": np.expand_dims(image_np.transpose(2, 0, 1), 0)}
|
||||
|
||||
@cached_property
|
||||
def model_cfg(self) -> dict[str, Any]:
|
||||
return json.load(self.model_cfg_path.open())
|
||||
|
||||
@cached_property
|
||||
def preprocess_cfg(self) -> dict[str, Any]:
|
||||
return json.load(self.preprocess_cfg_path.open())
|
||||
|
||||
|
||||
class MCLIPEncoder(OpenCLIPEncoder):
|
||||
def tokenize(self, text: str) -> dict[str, ndarray_i32]:
|
||||
tokens: dict[str, ndarray_i64] = self.tokenizer(text, return_tensors="np")
|
||||
return {k: v.astype(np.int32) for k, v in tokens.items()}
|
||||
|
||||
|
||||
_OPENCLIP_MODELS = {
|
||||
"RN50__openai",
|
||||
"RN50__yfcc15m",
|
||||
"RN50__cc12m",
|
||||
"RN101__openai",
|
||||
"RN101__yfcc15m",
|
||||
"RN50x4__openai",
|
||||
"RN50x16__openai",
|
||||
"RN50x64__openai",
|
||||
"ViT-B-32__openai",
|
||||
"ViT-B-32__laion2b_e16",
|
||||
"ViT-B-32__laion400m_e31",
|
||||
"ViT-B-32__laion400m_e32",
|
||||
"ViT-B-32__laion2b-s34b-b79k",
|
||||
"ViT-B-16__openai",
|
||||
"ViT-B-16__laion400m_e31",
|
||||
"ViT-B-16__laion400m_e32",
|
||||
"ViT-B-16-plus-240__laion400m_e31",
|
||||
"ViT-B-16-plus-240__laion400m_e32",
|
||||
"ViT-L-14__openai",
|
||||
"ViT-L-14__laion400m_e31",
|
||||
"ViT-L-14__laion400m_e32",
|
||||
"ViT-L-14__laion2b-s32b-b82k",
|
||||
"ViT-L-14-336__openai",
|
||||
"ViT-H-14__laion2b-s32b-b79k",
|
||||
"ViT-g-14__laion2b-s12b-b42k",
|
||||
}
|
||||
|
||||
|
||||
_MCLIP_MODELS = {
|
||||
"LABSE-Vit-L-14",
|
||||
"XLM-Roberta-Large-Vit-B-32",
|
||||
"XLM-Roberta-Large-Vit-B-16Plus",
|
||||
"XLM-Roberta-Large-Vit-L-14",
|
||||
}
|
||||
|
||||
|
||||
def _clean_model_name(model_name: str) -> str:
|
||||
return model_name.split("/")[-1].replace("::", "__")
|
||||
|
||||
|
||||
def is_openclip(model_name: str) -> bool:
|
||||
return _clean_model_name(model_name) in _OPENCLIP_MODELS
|
||||
|
||||
|
||||
def is_mclip(model_name: str) -> bool:
|
||||
return _clean_model_name(model_name) in _MCLIP_MODELS
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
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 app.schemas import ModelType, ndarray_f32
|
||||
|
||||
from ..config import settings
|
||||
from ..schemas import ModelType
|
||||
from .base import InferenceModel
|
||||
|
||||
|
||||
@@ -15,46 +20,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.model.prepare(
|
||||
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.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: ndarray_f32 | 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)
|
||||
|
||||
scores = bboxes[:, 4].tolist()
|
||||
bboxes = bboxes[:, :4].round().tolist()
|
||||
|
||||
def predict(self, image: cv2.Mat) -> list[dict[str, Any]]:
|
||||
height, width, _ = image.shape
|
||||
results = []
|
||||
faces = self.model.get(image)
|
||||
|
||||
for face in faces:
|
||||
x1, y1, x2, y2 = face.bbox
|
||||
|
||||
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)
|
||||
|
||||