Compare commits

..

105 Commits

Author SHA1 Message Date
mertalev
338f8dc233 update docs 2025-03-15 00:12:07 -04:00
mertalev
f2a98ab523 organize imports 2025-03-15 00:11:21 -04:00
mertalev
9e689e835e add rk3568 2025-03-15 00:07:00 -04:00
mertalev
76eb285204 set log level 2025-03-14 23:37:39 -04:00
mertalev
bc9bec8673 formatting 2025-03-14 17:01:23 -04:00
mertalev
a9508fc5c8 clarify throughput vs latency 2025-03-14 16:49:40 -04:00
mertalev
678cb8938a comparison with arm nn in docs 2025-03-14 16:42:01 -04:00
mertalev
1785d0916b update tests 2025-03-14 16:07:35 -04:00
mertalev
fedbecc9f2 fix 2025-03-14 15:46:47 -04:00
mertalev
7fc47600f5 remove unused import 2025-03-14 15:35:14 -04:00
mertalev
4db85a8954 more linting 2025-03-14 15:30:11 -04:00
mertalev
bd9374e4a9 linting 2025-03-14 15:04:46 -04:00
mertalev
0f1a551842 fix retry 2025-03-14 14:29:07 -04:00
mertalev
323bcde733 add rk3576 2025-03-13 18:11:35 -04:00
mertalev
4b57fb5b37 add siglip2 models 2025-03-13 18:10:00 -04:00
mertalev
b959ae2570 handle facial recognition models 2025-03-13 17:53:41 -04:00
mertalev
e3d041e3c2 remove import 2025-03-13 17:34:07 -04:00
mertalev
2ba2c597e5 no flash attention for now 2025-03-13 17:32:19 -04:00
mertalev
9958ac9ec9 upload to hf 2025-03-13 17:31:41 -04:00
mertalev
c57c562166 export cli 2025-03-12 22:50:29 -04:00
mertalev
ce2a41826e quoted type 2025-03-11 19:20:14 -04:00
mertalev
ec0fa4d52b refactor 2025-03-11 18:43:25 -04:00
mertalev
f5e44f12e1 Merge remote-tracking branch 'origin/main' into rknn-toolkit-lite2 2025-03-11 12:42:25 -04:00
yoni13
6e08f1f371 dont stuck on core_0 on rk3588 but untested 2025-01-25 19:14:34 +08:00
Yoni Yang
3ed5d3dbac Merge branch 'main' into rknn-toolkit-lite2 2025-01-24 16:49:10 +08:00
Yoni Yang
f9387d8478 Merge branch 'main' into rknn-toolkit-lite2 2025-01-24 12:20:29 +08:00
Yoni Yang
4bb98471f4 Merge branch 'main' into rknn-toolkit-lite2 2025-01-23 23:22:24 +08:00
Yoni Yang
794da29411 Merge branch 'main' into rknn-toolkit-lite2 2025-01-22 11:43:59 +08:00
yoni13
dd52c2d68c Update permission 2025-01-19 23:19:44 +08:00
Yoni Yang
59e4b6598e Merge branch 'main' into rknn-toolkit-lite2 2025-01-19 21:50:47 +08:00
yoni13
20ba9f97e9 update mapping 2025-01-19 13:15:38 +00:00
yoni13
ac4ce3ea9c add input outputs 2025-01-19 12:10:01 +00:00
yoni13
2b967ca358 raise NotImplementedError for now 2025-01-19 12:02:08 +08:00
yoni13
1653cd9cd7 update supported SOCs 2025-01-18 17:38:44 +08:00
yoni13
32f3707e52 fix types and ignored pattern 2025-01-18 17:17:48 +08:00
yoni13
58f1cc92d7 prettier happy 2025-01-18 17:03:49 +08:00
yoni13
d2b7e10f55 shellcheck happy 2025-01-18 16:56:15 +08:00
yoni13
b3ae5d34cc fix typo in tests 2025-01-18 16:54:18 +08:00
Yoni Yang
4e42fbc091 Merge branch 'main' into rknn-toolkit-lite2 2025-01-18 16:45:38 +08:00
yoni13
9926045d5e add a simple script to notify user if some op is not supported 2025-01-18 16:32:08 +08:00
yoni13
d7381ab5c1 refactor ignore_patterns 2025-01-18 04:48:11 +00:00
yoni13
87a46dcc5e remove unnecessary print 2025-01-18 11:30:49 +08:00
yoni13
be76857ae6 make these functions snake case. 2025-01-18 11:03:54 +08:00
yoni13
f5de3de163 fix typo and add a propper var name 2025-01-18 10:46:57 +08:00
yoni13
3634ae1f5b fix granularity 2025-01-18 09:58:39 +08:00
yoni13
05675921be remove unrequired devices 2025-01-17 20:09:50 +08:00
yoni13
f067212491 tpe 2025-01-17 19:56:23 +08:00
yoni13
bc48b67379 switch to sha256 2025-01-17 19:42:23 +08:00
yoni13
26d5fb0ac6 add checksum for libnnrt.so 2025-01-17 19:39:26 +08:00
yoni13
f32d991131 changes some cases 2025-01-17 19:25:01 +08:00
yoni13
9882b83cd4 Should FIx the quote that made mypy unhappy 2025-01-15 00:35:43 +08:00
yoni13
01eb09526e trying to fix pytest 2025-01-15 00:28:48 +08:00
yoni13
b5a4ed5160 this duplicated? 2025-01-14 19:22:28 +08:00
yoni13
0f03f77e8e remove non implemented tests 2025-01-14 19:02:16 +08:00
yoni13
c21ce40d9c switch to Runtime error instead of exit() 2025-01-14 18:50:21 +08:00
Yoni Yang
cb01a11f19 Merge branch 'main' into rknn-toolkit-lite2 2025-01-14 18:44:31 +08:00
yoni13
5244ed6d4d black app export 2025-01-14 18:40:28 +08:00
yoni13
8b80d034cb fixed some bugs 2025-01-14 10:38:45 +00:00
yoni13
4b0f93cf6a add test,founds bugs, fix it tomorrow 2025-01-14 01:08:44 +08:00
yoni13
6c4e6cb96f reformat 2025-01-13 18:37:25 +08:00
yoni13
b6cc2054c5 ignore rknn model if not using it 2025-01-13 18:37:01 +08:00
Yoni Yang
f328104e84 Merge branch 'main' into rknn-toolkit-lite2 2025-01-13 18:33:05 +08:00
yoni13
2f7e44aa63 typing be happy. 2025-01-13 18:24:12 +08:00
yoni13
ebdfe1b7b6 Load model by SOC name 2025-01-13 17:08:16 +08:00
yoni13
daf886088a Add export script 2025-01-13 05:44:22 +00:00
yoni13
4c7ac1438b only load knnx model when required 2025-01-12 19:11:16 +00:00
Yoni Yang
8965a9fb16 Merge branch 'main' into rknn-toolkit-lite2 2025-01-13 01:41:55 +08:00
yoni13
7ae4b7129d format be happy 2025-01-13 01:40:20 +08:00
yoni13
1775397a84 Sort them by alphablet 2025-01-13 01:38:14 +08:00
yoni13
68fccad462 Fix docs. 2025-01-13 01:11:32 +08:00
yoni13
bb67a9db6e fix formatting 2025-01-13 00:50:49 +08:00
yoni13
c109e28686 DOCS 2025-01-13 00:44:22 +08:00
yoni13
e6ff21b345 set default thread num to 2, not everyone has 8 gigs of ram 2025-01-12 16:05:18 +08:00
yoni13
c665fd2625 Fix Please do not set this parameter on other platforms. 2025-01-12 01:29:24 +08:00
yoni13
c72cf61ed0 support core_mask for specfic socs 2025-01-12 01:24:24 +08:00
yoni13
19ee48f6f0 fix path 2025-01-12 01:09:36 +08:00
yoni13
efaf70eb9d Set running threads from env 2025-01-12 01:02:16 +08:00
yoni13
665718b09e add rknn to src 2025-01-11 21:11:30 +08:00
yoni13
807111e3b5 Should Fix No module named 'rknn' 2025-01-11 20:41:38 +08:00
yoni13
815ed1ae66 Install onnxruntime 2025-01-11 20:33:48 +08:00
yoni13
416211916d Check if NPU drivers is loaded or not. 2025-01-11 17:59:16 +08:00
yoni13
23d0ea0e7b ruff 2025-01-11 16:28:26 +08:00
yoni13
d5e453a773 ruff format 2025-01-11 16:26:17 +08:00
yoni13
7f2af6f819 Fix typo: rknnlite.api 2025-01-11 16:19:51 +08:00
yoni13
f4671f4886 Indentation issue 2025-01-11 16:16:26 +08:00
yoni13
7aaf3aa57b Remove unused imports. 2025-01-11 16:03:33 +08:00
yoni13
506ca0d3a4 Dockerfile for rknn 2025-01-11 15:47:24 +08:00
yoni13
d5ef821b24 Set group RKNN to optional 2025-01-11 15:30:05 +08:00
yoni13
d10147f478 Handling Import and file not found Error for non-arm devices. 2025-01-11 15:19:53 +08:00
Yoni Yang
66004e3b83 Merge branch 'immich-app:main' into rknn-toolkit-lite2 2025-01-11 10:45:55 +08:00
yoni13
c20d110257 support for rknn.rknnpool.is_available 2025-01-11 10:39:45 +08:00
yoni13
a2722e16e7 Revert my changes to dockerfiles 2025-01-11 10:13:03 +08:00
yoni13
4d704e9f73 fix inf,-inf with 2 concurrency 2025-01-10 14:04:18 +00:00
Yoni Yang
9bc3e5b2e2 Update rknn.py 2025-01-10 20:20:21 +08:00
Yoni Yang
8608b9c6c5 Merge branch 'immich-app:main' into rknn-toolkit-lite2 2025-01-09 18:40:40 +08:00
yoni13
a94fad543b all infrencing works with 1 max job concurrency 2025-01-09 10:38:40 +00:00
Yoni Yang
082c426e34 Merge branch 'immich-app:main' into rknn-toolkit-lite2 2024-12-25 20:24:17 +08:00
Yoni Yang
da152bd284 Merge branch 'immich-app:main' into rknn-toolkit-lite2 2024-12-13 13:46:24 +08:00
Yoni Yang
257cc6c963 Init commit for using rknn, RecognitionFormDataLoadTest doesnt work 2024-12-04 14:32:46 +00:00
Yoni Yang
4140e93aea Merge branch 'immich-app:main' into rknn-toolkit-lite2 2024-12-04 13:15:14 +08:00
Yoni Yang
b6c4b37237 Merge branch 'immich-app:main' into rknn-toolkit2 2024-12-03 06:00:43 -08:00
Yoni Yang
bc849e2e9f ViT-B-32__openai/textual/ Runs with emulator now. 2024-12-01 16:42:53 +00:00
Yoni Yang
7fddf282cf lowercase 2024-11-30 14:24:38 +00:00
Yoni Yang
6ffc227330 test 2024-11-29 07:43:59 +00:00
Yoni Yang
8ef3e49f74 untested 2024-11-29 07:42:09 +00:00
224 changed files with 5176 additions and 7575 deletions

View File

@@ -49,7 +49,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
suffix: ["", "-cuda", "-openvino", "-armnn"]
suffix: ["", "-cuda", "-openvino", "-armnn","-rknn"]
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
@@ -129,6 +129,9 @@ jobs:
runner: ubuntu-24.04-arm
device: armnn
suffix: -armnn
- platforms: linux/arm64
device: rknn
suffix: -rknn
steps:
- name: Prepare
@@ -454,4 +457,4 @@ jobs:
run: exit 1
- name: All jobs passed or skipped
if: ${{ !(contains(needs.*.result, 'failure')) }}
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"

222
cli/package-lock.json generated
View File

@@ -1518,17 +1518,17 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.26.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.26.0.tgz",
"integrity": "sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==",
"version": "8.25.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.25.0.tgz",
"integrity": "sha512-VM7bpzAe7JO/BFf40pIT1lJqS/z1F8OaSsUB3rpFJucQA4cOSuH2RVVVkFULN+En0Djgr29/jb4EQnedUo95KA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.26.0",
"@typescript-eslint/type-utils": "8.26.0",
"@typescript-eslint/utils": "8.26.0",
"@typescript-eslint/visitor-keys": "8.26.0",
"@typescript-eslint/scope-manager": "8.25.0",
"@typescript-eslint/type-utils": "8.25.0",
"@typescript-eslint/utils": "8.25.0",
"@typescript-eslint/visitor-keys": "8.25.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -1544,20 +1544,20 @@
"peerDependencies": {
"@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
"typescript": ">=4.8.4 <5.8.0"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.26.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.26.0.tgz",
"integrity": "sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA==",
"version": "8.25.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.25.0.tgz",
"integrity": "sha512-4gbs64bnbSzu4FpgMiQ1A+D+urxkoJk/kqlDJ2W//5SygaEiAP2B4GoS7TEdxgwol2el03gckFV9lJ4QOMiiHg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.26.0",
"@typescript-eslint/types": "8.26.0",
"@typescript-eslint/typescript-estree": "8.26.0",
"@typescript-eslint/visitor-keys": "8.26.0",
"@typescript-eslint/scope-manager": "8.25.0",
"@typescript-eslint/types": "8.25.0",
"@typescript-eslint/typescript-estree": "8.25.0",
"@typescript-eslint/visitor-keys": "8.25.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1569,18 +1569,18 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
"typescript": ">=4.8.4 <5.8.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.26.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.26.0.tgz",
"integrity": "sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==",
"version": "8.25.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.25.0.tgz",
"integrity": "sha512-6PPeiKIGbgStEyt4NNXa2ru5pMzQ8OYKO1hX1z53HMomrmiSB+R5FmChgQAP1ro8jMtNawz+TRQo/cSXrauTpg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.26.0",
"@typescript-eslint/visitor-keys": "8.26.0"
"@typescript-eslint/types": "8.25.0",
"@typescript-eslint/visitor-keys": "8.25.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1591,14 +1591,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.26.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.26.0.tgz",
"integrity": "sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==",
"version": "8.25.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.25.0.tgz",
"integrity": "sha512-d77dHgHWnxmXOPJuDWO4FDWADmGQkN5+tt6SFRZz/RtCWl4pHgFl3+WdYCn16+3teG09DY6XtEpf3gGD0a186g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.26.0",
"@typescript-eslint/utils": "8.26.0",
"@typescript-eslint/typescript-estree": "8.25.0",
"@typescript-eslint/utils": "8.25.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.0.1"
},
@@ -1611,13 +1611,13 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
"typescript": ">=4.8.4 <5.8.0"
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.26.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.26.0.tgz",
"integrity": "sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==",
"version": "8.25.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.25.0.tgz",
"integrity": "sha512-+vUe0Zb4tkNgznQwicsvLUJgZIRs6ITeWSCclX1q85pR1iOiaj+4uZJIUp//Z27QWu5Cseiw3O3AR8hVpax7Aw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1629,14 +1629,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.26.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.26.0.tgz",
"integrity": "sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==",
"version": "8.25.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.25.0.tgz",
"integrity": "sha512-ZPaiAKEZ6Blt/TPAx5Ot0EIB/yGtLI2EsGoY6F7XKklfMxYQyvtL+gT/UCqkMzO0BVFHLDlzvFqQzurYahxv9Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.26.0",
"@typescript-eslint/visitor-keys": "8.26.0",
"@typescript-eslint/types": "8.25.0",
"@typescript-eslint/visitor-keys": "8.25.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -1652,20 +1652,20 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
"typescript": ">=4.8.4 <5.8.0"
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.26.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.26.0.tgz",
"integrity": "sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==",
"version": "8.25.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.25.0.tgz",
"integrity": "sha512-syqRbrEv0J1wywiLsK60XzHnQe/kRViI3zwFALrNEgnntn1l24Ra2KvOAWwWbWZ1lBZxZljPDGOq967dsl6fkA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.26.0",
"@typescript-eslint/types": "8.26.0",
"@typescript-eslint/typescript-estree": "8.26.0"
"@typescript-eslint/scope-manager": "8.25.0",
"@typescript-eslint/types": "8.25.0",
"@typescript-eslint/typescript-estree": "8.25.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1676,17 +1676,17 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
"typescript": ">=4.8.4 <5.8.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.26.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.26.0.tgz",
"integrity": "sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==",
"version": "8.25.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.25.0.tgz",
"integrity": "sha512-kCYXKAum9CecGVHGij7muybDfTS2sD3t0L4bJsEZLkyrXUImiCTq1M3LG2SRtOhiHFwMR9wAFplpT6XHYjTkwQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.26.0",
"@typescript-eslint/types": "8.25.0",
"eslint-visitor-keys": "^4.2.0"
},
"engines": {
@@ -1711,9 +1711,9 @@
}
},
"node_modules/@vitest/coverage-v8": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.8.tgz",
"integrity": "sha512-y7SAKsQirsEJ2F8bulBck4DoluhI2EEgTimHd6EEUgJBGKy9tC25cpywh1MH4FvDGoG2Unt7+asVd1kj4qOSAw==",
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.7.tgz",
"integrity": "sha512-Av8WgBJLTrfLOer0uy3CxjlVuWK4CzcLBndW1Nm2vI+3hZ2ozHututkfc7Blu1u6waeQ7J8gzPK/AsBRnWA5mQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1734,8 +1734,8 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@vitest/browser": "3.0.8",
"vitest": "3.0.8"
"@vitest/browser": "3.0.7",
"vitest": "3.0.7"
},
"peerDependenciesMeta": {
"@vitest/browser": {
@@ -1744,14 +1744,14 @@
}
},
"node_modules/@vitest/expect": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.8.tgz",
"integrity": "sha512-Xu6TTIavTvSSS6LZaA3EebWFr6tsoXPetOWNMOlc7LO88QVVBwq2oQWBoDiLCN6YTvNYsGSjqOO8CAdjom5DCQ==",
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.7.tgz",
"integrity": "sha512-QP25f+YJhzPfHrHfYHtvRn+uvkCFCqFtW9CktfBxmB+25QqWsx7VB2As6f4GmwllHLDhXNHvqedwhvMmSnNmjw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "3.0.8",
"@vitest/utils": "3.0.8",
"@vitest/spy": "3.0.7",
"@vitest/utils": "3.0.7",
"chai": "^5.2.0",
"tinyrainbow": "^2.0.0"
},
@@ -1760,13 +1760,13 @@
}
},
"node_modules/@vitest/mocker": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.8.tgz",
"integrity": "sha512-n3LjS7fcW1BCoF+zWZxG7/5XvuYH+lsFg+BDwwAz0arIwHQJFUEsKBQ0BLU49fCxuM/2HSeBPHQD8WjgrxMfow==",
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.7.tgz",
"integrity": "sha512-qui+3BLz9Eonx4EAuR/i+QlCX6AUZ35taDQgwGkK/Tw6/WgwodSrjN1X2xf69IA/643ZX5zNKIn2svvtZDrs4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "3.0.8",
"@vitest/spy": "3.0.7",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.17"
},
@@ -1787,9 +1787,9 @@
}
},
"node_modules/@vitest/pretty-format": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.8.tgz",
"integrity": "sha512-BNqwbEyitFhzYMYHUVbIvepOyeQOSFA/NeJMIP9enMntkkxLgOcgABH6fjyXG85ipTgvero6noreavGIqfJcIg==",
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.7.tgz",
"integrity": "sha512-CiRY0BViD/V8uwuEzz9Yapyao+M9M008/9oMOSQydwbwb+CMokEq3XVaF3XK/VWaOK0Jm9z7ENhybg70Gtxsmg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1800,13 +1800,13 @@
}
},
"node_modules/@vitest/runner": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.8.tgz",
"integrity": "sha512-c7UUw6gEcOzI8fih+uaAXS5DwjlBaCJUo7KJ4VvJcjL95+DSR1kova2hFuRt3w41KZEFcOEiq098KkyrjXeM5w==",
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.7.tgz",
"integrity": "sha512-WeEl38Z0S2ZcuRTeyYqaZtm4e26tq6ZFqh5y8YD9YxfWuu0OFiGFUbnxNynwLjNRHPsXyee2M9tV7YxOTPZl2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "3.0.8",
"@vitest/utils": "3.0.7",
"pathe": "^2.0.3"
},
"funding": {
@@ -1814,13 +1814,13 @@
}
},
"node_modules/@vitest/snapshot": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.8.tgz",
"integrity": "sha512-x8IlMGSEMugakInj44nUrLSILh/zy1f2/BgH0UeHpNyOocG18M9CWVIFBaXPt8TrqVZWmcPjwfG/ht5tnpba8A==",
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.7.tgz",
"integrity": "sha512-eqTUryJWQN0Rtf5yqCGTQWsCFOQe4eNz5Twsu21xYEcnFJtMU5XvmG0vgebhdLlrHQTSq5p8vWHJIeJQV8ovsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "3.0.8",
"@vitest/pretty-format": "3.0.7",
"magic-string": "^0.30.17",
"pathe": "^2.0.3"
},
@@ -1829,9 +1829,9 @@
}
},
"node_modules/@vitest/spy": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.8.tgz",
"integrity": "sha512-MR+PzJa+22vFKYb934CejhR4BeRpMSoxkvNoDit68GQxRLSf11aT6CTj3XaqUU9rxgWJFnqicN/wxw6yBRkI1Q==",
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.7.tgz",
"integrity": "sha512-4T4WcsibB0B6hrKdAZTM37ekuyFZt2cGbEGd2+L0P8ov15J1/HUsUaqkXEQPNAWr4BtPPe1gI+FYfMHhEKfR8w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1842,13 +1842,13 @@
}
},
"node_modules/@vitest/utils": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.8.tgz",
"integrity": "sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q==",
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.7.tgz",
"integrity": "sha512-xePVpCRfooFX3rANQjwoditoXgWb1MaFbzmGuPP59MK6i13mrnDw/yEIyJudLeW6/38mCNcwCiJIGmpDPibAIg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "3.0.8",
"@vitest/pretty-format": "3.0.7",
"loupe": "^3.1.3",
"tinyrainbow": "^2.0.0"
},
@@ -2429,13 +2429,13 @@
}
},
"node_modules/eslint-config-prettier": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz",
"integrity": "sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==",
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.2.tgz",
"integrity": "sha512-1105/17ZIMjmCOJOPNfVdbXafLCLj3hPmkmB7dLgt7XsQ/zkxSuDerE/xgO3RxoHysR1N1whmquY0lSn2O0VLg==",
"dev": true,
"license": "MIT",
"bin": {
"eslint-config-prettier": "bin/cli.js"
"eslint-config-prettier": "build/bin/cli.js"
},
"peerDependencies": {
"eslint": ">=7.0.0"
@@ -3617,9 +3617,9 @@
}
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz",
"integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==",
"dev": true,
"license": "MIT",
"bin": {
@@ -4278,9 +4278,9 @@
}
},
"node_modules/typescript": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -4349,9 +4349,9 @@
}
},
"node_modules/vite": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.1.tgz",
"integrity": "sha512-n2GnqDb6XPhlt9B8olZPrgMD/es/Nd1RdChF6CBD/fHW6pUyUTt2sQW2fPRX5GiD9XEa6+8A6A4f2vT6pSsE7Q==",
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz",
"integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4421,9 +4421,9 @@
}
},
"node_modules/vite-node": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.8.tgz",
"integrity": "sha512-6PhR4H9VGlcwXZ+KWCdMqbtG649xCPZqfI9j2PsK1FcXgEzro5bGHcVKFCTqPLaNKZES8Evqv4LwvZARsq5qlg==",
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.7.tgz",
"integrity": "sha512-2fX0QwX4GkkkpULXdT1Pf4q0tC1i1lFOyseKoonavXUNlQ77KpW2XqBGGNIm/J4Ows4KxgGJzDguYVPKwG/n5A==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4464,19 +4464,19 @@
}
},
"node_modules/vitest": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.8.tgz",
"integrity": "sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA==",
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.7.tgz",
"integrity": "sha512-IP7gPK3LS3Fvn44x30X1dM9vtawm0aesAa2yBIZ9vQf+qB69NXC5776+Qmcr7ohUXIQuLhk7xQR0aSUIDPqavg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "3.0.8",
"@vitest/mocker": "3.0.8",
"@vitest/pretty-format": "^3.0.8",
"@vitest/runner": "3.0.8",
"@vitest/snapshot": "3.0.8",
"@vitest/spy": "3.0.8",
"@vitest/utils": "3.0.8",
"@vitest/expect": "3.0.7",
"@vitest/mocker": "3.0.7",
"@vitest/pretty-format": "^3.0.7",
"@vitest/runner": "3.0.7",
"@vitest/snapshot": "3.0.7",
"@vitest/spy": "3.0.7",
"@vitest/utils": "3.0.7",
"chai": "^5.2.0",
"debug": "^4.4.0",
"expect-type": "^1.1.0",
@@ -4488,7 +4488,7 @@
"tinypool": "^1.0.2",
"tinyrainbow": "^2.0.0",
"vite": "^5.0.0 || ^6.0.0",
"vite-node": "3.0.8",
"vite-node": "3.0.7",
"why-is-node-running": "^2.3.0"
},
"bin": {
@@ -4504,8 +4504,8 @@
"@edge-runtime/vm": "*",
"@types/debug": "^4.1.12",
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"@vitest/browser": "3.0.8",
"@vitest/ui": "3.0.8",
"@vitest/browser": "3.0.7",
"@vitest/ui": "3.0.7",
"happy-dom": "*",
"jsdom": "*"
},
@@ -4534,9 +4534,9 @@
}
},
"node_modules/vitest-fetch-mock": {
"version": "0.4.5",
"resolved": "https://registry.npmjs.org/vitest-fetch-mock/-/vitest-fetch-mock-0.4.5.tgz",
"integrity": "sha512-nhWdCQIGtaSEUVl96pMm0WggyDGPDv5FUy/Q9Hx3cs2RGmh3Q/uRsLClGbdG3kXBkJ3br5yTUjB2MeW25TwdOA==",
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/vitest-fetch-mock/-/vitest-fetch-mock-0.4.4.tgz",
"integrity": "sha512-i2RNEAKBgnLWwj5DVz8ouzaHaPVg1xaYgAUmU5p+baJ149upnO+yJLPchAiY9ij8hf0PDkJVVke1pftBxmT05g==",
"dev": true,
"license": "MIT",
"engines": {

View File

@@ -95,12 +95,12 @@ services:
image: immich-machine-learning-dev:latest
# extends:
# file: hwaccel.ml.yml
# service: cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference
# service: cpu # set to one of [armnn, cuda, openvino, openvino-wsl, rknn] for accelerated inference
build:
context: ../machine-learning
dockerfile: Dockerfile
args:
- DEVICE=cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference
- DEVICE=cpu # set to one of [armnn, cuda, openvino, openvino-wsl, rknn] for accelerated inference
ports:
- 3003:3003
volumes:

View File

@@ -38,12 +38,12 @@ services:
image: immich-machine-learning:latest
# extends:
# file: hwaccel.ml.yml
# service: cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference
# service: cpu # set to one of [armnn, cuda, openvino, openvino-wsl, rknn] for accelerated inference
build:
context: ../machine-learning
dockerfile: Dockerfile
args:
- DEVICE=cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference
- DEVICE=cpu # set to one of [armnn, cuda, openvino, openvino-wsl, rknn] for accelerated inference
ports:
- 3003:3003
volumes:
@@ -77,22 +77,12 @@ services:
- 5432:5432
healthcheck:
test: >-
pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1;
Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align
--command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')";
echo "checksum failure count is $$Chksum";
[ "$$Chksum" = '0' ] || exit 1
pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1; Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
command: >-
postgres
-c shared_preload_libraries=vectors.so
-c 'search_path="$$user", public, vectors'
-c logging_collector=on
-c max_wal_size=2GB
-c shared_buffers=512MB
-c wal_compression=on
postgres -c shared_preload_libraries=vectors.so -c 'search_path="$$user", public, vectors' -c logging_collector=on -c max_wal_size=2GB -c shared_buffers=512MB -c wal_compression=on
restart: always
# set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics
@@ -109,7 +99,7 @@ services:
# add data source for http://immich-prometheus:9090 to get started
immich-grafana:
container_name: immich_grafana
command: ['./run.sh', '-disable-reporting']
command: [ './run.sh', '-disable-reporting' ]
ports:
- 3000:3000
image: grafana/grafana:11.5.2-ubuntu@sha256:8b5858c447e06fd7a89006b562ba7bba7c4d5813600c7982374c41852adefaeb

View File

@@ -33,12 +33,12 @@ services:
immich-machine-learning:
container_name: immich_machine_learning
# For hardware acceleration, add one of -[armnn, cuda, openvino] to the image tag.
# For hardware acceleration, add one of -[armnn, cuda, openvino, rknn] to the image tag.
# Example tag: ${IMMICH_VERSION:-release}-cuda
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
# extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/ml-hardware-acceleration
# file: hwaccel.ml.yml
# service: cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference - use the `-wsl` version for WSL2 where applicable
# service: cpu # set to one of [armnn, cuda, openvino, openvino-wsl, rknn] for accelerated inference - use the `-wsl` version for WSL2 where applicable
volumes:
- model-cache:/cache
env_file:
@@ -67,22 +67,12 @@ services:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
healthcheck:
test: >-
pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1;
Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align
--command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')";
echo "checksum failure count is $$Chksum";
[ "$$Chksum" = '0' ] || exit 1
pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1; Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
command: >-
postgres
-c shared_preload_libraries=vectors.so
-c 'search_path="$$user", public, vectors'
-c logging_collector=on
-c max_wal_size=2GB
-c shared_buffers=512MB
-c wal_compression=on
postgres -c shared_preload_libraries=vectors.so -c 'search_path="$$user", public, vectors' -c logging_collector=on -c max_wal_size=2GB -c shared_buffers=512MB -c wal_compression=on
restart: always
volumes:

View File

@@ -13,6 +13,13 @@ services:
volumes:
- /lib/firmware/mali_csffw.bin:/lib/firmware/mali_csffw.bin:ro # Mali firmware for your chipset (not always required depending on the driver)
- /usr/lib/libmali.so:/usr/lib/libmali.so:ro # Mali driver for your chipset (always required)
rknn:
security_opt:
- systempaths=unconfined
- apparmor=unconfined
devices:
- /dev/dri:/dev/dri
cpu: {}

View File

@@ -12,6 +12,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
- ARM NN (Mali)
- CUDA (NVIDIA GPUs with [compute capability](https://developer.nvidia.com/cuda-gpus) 5.2 or higher)
- OpenVINO (Intel GPUs such as Iris Xe and Arc)
- RKNN (Rockchip)
## Limitations
@@ -19,6 +20,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
- Only Linux and Windows (through WSL2) servers are supported.
- ARM NN is only supported on devices with Mali GPUs. Other Arm devices are not supported.
- Some models may not be compatible with certain backends. CUDA is the most reliable.
- Search latency isn't improved by ARM NN due to model compatibility issues preventing its use. However, smart search jobs do make use of ARM NN.
## Prerequisites
@@ -33,6 +35,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
- The `hwaccel.ml.yml` file assumes the path to it is `/usr/lib/libmali.so`, so update accordingly if it is elsewhere
- The `hwaccel.ml.yml` file assumes an additional file `/lib/firmware/mali_csffw.bin`, so update accordingly if your device's driver does not require this file
- Optional: Configure your `.env` file, see [environment variables](/docs/install/environment-variables) for ARM NN specific settings
- In particular, the `MACHINE_LEARNING_ANN_FP16_TURBO` can significantly improve performance at the cost of very slightly lower accuracy
#### CUDA
@@ -47,6 +50,16 @@ You do not need to redo any machine learning jobs after enabling hardware accele
- Ensure the server's kernel version is new enough to use the device for hardware accceleration.
- Expect higher RAM usage when using OpenVINO compared to CPU processing.
#### RKNN
- You must have a supported Rockchip SoC: only RK3566, RK3568, RK3576 and RK3588 are supported at this moment.
- Make sure you have the appropriate linux kernel driver installed
- This is usually pre-installed on the device vendor's Linux images
- RKNPU driver V0.9.8 or later must be available in the host server
- You may confirm this by running `cat /sys/kernel/debug/rknpu/version` to check the version
- Optional: Configure your `.env` file, see [environment variables](/docs/install/environment-variables) for RKNN specific settings
- In particular, setting `MACHINE_LEARNING_RKNN_THREADS` to 2 or 3 can _dramatically_ improve performance for RK3576 and RK3588 compared to the default of 1, at the expense of multiplying the amount of RAM each model uses by that amount.
## Setup
1. If you do not already have it, download the latest [`hwaccel.ml.yml`][hw-file] file and ensure it's in the same folder as the `docker-compose.yml`.
@@ -127,3 +140,12 @@ Note that you should increase job concurrencies to increase overall utilization
- If you encounter an error when a model is running, try a different model to see if the issue is model-specific.
- You may want to increase concurrency past the default for higher utilization. However, keep in mind that this will also increase VRAM consumption.
- Larger models benefit more from hardware acceleration, if you have the VRAM for them.
- Compared to ARM NN, RKNPU has:
- Wider model support (including for search, which ARM NN does not accelerate)
- Less heat generation
- Very slightly lower accuracy (RKNPU always uses FP16, while ARM NN by default uses higher precision FP32 unless `MACHINE_LEARNING_ANN_FP16_TURBO` is enabled)
- Varying speed (tested on RK3588):
- If `MACHINE_LEARNING_RKNN_THREADS` is at the default of 1, RKNPU will have substantially lower throughput for ML jobs than ARM NN in most cases, but similar latency (such as when searching)
- If `MACHINE_LEARNING_RKNN_THREADS` is set to 3, it will be somewhat faster than ARM NN at FP32, but somewhat slower than ARM NN if `MACHINE_LEARNING_ANN_FP16_TURBO` is enabled
- When other tasks also use the GPU (like transcoding), RKNPU has a significant advantage over ARM NN as it uses the otherwise idle NPU instead of competing for GPU usage
- Lower RAM usage if `MACHINE_LEARNING_RKNN_THREADS` is at the default of 1, but significantly higher if greater than 1 (which is necessary for it to fully utilize the NPU and hence be comparable in speed to ARM NN)

View File

@@ -170,6 +170,8 @@ Redis (Sentinel) URL example JSON before encoding:
| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning |
| `MACHINE_LEARNING_PING_TIMEOUT` | How long (ms) to wait for a PING response when checking if an ML server is available | `2000` | server |
| `MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME` | How long to ignore ML servers that are offline before trying again | `30000` | server |
| `MACHINE_LEARNING_RKNN` | Enable RKNN hardware acceleration if supported | `True` | machine learning |
| `MACHINE_LEARNING_RKNN_THREADS` | How many threads of RKNN runtime should be spinned up while inferencing. | `1` | machine learning |
\*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.

View File

@@ -15734,9 +15734,9 @@
}
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz",
"integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==",
"dev": true,
"license": "MIT",
"bin": {

View File

@@ -1,10 +1,12 @@
import React from 'react';
import Link from '@docusaurus/Link';
import Layout from '@theme/Layout';
import { useColorMode } from '@docusaurus/theme-common';
import { discordPath, discordViewBox } from '@site/src/components/svg-paths';
import ThemedImage from '@theme/ThemedImage';
import Icon from '@mdi/react';
function HomepageHeader() {
const { isDarkTheme } = useColorMode();
return (
<header>
<div className="top-[calc(12%)] md:top-[calc(30%)] h-screen w-full absolute -z-10">
@@ -12,8 +14,8 @@ function HomepageHeader() {
<div className="w-full h-[120vh] absolute left-0 top-0 backdrop-blur-3xl bg-immich-bg/40 dark:bg-transparent"></div>
</div>
<section className="text-center pt-12 sm:pt-24 bg-immich-bg/50 dark:bg-immich-dark-bg/80">
<ThemedImage
sources={{ dark: 'img/logomark-dark.svg', light: 'img/logomark-light.svg' }}
<img
src={isDarkTheme ? 'img/logomark-dark.svg' : 'img/logomark-light.svg'}
className="h-[115px] w-[115px] mb-2 antialiased rounded-none"
alt="Immich logo"
/>
@@ -33,6 +35,7 @@ function HomepageHeader() {
sacrificing your privacy.
</p>
</div>
<div className="flex flex-col sm:flex-row place-items-center place-content-center mt-9 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-xl no-underline hover:no-underline text-white hover:text-gray-50 dark:text-immich-dark-bg font-bold uppercase"
@@ -55,6 +58,7 @@ function HomepageHeader() {
Buy Merch
</Link>
</div>
<div className="my-12 flex gap-1 font-medium place-items-center place-content-center text-immich-primary dark:text-immich-dark-primary">
<Icon
path={discordPath}
@@ -63,19 +67,22 @@ function HomepageHeader() {
/>
<Link to="https://discord.immich.app/">Join our Discord</Link>
</div>
<ThemedImage
sources={{ dark: '/img/screenshot-dark.webp', light: '/img/screenshot-light.webp' }}
<img
src={isDarkTheme ? '/img/screenshot-dark.webp' : '/img/screenshot-light.webp'}
alt="screenshots"
className="w-[95%] lg:w-[85%] xl:w-[70%] 2xl:w-[60%] "
/>
<div className="mx-[25%] m-auto my-14 md:my-28">
<hr className="border bg-gray-500 dark:bg-gray-400" />
</div>
<ThemedImage
sources={{ dark: 'img/logomark-dark.svg', light: 'img/logomark-light.svg' }}
<img
src={isDarkTheme ? 'img/logomark-dark.svg' : 'img/logomark-light.svg'}
className="h-[115px] w-[115px] mb-2 antialiased rounded-none"
alt="Immich logo"
/>
<div>
<p className="font-bold text-2xl md:text-5xl ">Download the mobile app</p>
<p className="text-lg">
@@ -94,8 +101,9 @@ function HomepageHeader() {
</a>
</div>
</div>
<ThemedImage
sources={{ dark: '/img/app-qr-code-dark.svg', light: '/img/app-qr-code-light.svg' }}
<img
src={isDarkTheme ? '/img/app-qr-code-dark.svg' : '/img/app-qr-code-light.svg'}
alt="app qr code"
width={'150px'}
className="shadow-lg p-3 my-8 dark:bg-immich-dark-bg "

View File

@@ -1,7 +1,10 @@
import React from 'react';
import Link from '@docusaurus/Link';
import Layout from '@theme/Layout';
import { useColorMode } from '@docusaurus/theme-common';
function HomepageHeader() {
const { isDarkTheme } = useColorMode();
return (
<header>
<section className="max-w-[900px] m-4 p-4 md:p-6 md:m-auto md:my-12 border border-red-400 rounded-2xl bg-slate-200 dark:bg-immich-dark-gray">

614
e2e/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,24 @@
*.zip
*.onnx
*.rknn
*.npy
*_attr__value
*.weight
*.bias
onnx__*
*in_proj_bias
*.proj
*.latent
*.pos_embed
vocab.txt
export/immich_model_exporter/models/**/README.md
tokenizer.json
tokenizer_config.json
special_tokens_map.json
preprocess_cfg.json
config.json
merges.txt
vocab.json
upload/
venv/
__pycache__/

View File

@@ -15,12 +15,13 @@ RUN mkdir /opt/armnn && \
cd /opt/ann && \
sh build.sh
FROM builder-cpu AS builder-rknn
FROM builder-${DEVICE} AS builder
ARG DEVICE
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
VIRTUAL_ENV=/opt/venv
PYTHONUNBUFFERED=1
WORKDIR /usr/src/app
RUN apt-get update && apt-get install -y --no-install-recommends g++
@@ -29,7 +30,7 @@ COPY --from=ghcr.io/astral-sh/uv:latest@sha256:562193a4a9d398f8aedddcb223e583da3
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress --active --link-mode copy
uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress
FROM python:3.11-slim-bookworm@sha256:614c8691ab74150465ec9123378cd4dde7a6e57be9e558c3108df40664667a4c AS prod-cpu
@@ -77,6 +78,10 @@ COPY --from=builder-armnn \
/opt/ann/build.sh \
/opt/armnn/
FROM prod-cpu AS prod-rknn
ADD --checksum=sha256:73993ed4b440460825f21611731564503cc1d5a0c123746477da6cd574f34885 https://github.com/airockchip/rknn-toolkit2/raw/refs/tags/v2.3.0/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so /usr/lib/
FROM prod-${DEVICE} AS prod
ARG DEVICE
@@ -90,17 +95,16 @@ WORKDIR /usr/src/app
ENV TRANSFORMERS_CACHE=/cache \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/opt/venv/bin:$PATH" \
PATH="/usr/src/app/.venv/bin:$PATH" \
PYTHONPATH=/usr/src \
DEVICE=${DEVICE} \
VIRTUAL_ENV=/opt/venv
DEVICE=${DEVICE}
# prevent core dumps
RUN echo "hard core 0" >> /etc/security/limits.conf && \
echo "fs.suid_dumpable 0" >> /etc/sysctl.conf && \
echo 'ulimit -S -c 0 > /dev/null 2>&1' >> /etc/profile
COPY --from=builder /opt/venv /opt/venv
COPY --from=builder /usr/src/app/.venv /usr/src/app/.venv
COPY ann/ann.py /usr/src/ann/ann.py
COPY start.sh log_conf.json gunicorn_conf.py ./
COPY app .
@@ -123,4 +127,4 @@ ENV IMMICH_SOURCE_URL=https://github.com/immich-app/immich/commit/${BUILD_SOURCE
ENTRYPOINT ["tini", "--"]
CMD ["./start.sh"]
HEALTHCHECK CMD python3 healthcheck.py
HEALTHCHECK CMD python3 healthcheck.py

View File

@@ -64,6 +64,8 @@ class Settings(BaseSettings):
ann: bool = True
ann_fp16_turbo: bool = False
ann_tuning_level: int = 2
rknn: bool = True
rknn_threads: int = 1
preload: PreloadModelData | None = None
max_batch_size: MaxBatchSize | None = None

View File

@@ -136,6 +136,12 @@ def ann_session() -> Iterator[mock.Mock]:
yield mocked
@pytest.fixture(scope="function")
def rknn_session() -> Iterator[mock.Mock]:
with mock.patch("app.sessions.rknn.RknnPoolExecutor") as mocked:
yield mocked
@pytest.fixture(scope="function")
def rmtree() -> Iterator[mock.Mock]:
with mock.patch("app.models.base.rmtree", autospec=True) as mocked:

View File

@@ -226,9 +226,9 @@ async def load(model: InferenceModel) -> InferenceModel:
except FileNotFoundError as e:
if model.model_format == ModelFormat.ONNX:
raise e
log.exception(e)
log.warning(
f"{model.model_format.upper()} is available, but model '{model.model_name}' does not support it."
f"{model.model_format.upper()} is available, but model '{model.model_name}' does not support it.",
exc_info=e,
)
model.model_format = ModelFormat.ONNX
model.load()

View File

@@ -8,6 +8,7 @@ from typing import Any, ClassVar
from huggingface_hub import snapshot_download
import ann.ann
import app.sessions.rknn as rknn
from app.sessions.ort import OrtSession
from ..config import clean_name, log, settings
@@ -66,12 +67,17 @@ class InferenceModel(ABC):
pass
def _download(self) -> None:
ignore_patterns = [] if self.model_format == ModelFormat.ARMNN else ["*.armnn"]
ignored_patterns: dict[ModelFormat, list[str]] = {
ModelFormat.ONNX: ["*.armnn", "*.rknn"],
ModelFormat.ARMNN: ["*.rknn"],
ModelFormat.RKNN: ["*.armnn"],
}
snapshot_download(
f"immich-app/{clean_name(self.model_name)}",
cache_dir=self.cache_dir,
local_dir=self.cache_dir,
ignore_patterns=ignore_patterns,
ignore_patterns=ignored_patterns.get(self.model_format, []),
)
def _load(self) -> ModelSession:
@@ -108,17 +114,25 @@ class InferenceModel(ABC):
session: ModelSession = AnnSession(model_path)
case ".onnx":
session = OrtSession(model_path)
case ".rknn":
session = rknn.RknnSession(model_path)
case _:
raise ValueError(f"Unsupported model file type: {model_path.suffix}")
return session
def model_path_for_format(self, model_format: ModelFormat) -> Path:
model_path_prefix = rknn.model_prefix if model_format == ModelFormat.RKNN else None
if model_path_prefix:
return self.model_dir / model_path_prefix / f"model.{model_format}"
return self.model_dir / f"model.{model_format}"
@property
def model_dir(self) -> Path:
return self.cache_dir / self.model_type.value
@property
def model_path(self) -> Path:
return self.model_dir / f"model.{self.model_format}"
return self.model_path_for_format(self.model_format)
@property
def model_task(self) -> ModelTask:
@@ -155,4 +169,9 @@ class InferenceModel(ABC):
@property
def _model_format_default(self) -> ModelFormat:
return ModelFormat.ARMNN if ann.ann.is_available and settings.ann else ModelFormat.ONNX
if rknn.is_available:
return ModelFormat.RKNN
elif ann.ann.is_available and settings.ann:
return ModelFormat.ARMNN
else:
return ModelFormat.ONNX

View File

@@ -44,6 +44,18 @@ _OPENCLIP_MODELS = {
"nllb-clip-base-siglip__v1",
"nllb-clip-large-siglip__mrl",
"nllb-clip-large-siglip__v1",
"ViT-B-16-SigLIP2__webli",
"ViT-B-32-SigLIP2-256__webli",
"ViT-L-16-SigLIP2-256__webli",
"ViT-L-16-SigLIP2-384__webli",
"ViT-L-16-SigLIP2-512__webli",
"ViT-SO400M-14-SigLIP2-378__webli",
"ViT-SO400M-14-SigLIP2__webli",
"ViT-SO400M-16-SigLIP2-256__webli",
"ViT-SO400M-16-SigLIP2-384__webli",
"ViT-SO400M-16-SigLIP2-512__webli",
"ViT-gopt-16-SigLIP2-256__webli",
"ViT-gopt-16-SigLIP2-384__webli",
}
@@ -65,6 +77,9 @@ _INSIGHTFACE_MODELS = {
SUPPORTED_PROVIDERS = ["CUDAExecutionProvider", "OpenVINOExecutionProvider", "CPUExecutionProvider"]
RKNN_SUPPORTED_SOCS = ["rk3566", "rk3568", "rk3576", "rk3588"]
RKNN_COREMASK_SUPPORTED_SOCS = ["rk3576", "rk3588"]
def get_model_source(model_name: str) -> ModelSource | None:
cleaned_name = clean_name(model_name)

View File

@@ -31,7 +31,7 @@ class FaceRecognizer(InferenceModel):
self._add_batch_axis(self.model_path)
session = self._make_session(self.model_path)
self.model = ArcFaceONNX(
self.model_path.with_suffix(".onnx").as_posix(),
self.model_path_for_format(ModelFormat.ONNX).as_posix(),
session=session,
)
return session

View File

@@ -35,6 +35,7 @@ class ModelType(StrEnum):
class ModelFormat(StrEnum):
ARMNN = "armnn"
ONNX = "onnx"
RKNN = "rknn"
class ModelSource(StrEnum):

View File

@@ -0,0 +1,76 @@
from __future__ import annotations
from pathlib import Path
from typing import Any, NamedTuple
import numpy as np
from numpy.typing import NDArray
from app.config import log, settings
from app.schemas import SessionNode
from .rknnpool import RknnPoolExecutor, is_available, soc_name
is_available = is_available and settings.rknn
model_prefix = Path("rknpu") / soc_name if is_available and soc_name is not None else None
def run_inference(rknn_lite: Any, input: list[NDArray[np.float32]]) -> list[NDArray[np.float32]]:
outputs: list[NDArray[np.float32]] = rknn_lite.inference(inputs=input, data_format="nchw")
return outputs
input_output_mapping: dict[str, dict[str, Any]] = {
"detection": {
"input": {"norm_tensor:0": (1, 3, 640, 640)},
"output": {
"norm_tensor:1": (12800, 1),
"norm_tensor:2": (3200, 1),
"norm_tensor:3": (800, 1),
"norm_tensor:4": (12800, 4),
"norm_tensor:5": (3200, 4),
"norm_tensor:6": (800, 4),
"norm_tensor:7": (12800, 10),
"norm_tensor:8": (3200, 10),
"norm_tensor:9": (800, 10),
},
},
"recognition": {"input": {"norm_tensor:0": (1, 3, 112, 112)}, "output": {"norm_tensor:1": (1, 512)}},
}
class RknnSession:
def __init__(self, model_path: Path) -> None:
self.model_type = "detection" if "detection" in model_path.parts else "recognition"
self.tpe = settings.rknn_threads
log.info(f"Loading RKNN model from {model_path} with {self.tpe} threads.")
self.rknnpool = RknnPoolExecutor(model_path=model_path.as_posix(), tpes=self.tpe, func=run_inference)
log.info(f"Loaded RKNN model from {model_path} with {self.tpe} threads.")
def get_inputs(self) -> list[SessionNode]:
return [RknnNode(name=k, shape=v) for k, v in input_output_mapping[self.model_type]["input"].items()]
def get_outputs(self) -> list[SessionNode]:
return [RknnNode(name=k, shape=v) for k, v in input_output_mapping[self.model_type]["output"].items()]
def run(
self,
output_names: list[str] | None,
input_feed: dict[str, NDArray[np.float32]] | dict[str, NDArray[np.int32]],
run_options: Any = None,
) -> list[NDArray[np.float32]]:
input_data: list[NDArray[np.float32]] = [np.ascontiguousarray(v) for v in input_feed.values()]
self.rknnpool.put(input_data)
res = self.rknnpool.get()
if res is None:
raise RuntimeError("RKNN inference failed!")
return res
class RknnNode(NamedTuple):
name: str | None
shape: tuple[int, ...]
__all__ = ["RknnSession", "RknnNode", "is_available", "soc_name", "model_prefix"]

View File

@@ -0,0 +1,91 @@
# This code is from leafqycc/rknn-multi-threaded
# Following Apache License 2.0
import logging
from concurrent.futures import Future, ThreadPoolExecutor
from pathlib import Path
from queue import Queue
from typing import Callable
import numpy as np
from numpy.typing import NDArray
from app.config import log
from app.models.constants import RKNN_COREMASK_SUPPORTED_SOCS, RKNN_SUPPORTED_SOCS
def get_soc(device_tree_path: Path | str) -> str | None:
try:
with Path(device_tree_path).open() as f:
device_compatible_str = f.read()
for soc in RKNN_SUPPORTED_SOCS:
if soc in device_compatible_str:
return soc
log.warning("Device is not supported for RKNN")
except OSError as e:
log.warning(f"Could not read {device_tree_path}. Reason: %s", e)
return None
soc_name = None
is_available = False
try:
from rknnlite.api import RKNNLite
soc_name = get_soc("/proc/device-tree/compatible")
is_available = soc_name is not None
except ImportError:
log.debug("RKNN is not available")
def init_rknn(model_path: str) -> "RKNNLite":
if not is_available:
raise RuntimeError("rknn is not available!")
rknn_lite = RKNNLite()
rknn_lite.rknn_log.logger.setLevel(logging.ERROR)
ret = rknn_lite.load_rknn(model_path)
if ret != 0:
raise RuntimeError("Failed to load RKNN model")
if soc_name in RKNN_COREMASK_SUPPORTED_SOCS:
ret = rknn_lite.init_runtime(core_mask=RKNNLite.NPU_CORE_AUTO)
else:
ret = rknn_lite.init_runtime() # Please do not set this parameter on other platforms.
if ret != 0:
raise RuntimeError("Failed to inititalize RKNN runtime environment")
return rknn_lite
class RknnPoolExecutor:
def __init__(
self,
model_path: str,
tpes: int,
func: Callable[["RKNNLite", list[NDArray[np.float32]]], list[NDArray[np.float32]]],
) -> None:
self.tpes = tpes
self.queue: Queue[Future[list[NDArray[np.float32]]]] = Queue()
self.rknn_pool = [init_rknn(model_path) for _ in range(tpes)]
self.pool = ThreadPoolExecutor(max_workers=tpes)
self.func = func
self.num = 0
def put(self, inputs: list[NDArray[np.float32]]) -> None:
self.queue.put(self.pool.submit(self.func, self.rknn_pool[self.num % self.tpes], inputs))
self.num += 1
def get(self) -> list[NDArray[np.float32]] | None:
if self.queue.empty():
return None
fut = self.queue.get()
return fut.result()
def release(self) -> None:
self.pool.shutdown()
for rknn_lite in self.rknn_pool:
rknn_lite.release()
def __del__(self) -> None:
self.release()

View File

@@ -25,6 +25,7 @@ from app.models.facial_recognition.detection import FaceDetector
from app.models.facial_recognition.recognition import FaceRecognizer
from app.sessions.ann import AnnSession
from app.sessions.ort import OrtSession
from app.sessions.rknn import RknnSession, run_inference
from .config import Settings, settings
from .models.base import InferenceModel
@@ -69,6 +70,14 @@ class TestBase:
assert encoder.model_format == ModelFormat.ARMNN
def test_sets_default_model_format_to_rknn_if_available(self, mocker: MockerFixture) -> None:
mocker.patch.object(settings, "rknn", True)
mocker.patch("app.sessions.rknn.is_available", True)
encoder = OpenClipTextualEncoder("ViT-B-32__openai")
assert encoder.model_format == ModelFormat.RKNN
def test_casts_cache_dir_string_to_path(self) -> None:
cache_dir = "/test_cache"
encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir=cache_dir)
@@ -125,7 +134,7 @@ class TestBase:
"immich-app/ViT-B-32__openai",
cache_dir=encoder.cache_dir,
local_dir=encoder.cache_dir,
ignore_patterns=["*.armnn"],
ignore_patterns=["*.armnn", "*.rknn"],
)
def test_download_downloads_armnn_if_preferred_format(self, snapshot_download: mock.Mock) -> None:
@@ -136,7 +145,18 @@ class TestBase:
"immich-app/ViT-B-32__openai",
cache_dir=encoder.cache_dir,
local_dir=encoder.cache_dir,
ignore_patterns=[],
ignore_patterns=["*.rknn"],
)
def test_download_downloads_rknn_if_preferred_format(self, snapshot_download: mock.Mock) -> None:
encoder = OpenClipTextualEncoder("ViT-B-32__openai", model_format=ModelFormat.RKNN)
encoder.download()
snapshot_download.assert_called_once_with(
"immich-app/ViT-B-32__openai",
cache_dir=encoder.cache_dir,
local_dir=encoder.cache_dir,
ignore_patterns=["*.armnn"],
)
def test_throws_exception_if_model_path_does_not_exist(
@@ -328,6 +348,33 @@ class TestAnnSession:
np_spy.assert_has_calls([mock.call(input1), mock.call(input2)])
class TestRknnSession:
def test_creates_rknn_session(self, rknn_session: mock.Mock, info: mock.Mock, mocker: MockerFixture) -> None:
model_path = mock.MagicMock(spec=Path)
tpe = 1
mocker.patch("app.sessions.rknn.soc_name", "rk3566")
mocker.patch("app.sessions.rknn.is_available", True)
RknnSession(model_path)
rknn_session.assert_called_once_with(model_path=model_path.as_posix(), tpes=tpe, func=run_inference)
info.assert_has_calls([mock.call(f"Loaded RKNN model from {model_path} with {tpe} threads.")])
def test_run_rknn(self, rknn_session: mock.Mock, mocker: MockerFixture) -> None:
rknn_session.return_value.load.return_value = 123
np_spy = mocker.spy(np, "ascontiguousarray")
mocker.patch("app.sessions.rknn.soc_name", "rk3566")
session = RknnSession(Path("ViT-B-32__openai"))
[input1, input2] = [np.random.rand(1, 3, 224, 224).astype(np.float32) for _ in range(2)]
input_feed = {"input.1": input1, "input.2": input2}
session.run(None, input_feed)
rknn_session.return_value.put.assert_called_once_with([input1, input2])
np_spy.call_count == 2
np_spy.assert_has_calls([mock.call(input1), mock.call(input2)])
class TestCLIP:
embedding = np.random.rand(512).astype(np.float32)
cache_dir = Path("test_cache")
@@ -829,9 +876,7 @@ class TestLoad:
mock_model.clear_cache.assert_not_called()
mock_model.load.assert_not_called()
async def test_falls_back_to_onnx_if_other_format_does_not_exist(
self, exception: mock.Mock, warning: mock.Mock
) -> None:
async def test_falls_back_to_onnx_if_other_format_does_not_exist(self, warning: mock.Mock) -> None:
mock_model = mock.Mock(spec=InferenceModel)
mock_model.model_name = "test_model_name"
mock_model.model_type = ModelType.VISUAL
@@ -846,8 +891,9 @@ class TestLoad:
mock_model.clear_cache.assert_not_called()
assert mock_model.load.call_count == 2
exception.assert_called_once_with(error)
warning.assert_called_once_with("ARMNN is available, but model 'test_model_name' does not support it.")
warning.assert_called_once_with(
"ARMNN is available, but model 'test_model_name' does not support it.", exc_info=error
)
mock_model.model_format = ModelFormat.ONNX

View File

@@ -0,0 +1 @@
3.12

View File

@@ -1,20 +0,0 @@
FROM mambaorg/micromamba:bookworm-slim@sha256:e3797091302382ea841498bc93a7b0a50f7c1448333d5e946d2d1608d0c5f43d AS builder
ENV TRANSFORMERS_CACHE=/cache \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PATH="/opt/venv/bin:$PATH" \
PYTHONPATH=/usr/src
COPY --chown=$MAMBA_USER:$MAMBA_USER conda-lock.yml /tmp/conda-lock.yml
RUN micromamba install -y -n base -f /tmp/conda-lock.yml && \
micromamba remove -y -n base cxx-compiler && \
micromamba clean --all --yes
WORKDIR /usr/src/app
COPY --chown=$MAMBA_USER:$MAMBA_USER start.sh .
COPY --chown=$MAMBA_USER:$MAMBA_USER app .
ENTRYPOINT ["/usr/local/bin/_entrypoint.sh"]
CMD ["./start.sh"]

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +0,0 @@
name: base
channels:
- conda-forge
platforms:
- linux-64
- linux-aarch64
dependencies:
- black
- conda-lock
- mypy
- pytest
- pytest-cov
- pytest-mock
- ruff
category: dev

View File

@@ -1,25 +0,0 @@
name: base
channels:
- conda-forge
- nvidia
- pytorch
platforms:
- linux-64
dependencies:
- cxx-compiler
- onnx==1.*
- onnxruntime==1.*
- open-clip-torch==2.*
- orjson==3.*
- pip
- python==3.11.*
- pytorch>=2.3
- rich==13.*
- safetensors==0.*
- setuptools==68.*
- torchvision
- transformers==4.*
- pip:
- multilingual-clip
- onnxsim
category: main

View File

@@ -0,0 +1,98 @@
from pathlib import Path
import typer
from tenacity import retry, stop_after_attempt, wait_fixed
from typing_extensions import Annotated
from .exporters.constants import DELETE_PATTERNS, SOURCE_TO_METADATA, ModelSource
from .exporters.onnx import export as onnx_export
from .exporters.rknn import export as rknn_export
app = typer.Typer(pretty_exceptions_show_locals=False)
def generate_readme(model_name: str, model_source: ModelSource) -> str:
(name, link, type) = SOURCE_TO_METADATA[model_source]
match model_source:
case ModelSource.MCLIP:
tags = ["immich", "clip", "multilingual"]
case ModelSource.OPENCLIP:
tags = ["immich", "clip"]
lowered = model_name.lower()
if "xlm" in lowered or "nllb" in lowered:
tags.append("multilingual")
case ModelSource.INSIGHTFACE:
tags = ["immich", "facial-recognition"]
case _:
raise ValueError(f"Unsupported model source {model_source}")
return f"""---
tags:
{" - " + "\n - ".join(tags)}
---
# Model Description
This repo contains ONNX exports for the associated {type} model by {name}. See the [{name}]({link}) repo for more info.
This repo is specifically intended for use with [Immich](https://immich.app/), a self-hosted photo library.
"""
@app.command()
def main(
model_name: str,
model_source: ModelSource,
output_dir: Path = Path("./models"),
no_cache: bool = False,
hf_organization: str = "immich-app",
hf_auth_token: Annotated[str | None, typer.Option(envvar="HF_AUTH_TOKEN")] = None,
) -> None:
hf_model_name = model_name.split("/")[-1]
hf_model_name = hf_model_name.replace("xlm-roberta-large", "XLM-Roberta-Large")
hf_model_name = hf_model_name.replace("xlm-roberta-base", "XLM-Roberta-Base")
output_dir = output_dir / hf_model_name
match model_source:
case ModelSource.MCLIP | ModelSource.OPENCLIP:
output_dir.mkdir(parents=True, exist_ok=True)
onnx_export(model_name, model_source, output_dir, no_cache=no_cache)
case ModelSource.INSIGHTFACE:
from huggingface_hub import snapshot_download
# TODO: start from insightface dump instead of downloading from HF
snapshot_download(f"immich-app/{hf_model_name}", local_dir=output_dir)
case _:
raise ValueError(f"Unsupported model source {model_source}")
try:
rknn_export(output_dir, no_cache=no_cache)
except Exception as e:
print(f"Failed to export model {model_name} to rknn: {e}")
(output_dir / "rknpu").unlink(missing_ok=True)
readme_path = output_dir / "README.md"
if no_cache or not readme_path.exists():
with open(readme_path, "w") as f:
f.write(generate_readme(model_name, model_source))
if hf_auth_token is not None:
from huggingface_hub import create_repo, upload_folder
repo_id = f"{hf_organization}/{hf_model_name}"
@retry(stop=stop_after_attempt(5), wait=wait_fixed(5))
def upload_model() -> None:
create_repo(repo_id, exist_ok=True, token=hf_auth_token)
upload_folder(
repo_id=repo_id,
folder_path=output_dir,
# remote repo files to be deleted before uploading
# deletion is in the same commit as the upload, so it's atomic
delete_patterns=DELETE_PATTERNS,
token=hf_auth_token,
)
upload_model()
if __name__ == "__main__":
typer.run(main)

View File

@@ -0,0 +1,42 @@
from enum import StrEnum
from typing import NamedTuple
class ModelSource(StrEnum):
INSIGHTFACE = "insightface"
MCLIP = "mclip"
OPENCLIP = "openclip"
class SourceMetadata(NamedTuple):
name: str
link: str
type: str
SOURCE_TO_METADATA = {
ModelSource.MCLIP: SourceMetadata("M-CLIP", "https://huggingface.co/M-CLIP", "CLIP"),
ModelSource.OPENCLIP: SourceMetadata("OpenCLIP", "https://github.com/mlfoundations/open_clip", "CLIP"),
ModelSource.INSIGHTFACE: SourceMetadata(
"InsightFace", "https://github.com/deepinsight/insightface/tree/master", "facial recognition"
),
}
RKNN_SOCS = ["rk3566", "rk3568", "rk3576", "rk3588"]
# glob to delete old UUID blobs when reuploading models
_uuid_char = "[a-fA-F0-9]"
_uuid_glob = _uuid_char * 8 + "-" + _uuid_char * 4 + "-" + _uuid_char * 4 + "-" + _uuid_char * 4 + "-" + _uuid_char * 12
DELETE_PATTERNS = [
"**/*onnx*",
"**/Constant*",
"**/*.weight",
"**/*.bias",
"**/*.proj",
"**/*in_proj_bias",
"**/*.npy",
"**/*.latent",
"**/*.pos_embed",
f"**/{_uuid_glob}",
]

View File

@@ -0,0 +1,20 @@
from pathlib import Path
from ..constants import ModelSource
from .models import mclip, openclip
def export(
model_name: str, model_source: ModelSource, output_dir: Path, opset_version: int = 19, no_cache: bool = False
) -> None:
visual_dir = output_dir / "visual"
textual_dir = output_dir / "textual"
match model_source:
case ModelSource.MCLIP:
mclip.to_onnx(model_name, opset_version, visual_dir, textual_dir, no_cache=no_cache)
case ModelSource.OPENCLIP:
name, _, pretrained = model_name.partition("__")
config = openclip.OpenCLIPModelConfig(name, pretrained)
openclip.to_onnx(config, opset_version, visual_dir, textual_dir, no_cache=no_cache)
case _:
raise ValueError(f"Unsupported model source {model_source}")

View File

@@ -1,11 +1,6 @@
import os
import tempfile
import warnings
from pathlib import Path
import torch
from multilingual_clip.pt_multilingual_clip import MultilingualCLIP
from transformers import AutoTokenizer
from typing import Any
from .openclip import OpenCLIPModelConfig
from .openclip import to_onnx as openclip_to_onnx
@@ -21,25 +16,40 @@ _MCLIP_TO_OPENCLIP = {
def to_onnx(
model_name: str,
opset_version: int,
output_dir_visual: Path | str,
output_dir_textual: Path | str,
no_cache: bool = False,
) -> tuple[Path, Path]:
textual_path = get_model_path(output_dir_textual)
with tempfile.TemporaryDirectory() as tmpdir:
model = MultilingualCLIP.from_pretrained(model_name, cache_dir=os.environ.get("CACHE_DIR", tmpdir))
if no_cache or not textual_path.exists():
import torch
from multilingual_clip.pt_multilingual_clip import MultilingualCLIP
from transformers import AutoTokenizer
torch.backends.mha.set_fastpath_enabled(False)
model = MultilingualCLIP.from_pretrained(model_name)
AutoTokenizer.from_pretrained(model_name).save_pretrained(output_dir_textual)
model.eval()
for param in model.parameters():
param.requires_grad_(False)
export_text_encoder(model, textual_path)
visual_path, _ = openclip_to_onnx(_MCLIP_TO_OPENCLIP[model_name], output_dir_visual)
assert visual_path is not None, "Visual model export failed"
_export_text_encoder(model, textual_path, opset_version)
else:
print(f"Model {textual_path} already exists, skipping")
visual_path, _ = openclip_to_onnx(
_MCLIP_TO_OPENCLIP[model_name], opset_version, output_dir_visual, no_cache=no_cache
)
assert visual_path is not None, "Visual model export failed"
return visual_path, textual_path
def export_text_encoder(model: MultilingualCLIP, output_path: Path | str) -> None:
def _export_text_encoder(model: Any, output_path: Path | str, opset_version: int) -> None:
import torch
from multilingual_clip.pt_multilingual_clip import MultilingualCLIP
output_path = Path(output_path)
def forward(self: MultilingualCLIP, input_ids: torch.Tensor, attention_mask: torch.Tensor) -> torch.Tensor:
@@ -61,7 +71,7 @@ def export_text_encoder(model: MultilingualCLIP, output_path: Path | str) -> Non
output_path.as_posix(),
input_names=["input_ids", "attention_mask"],
output_names=["embedding"],
opset_version=17,
opset_version=opset_version,
# dynamic_axes={
# "input_ids": {0: "batch_size", 1: "sequence_length"},
# "attention_mask": {0: "batch_size", 1: "sequence_length"},

View File

@@ -0,0 +1,153 @@
import warnings
from dataclasses import dataclass
from functools import cached_property
from pathlib import Path
from typing import Any
from .util import get_model_path, save_config
@dataclass
class OpenCLIPModelConfig:
name: str
pretrained: str
@cached_property
def model_config(self) -> dict[str, Any]:
import open_clip
config: dict[str, Any] | None = open_clip.get_model_config(self.name)
if config is None:
raise ValueError(f"Unknown model {self.name}")
return config
@property
def image_size(self) -> int:
image_size: int = self.model_config["vision_cfg"]["image_size"]
return image_size
@property
def sequence_length(self) -> int:
context_length: int = self.model_config["text_cfg"].get("context_length", 77)
return context_length
def to_onnx(
model_cfg: OpenCLIPModelConfig,
opset_version: int,
output_dir_visual: Path | str | None = None,
output_dir_textual: Path | str | None = None,
no_cache: bool = False,
) -> tuple[Path | None, Path | None]:
visual_path = None
textual_path = None
if output_dir_visual is not None:
output_dir_visual = Path(output_dir_visual)
visual_path = get_model_path(output_dir_visual)
if output_dir_textual is not None:
output_dir_textual = Path(output_dir_textual)
textual_path = get_model_path(output_dir_textual)
if not no_cache and (
(textual_path is None or textual_path.exists()) and (visual_path is None or visual_path.exists())
):
print(f"Models {textual_path} and {visual_path} already exist, skipping")
return visual_path, textual_path
import open_clip
import torch
from transformers import AutoTokenizer
torch.backends.mha.set_fastpath_enabled(False)
model = open_clip.create_model(
model_cfg.name,
pretrained=model_cfg.pretrained,
jit=False,
require_pretrained=True,
)
text_vision_cfg = open_clip.get_model_config(model_cfg.name)
model.eval()
for param in model.parameters():
param.requires_grad_(False)
if visual_path is not None and output_dir_visual is not None:
if no_cache or not visual_path.exists():
save_config(
open_clip.get_model_preprocess_cfg(model),
output_dir_visual / "preprocess_cfg.json",
)
save_config(text_vision_cfg, output_dir_visual.parent / "config.json")
_export_image_encoder(model, model_cfg, visual_path, opset_version)
else:
print(f"Model {visual_path} already exists, skipping")
if textual_path is not None and output_dir_textual is not None:
if no_cache or not textual_path.exists():
tokenizer_name = text_vision_cfg["text_cfg"].get("hf_tokenizer_name", "openai/clip-vit-base-patch32")
AutoTokenizer.from_pretrained(tokenizer_name).save_pretrained(output_dir_textual)
_export_text_encoder(model, model_cfg, textual_path, opset_version)
else:
print(f"Model {textual_path} already exists, skipping")
return visual_path, textual_path
def _export_image_encoder(
model: Any, model_cfg: OpenCLIPModelConfig, output_path: Path | str, opset_version: int
) -> None:
import torch
output_path = Path(output_path)
def encode_image(image: torch.Tensor) -> torch.Tensor:
output = model.encode_image(image, normalize=True)
assert isinstance(output, torch.Tensor)
return output
model.forward = encode_image
args = (torch.randn(1, 3, model_cfg.image_size, model_cfg.image_size),)
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
torch.onnx.export(
model,
args,
output_path.as_posix(),
input_names=["image"],
output_names=["embedding"],
opset_version=opset_version,
# dynamic_axes={"image": {0: "batch_size"}},
)
def _export_text_encoder(
model: Any, model_cfg: OpenCLIPModelConfig, output_path: Path | str, opset_version: int
) -> None:
import torch
output_path = Path(output_path)
def encode_text(text: torch.Tensor) -> torch.Tensor:
output = model.encode_text(text, normalize=True)
assert isinstance(output, torch.Tensor)
return output
model.forward = encode_text
args = (torch.ones(1, model_cfg.sequence_length, dtype=torch.int32),)
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
torch.onnx.export(
model,
args,
output_path.as_posix(),
input_names=["text"],
output_names=["embedding"],
opset_version=opset_version,
# dynamic_axes={"text": {0: "batch_size"}},
)

View File

@@ -0,0 +1,96 @@
from pathlib import Path
from .constants import RKNN_SOCS
def _export_platform(
model_dir: Path,
target_platform: str,
inputs: list[str] | None = None,
input_size_list: list[list[int]] | None = None,
fuse_matmul_softmax_matmul_to_sdpa: bool = True,
no_cache: bool = False,
) -> None:
from rknn.api import RKNN
input_path = model_dir / "model.onnx"
output_path = model_dir / "rknpu" / target_platform / "model.rknn"
if not no_cache and output_path.exists():
print(f"Model {input_path} already exists at {output_path}, skipping")
return
print(f"Exporting model {input_path} to {output_path}")
rknn = RKNN(verbose=False)
rknn.config(
target_platform=target_platform,
disable_rules=["fuse_matmul_softmax_matmul_to_sdpa"] if not fuse_matmul_softmax_matmul_to_sdpa else [],
enable_flash_attention=False,
model_pruning=True,
)
ret = rknn.load_onnx(model=input_path.as_posix(), inputs=inputs, input_size_list=input_size_list)
if ret != 0:
raise RuntimeError("Load failed!")
ret = rknn.build(do_quantization=False)
if ret != 0:
raise RuntimeError("Build failed!")
output_path.parent.mkdir(parents=True, exist_ok=True)
ret = rknn.export_rknn(output_path.as_posix())
if ret != 0:
raise RuntimeError("Export rknn model failed!")
def _export_platforms(
model_dir: Path,
inputs: list[str] | None = None,
input_size_list: list[list[int]] | None = None,
no_cache: bool = False,
) -> None:
fuse_matmul_softmax_matmul_to_sdpa = True
for soc in RKNN_SOCS:
try:
_export_platform(
model_dir,
soc,
inputs=inputs,
input_size_list=input_size_list,
fuse_matmul_softmax_matmul_to_sdpa=fuse_matmul_softmax_matmul_to_sdpa,
no_cache=no_cache,
)
except Exception as e:
print(f"Failed to export model for {soc}: {e}")
if "inputs or 'outputs' must be set" in str(e):
print("Retrying without fuse_matmul_softmax_matmul_to_sdpa")
fuse_matmul_softmax_matmul_to_sdpa = False
_export_platform(
model_dir,
soc,
inputs=inputs,
input_size_list=input_size_list,
fuse_matmul_softmax_matmul_to_sdpa=fuse_matmul_softmax_matmul_to_sdpa,
no_cache=no_cache,
)
def export(model_dir: Path, no_cache: bool = False) -> None:
textual = model_dir / "textual"
visual = model_dir / "visual"
detection = model_dir / "detection"
recognition = model_dir / "recognition"
if textual.is_dir():
_export_platforms(textual, no_cache=no_cache)
if visual.is_dir():
_export_platforms(visual, no_cache=no_cache)
if detection.is_dir():
_export_platforms(detection, inputs=["input.1"], input_size_list=[[1, 3, 640, 640]], no_cache=no_cache)
if recognition.is_dir():
_export_platforms(recognition, inputs=["input.1"], input_size_list=[[1, 3, 112, 112]], no_cache=no_cache)

View File

@@ -0,0 +1,88 @@
import subprocess
from exporters.constants import ModelSource
mclip = [
"M-CLIP/LABSE-Vit-L-14",
"M-CLIP/XLM-Roberta-Large-Vit-B-16Plus",
"M-CLIP/XLM-Roberta-Large-Vit-B-32",
"M-CLIP/XLM-Roberta-Large-Vit-L-14",
]
openclip = [
"RN101__openai",
"RN101__yfcc15m",
"RN50__cc12m",
"RN50__openai",
"RN50__yfcc15m",
"RN50x16__openai",
"RN50x4__openai",
"RN50x64__openai",
"ViT-B-16-SigLIP-256__webli",
"ViT-B-16-SigLIP-384__webli",
"ViT-B-16-SigLIP-512__webli",
"ViT-B-16-SigLIP-i18n-256__webli",
"ViT-B-16-SigLIP2__webli",
"ViT-B-16-SigLIP__webli",
"ViT-B-16-plus-240__laion400m_e31",
"ViT-B-16-plus-240__laion400m_e32",
"ViT-B-16__laion400m_e31",
"ViT-B-16__laion400m_e32",
"ViT-B-16__openai",
"ViT-B-32-SigLIP2-256__webli",
"ViT-B-32__laion2b-s34b-b79k",
"ViT-B-32__laion2b_e16",
"ViT-B-32__laion400m_e31",
"ViT-B-32__laion400m_e32",
"ViT-B-32__openai",
"ViT-H-14-378-quickgelu__dfn5b",
"ViT-H-14-quickgelu__dfn5b",
"ViT-H-14__laion2b-s32b-b79k",
"ViT-L-14-336__openai",
"ViT-L-14-quickgelu__dfn2b",
"ViT-L-14__laion2b-s32b-b82k",
"ViT-L-14__laion400m_e31",
"ViT-L-14__laion400m_e32",
"ViT-L-14__openai",
"ViT-L-16-SigLIP-256__webli",
"ViT-L-16-SigLIP-384__webli",
"ViT-L-16-SigLIP2-256__webli",
"ViT-L-16-SigLIP2-384__webli",
"ViT-L-16-SigLIP2-512__webli",
"ViT-SO400M-14-SigLIP-384__webli",
"ViT-SO400M-14-SigLIP2-378__webli",
"ViT-SO400M-14-SigLIP2__webli",
"ViT-SO400M-16-SigLIP2-256__webli",
"ViT-SO400M-16-SigLIP2-384__webli",
"ViT-SO400M-16-SigLIP2-512__webli",
"ViT-gopt-16-SigLIP2-256__webli",
"ViT-gopt-16-SigLIP2-384__webli",
"nllb-clip-base-siglip__mrl",
"nllb-clip-base-siglip__v1",
"nllb-clip-large-siglip__mrl",
"nllb-clip-large-siglip__v1",
"xlm-roberta-base-ViT-B-32__laion5b_s13b_b90k",
"xlm-roberta-large-ViT-H-14__frozen_laion5b_s13b_b90k",
]
insightface = [
"antelopev2",
"buffalo_l",
"buffalo_m",
"buffalo_s",
]
def export_models(models: list[str], source: ModelSource) -> None:
for model in models:
try:
print(f"Exporting model {model}")
subprocess.check_call(["python", "-m", "immich_model_exporter.export", model, source])
except Exception as e:
print(f"Failed to export model {model}: {e}")
if __name__ == "__main__":
export_models(mclip, ModelSource.MCLIP)
export_models(openclip, ModelSource.OPENCLIP)
export_models(insightface, ModelSource.INSIGHTFACE)

View File

@@ -1,114 +0,0 @@
import os
import tempfile
import warnings
from dataclasses import dataclass, field
from pathlib import Path
import open_clip
import torch
from transformers import AutoTokenizer
from .util import get_model_path, save_config
@dataclass
class OpenCLIPModelConfig:
name: str
pretrained: str
image_size: int = field(init=False)
sequence_length: int = field(init=False)
def __post_init__(self) -> None:
open_clip_cfg = open_clip.get_model_config(self.name)
if open_clip_cfg is None:
raise ValueError(f"Unknown model {self.name}")
self.image_size = open_clip_cfg["vision_cfg"]["image_size"]
self.sequence_length = open_clip_cfg["text_cfg"].get("context_length", 77)
def to_onnx(
model_cfg: OpenCLIPModelConfig,
output_dir_visual: Path | str | None = None,
output_dir_textual: Path | str | None = None,
) -> tuple[Path | None, Path | None]:
visual_path = None
textual_path = None
with tempfile.TemporaryDirectory() as tmpdir:
model = open_clip.create_model(
model_cfg.name,
pretrained=model_cfg.pretrained,
jit=False,
cache_dir=os.environ.get("CACHE_DIR", tmpdir),
require_pretrained=True,
)
text_vision_cfg = open_clip.get_model_config(model_cfg.name)
model.eval()
for param in model.parameters():
param.requires_grad_(False)
if output_dir_visual is not None:
output_dir_visual = Path(output_dir_visual)
visual_path = get_model_path(output_dir_visual)
save_config(open_clip.get_model_preprocess_cfg(model), output_dir_visual / "preprocess_cfg.json")
save_config(text_vision_cfg, output_dir_visual.parent / "config.json")
export_image_encoder(model, model_cfg, visual_path)
if output_dir_textual is not None:
output_dir_textual = Path(output_dir_textual)
textual_path = get_model_path(output_dir_textual)
tokenizer_name = text_vision_cfg["text_cfg"].get("hf_tokenizer_name", "openai/clip-vit-base-patch32")
AutoTokenizer.from_pretrained(tokenizer_name).save_pretrained(output_dir_textual)
export_text_encoder(model, model_cfg, textual_path)
return visual_path, textual_path
def export_image_encoder(model: open_clip.CLIP, model_cfg: OpenCLIPModelConfig, output_path: Path | str) -> None:
output_path = Path(output_path)
def encode_image(image: torch.Tensor) -> torch.Tensor:
output = model.encode_image(image, normalize=True)
assert isinstance(output, torch.Tensor)
return output
args = (torch.randn(1, 3, model_cfg.image_size, model_cfg.image_size),)
traced = torch.jit.trace(encode_image, args) # type: ignore[no-untyped-call]
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
torch.onnx.export(
traced,
args,
output_path.as_posix(),
input_names=["image"],
output_names=["embedding"],
opset_version=17,
# dynamic_axes={"image": {0: "batch_size"}},
)
def export_text_encoder(model: open_clip.CLIP, model_cfg: OpenCLIPModelConfig, output_path: Path | str) -> None:
output_path = Path(output_path)
def encode_text(text: torch.Tensor) -> torch.Tensor:
output = model.encode_text(text, normalize=True)
assert isinstance(output, torch.Tensor)
return output
args = (torch.ones(1, model_cfg.sequence_length, dtype=torch.int32),)
traced = torch.jit.trace(encode_text, args) # type: ignore[no-untyped-call]
with warnings.catch_warnings():
warnings.simplefilter("ignore", UserWarning)
torch.onnx.export(
traced,
args,
output_path.as_posix(),
input_names=["text"],
output_names=["embedding"],
opset_version=17,
# dynamic_axes={"text": {0: "batch_size"}},
)

View File

@@ -1,49 +0,0 @@
from pathlib import Path
import onnx
import onnxruntime as ort
import onnxsim
def save_onnx(model: onnx.ModelProto, output_path: Path | str) -> None:
try:
onnx.save(model, output_path)
except ValueError as e:
if "The proto size is larger than the 2 GB limit." in str(e):
onnx.save(model, output_path, save_as_external_data=True, size_threshold=1_000_000)
else:
raise e
def optimize_onnxsim(model_path: Path | str, output_path: Path | str) -> None:
model_path = Path(model_path)
output_path = Path(output_path)
model = onnx.load(model_path.as_posix())
model, check = onnxsim.simplify(model)
assert check, "Simplified ONNX model could not be validated"
for file in model_path.parent.iterdir():
if file.name.startswith("Constant") or "onnx" in file.name or file.suffix == ".weight":
file.unlink()
save_onnx(model, output_path)
def optimize_ort(
model_path: Path | str,
output_path: Path | str,
level: ort.GraphOptimizationLevel = ort.GraphOptimizationLevel.ORT_ENABLE_BASIC,
) -> None:
model_path = Path(model_path)
output_path = Path(output_path)
sess_options = ort.SessionOptions()
sess_options.graph_optimization_level = level
sess_options.optimized_model_filepath = output_path.as_posix()
ort.InferenceSession(model_path.as_posix(), providers=["CPUExecutionProvider"], sess_options=sess_options)
def optimize(model_path: Path | str) -> None:
model_path = Path(model_path)
optimize_ort(model_path, model_path)
optimize_onnxsim(model_path, model_path)

View File

@@ -0,0 +1,67 @@
[project]
name = "immich_model_exporter"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.10, <4.0"
dependencies = [
"huggingface-hub>=0.29.3",
"multilingual-clip>=1.0.10",
"onnx>=1.14.1",
"onnxruntime>=1.16.0",
"open-clip-torch>=2.31.0",
"typer>=0.15.2",
"rknn-toolkit2>=2.3.0",
"transformers>=4.49.0",
"tenacity>=9.0.0",
]
[dependency-groups]
dev = ["black>=23.3.0", "mypy>=1.3.0", "ruff>=0.0.272"]
[tool.uv]
override-dependencies = [
"onnx>=1.16.0,<2",
"onnxruntime>=1.18.2,<2",
"torch>=2.4",
"torchvision>=0.21",
]
[tool.uv.sources]
torch = [{ index = "pytorch-cpu" }]
torchvision = [{ index = "pytorch-cpu" }]
[[tool.uv.index]]
name = "pytorch-cpu"
url = "https://download.pytorch.org/whl/cpu"
explicit = true
[tool.hatch.build.targets.sdist]
include = ["immich_model_exporter"]
[tool.hatch.build.targets.wheel]
include = ["immich_model_exporter"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.mypy]
python_version = "3.12"
follow_imports = "silent"
warn_redundant_casts = true
disallow_any_generics = true
check_untyped_defs = true
disallow_untyped_defs = true
ignore_missing_imports = true
[tool.ruff]
line-length = 120
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I"]
[tool.black]
line-length = 120
target-version = ['py312']

View File

@@ -1,113 +0,0 @@
import gc
import os
from pathlib import Path
from tempfile import TemporaryDirectory
import torch
from huggingface_hub import create_repo, upload_folder
from models import mclip, openclip
from models.optimize import optimize
from rich.progress import Progress
models = [
"M-CLIP/LABSE-Vit-L-14",
"M-CLIP/XLM-Roberta-Large-Vit-B-16Plus",
"M-CLIP/XLM-Roberta-Large-Vit-B-32",
"M-CLIP/XLM-Roberta-Large-Vit-L-14",
"RN101::openai",
"RN101::yfcc15m",
"RN50::cc12m",
"RN50::openai",
"RN50::yfcc15m",
"RN50x16::openai",
"RN50x4::openai",
"RN50x64::openai",
"ViT-B-16-SigLIP-256::webli",
"ViT-B-16-SigLIP-384::webli",
"ViT-B-16-SigLIP-512::webli",
"ViT-B-16-SigLIP-i18n-256::webli",
"ViT-B-16-SigLIP::webli",
"ViT-B-16-plus-240::laion400m_e31",
"ViT-B-16-plus-240::laion400m_e32",
"ViT-B-16::laion400m_e31",
"ViT-B-16::laion400m_e32",
"ViT-B-16::openai",
"ViT-B-32::laion2b-s34b-b79k",
"ViT-B-32::laion2b_e16",
"ViT-B-32::laion400m_e31",
"ViT-B-32::laion400m_e32",
"ViT-B-32::openai",
"ViT-H-14-378-quickgelu::dfn5b",
"ViT-H-14-quickgelu::dfn5b",
"ViT-H-14::laion2b-s32b-b79k",
"ViT-L-14-336::openai",
"ViT-L-14-quickgelu::dfn2b",
"ViT-L-14::laion2b-s32b-b82k",
"ViT-L-14::laion400m_e31",
"ViT-L-14::laion400m_e32",
"ViT-L-14::openai",
"ViT-L-16-SigLIP-256::webli",
"ViT-L-16-SigLIP-384::webli",
"ViT-SO400M-14-SigLIP-384::webli",
"ViT-g-14::laion2b-s12b-b42k",
"nllb-clip-base-siglip::mrl",
"nllb-clip-base-siglip::v1",
"nllb-clip-large-siglip::mrl",
"nllb-clip-large-siglip::v1",
"xlm-roberta-base-ViT-B-32::laion5b_s13b_b90k",
"xlm-roberta-large-ViT-H-14::frozen_laion5b_s13b_b90k",
]
# glob to delete old UUID blobs when reuploading models
uuid_char = "[a-fA-F0-9]"
uuid_glob = uuid_char * 8 + "-" + uuid_char * 4 + "-" + uuid_char * 4 + "-" + uuid_char * 4 + "-" + uuid_char * 12
# remote repo files to be deleted before uploading
# deletion is in the same commit as the upload, so it's atomic
delete_patterns = ["**/*onnx*", "**/Constant*", "**/*.weight", "**/*.bias", f"**/{uuid_glob}"]
with Progress() as progress:
task = progress.add_task("[green]Exporting models...", total=len(models))
token = os.environ.get("HF_AUTH_TOKEN")
torch.backends.mha.set_fastpath_enabled(False)
with TemporaryDirectory() as tmp:
tmpdir = Path(tmp)
for model in models:
model_name = model.split("/")[-1].replace("::", "__")
hf_model_name = model_name.replace("xlm-roberta-large", "XLM-Roberta-Large")
hf_model_name = model_name.replace("xlm-roberta-base", "XLM-Roberta-Base")
config_path = tmpdir / model_name / "config.json"
def export() -> None:
progress.update(task, description=f"[green]Exporting {hf_model_name}")
visual_dir = tmpdir / hf_model_name / "visual"
textual_dir = tmpdir / hf_model_name / "textual"
if model.startswith("M-CLIP"):
visual_path, textual_path = mclip.to_onnx(model, visual_dir, textual_dir)
else:
name, _, pretrained = model_name.partition("__")
config = openclip.OpenCLIPModelConfig(name, pretrained)
visual_path, textual_path = openclip.to_onnx(config, visual_dir, textual_dir)
progress.update(task, description=f"[green]Optimizing {hf_model_name} (visual)")
optimize(visual_path)
progress.update(task, description=f"[green]Optimizing {hf_model_name} (textual)")
optimize(textual_path)
gc.collect()
def upload() -> None:
progress.update(task, description=f"[yellow]Uploading {hf_model_name}")
repo_id = f"immich-app/{hf_model_name}"
create_repo(repo_id, exist_ok=True)
upload_folder(
repo_id=repo_id,
folder_path=tmpdir / hf_model_name,
delete_patterns=delete_patterns,
token=token,
)
export()
if token is not None:
upload()
progress.update(task, advance=1)

1395
machine-learning/export/uv.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -51,6 +51,7 @@ cpu = ["onnxruntime>=1.15.0,<2"]
cuda = ["onnxruntime-gpu>=1.17.0,<2"]
openvino = ["onnxruntime-openvino>=1.17.1,<1.19.0"]
armnn = ["onnxruntime>=1.15.0,<2"]
rknn = ["onnxruntime>=1.15.0,<2", "rknn-toolkit-lite2>=2.3.0,<3"]
[tool.uv]
compile-bytecode = true

View File

@@ -1109,6 +1109,10 @@ cuda = [
openvino = [
{ name = "onnxruntime-openvino" },
]
rknn = [
{ name = "onnxruntime" },
{ name = "rknn-toolkit-lite2" },
]
[package.dev-dependencies]
dev = [
@@ -1162,6 +1166,7 @@ requires-dist = [
{ name = "insightface", specifier = ">=0.7.3,<1.0" },
{ name = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.15.0,<2" },
{ name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.15.0,<2" },
{ name = "onnxruntime", marker = "extra == 'rknn'", specifier = ">=1.15.0,<2" },
{ name = "onnxruntime-gpu", marker = "extra == 'cuda'", specifier = ">=1.17.0,<2", index = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/" },
{ name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.17.1,<1.19.0" },
{ name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" },
@@ -1171,10 +1176,11 @@ requires-dist = [
{ name = "pydantic-settings", specifier = ">=2.5.2,<3" },
{ name = "python-multipart", specifier = ">=0.0.6,<1.0" },
{ name = "rich", specifier = ">=13.4.2" },
{ name = "rknn-toolkit-lite2", marker = "extra == 'rknn'", specifier = ">=2.3.0,<3" },
{ name = "tokenizers", specifier = ">=0.15.0,<1.0" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.22.0,<1.0" },
]
provides-extras = ["cpu", "cuda", "openvino", "armnn"]
provides-extras = ["cpu", "cuda", "openvino", "armnn", "rknn"]
[package.metadata.requires-dev]
dev = [
@@ -2131,6 +2137,77 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
]
[[package]]
name = "rknn-toolkit-lite2"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "psutil" },
{ name = "ruamel-yaml" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/77/6af374a4a8cd2aee762a1fb8a3050dcf3f129134bbdc4bb6bed755c4325b/rknn_toolkit_lite2-2.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b6733689bd09a262bcb6ba4744e690dd4b37ebeac4ed427cf45242c4b4ce9a4", size = 559372 },
{ url = "https://files.pythonhosted.org/packages/9b/0c/76ff1eb09d09ce4394a6959d2343a321d28dd9e604348ffdafceafdc344c/rknn_toolkit_lite2-2.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3e4fefe355dc34a155680e4bcb9e4abb37ebc271f045ec9e0a4a3a018bc5beb", size = 569149 },
{ url = "https://files.pythonhosted.org/packages/0d/6e/8679562028051b02312212defc6e8c07248953f10dd7ad506e941b575bf3/rknn_toolkit_lite2-2.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37394371d1561f470c553f39869d7c35ff93405dffe3d0d72babf297a2b0aee9", size = 527457 },
]
[[package]]
name = "ruamel-yaml"
version = "0.18.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ruamel-yaml-clib", marker = "python_full_version < '3.13' and platform_python_implementation == 'CPython'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ea/46/f44d8be06b85bc7c4d8c95d658be2b68f27711f279bf9dd0612a5e4794f5/ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58", size = 143447 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/36/dfc1ebc0081e6d39924a2cc53654497f967a084a436bb64402dfce4254d9/ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1", size = 117729 },
]
[[package]]
name = "ruamel-yaml-clib"
version = "0.2.12"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/20/84/80203abff8ea4993a87d823a5f632e4d92831ef75d404c9fc78d0176d2b5/ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f", size = 225315 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/70/57/40a958e863e299f0c74ef32a3bde9f2d1ea8d69669368c0c502a0997f57f/ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5", size = 131301 },
{ url = "https://files.pythonhosted.org/packages/98/a8/29a3eb437b12b95f50a6bcc3d7d7214301c6c529d8fdc227247fa84162b5/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969", size = 633728 },
{ url = "https://files.pythonhosted.org/packages/35/6d/ae05a87a3ad540259c3ad88d71275cbd1c0f2d30ae04c65dcbfb6dcd4b9f/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df", size = 722230 },
{ url = "https://files.pythonhosted.org/packages/7f/b7/20c6f3c0b656fe609675d69bc135c03aac9e3865912444be6339207b6648/ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76", size = 686712 },
{ url = "https://files.pythonhosted.org/packages/cd/11/d12dbf683471f888d354dac59593873c2b45feb193c5e3e0f2ebf85e68b9/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6", size = 663936 },
{ url = "https://files.pythonhosted.org/packages/72/14/4c268f5077db5c83f743ee1daeb236269fa8577133a5cfa49f8b382baf13/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd", size = 696580 },
{ url = "https://files.pythonhosted.org/packages/30/fc/8cd12f189c6405a4c1cf37bd633aa740a9538c8e40497c231072d0fef5cf/ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a", size = 663393 },
{ url = "https://files.pythonhosted.org/packages/80/29/c0a017b704aaf3cbf704989785cd9c5d5b8ccec2dae6ac0c53833c84e677/ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da", size = 100326 },
{ url = "https://files.pythonhosted.org/packages/3a/65/fa39d74db4e2d0cd252355732d966a460a41cd01c6353b820a0952432839/ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28", size = 118079 },
{ url = "https://files.pythonhosted.org/packages/fb/8f/683c6ad562f558cbc4f7c029abcd9599148c51c54b5ef0f24f2638da9fbb/ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6", size = 132224 },
{ url = "https://files.pythonhosted.org/packages/3c/d2/b79b7d695e2f21da020bd44c782490578f300dd44f0a4c57a92575758a76/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e", size = 641480 },
{ url = "https://files.pythonhosted.org/packages/68/6e/264c50ce2a31473a9fdbf4fa66ca9b2b17c7455b31ef585462343818bd6c/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e", size = 739068 },
{ url = "https://files.pythonhosted.org/packages/86/29/88c2567bc893c84d88b4c48027367c3562ae69121d568e8a3f3a8d363f4d/ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52", size = 703012 },
{ url = "https://files.pythonhosted.org/packages/11/46/879763c619b5470820f0cd6ca97d134771e502776bc2b844d2adb6e37753/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642", size = 704352 },
{ url = "https://files.pythonhosted.org/packages/02/80/ece7e6034256a4186bbe50dee28cd032d816974941a6abf6a9d65e4228a7/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2", size = 737344 },
{ url = "https://files.pythonhosted.org/packages/f0/ca/e4106ac7e80efbabdf4bf91d3d32fc424e41418458251712f5672eada9ce/ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3", size = 714498 },
{ url = "https://files.pythonhosted.org/packages/67/58/b1f60a1d591b771298ffa0428237afb092c7f29ae23bad93420b1eb10703/ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4", size = 100205 },
{ url = "https://files.pythonhosted.org/packages/b4/4f/b52f634c9548a9291a70dfce26ca7ebce388235c93588a1068028ea23fcc/ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb", size = 118185 },
{ url = "https://files.pythonhosted.org/packages/48/41/e7a405afbdc26af961678474a55373e1b323605a4f5e2ddd4a80ea80f628/ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632", size = 133433 },
{ url = "https://files.pythonhosted.org/packages/ec/b0/b850385604334c2ce90e3ee1013bd911aedf058a934905863a6ea95e9eb4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d", size = 647362 },
{ url = "https://files.pythonhosted.org/packages/44/d0/3f68a86e006448fb6c005aee66565b9eb89014a70c491d70c08de597f8e4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c", size = 754118 },
{ url = "https://files.pythonhosted.org/packages/52/a9/d39f3c5ada0a3bb2870d7db41901125dbe2434fa4f12ca8c5b83a42d7c53/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd", size = 706497 },
{ url = "https://files.pythonhosted.org/packages/b0/fa/097e38135dadd9ac25aecf2a54be17ddf6e4c23e43d538492a90ab3d71c6/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31", size = 698042 },
{ url = "https://files.pythonhosted.org/packages/ec/d5/a659ca6f503b9379b930f13bc6b130c9f176469b73b9834296822a83a132/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680", size = 745831 },
{ url = "https://files.pythonhosted.org/packages/db/5d/36619b61ffa2429eeaefaab4f3374666adf36ad8ac6330d855848d7d36fd/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d", size = 715692 },
{ url = "https://files.pythonhosted.org/packages/b1/82/85cb92f15a4231c89b95dfe08b09eb6adca929ef7df7e17ab59902b6f589/ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5", size = 98777 },
{ url = "https://files.pythonhosted.org/packages/d7/8f/c3654f6f1ddb75daf3922c3d8fc6005b1ab56671ad56ffb874d908bfa668/ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4", size = 115523 },
{ url = "https://files.pythonhosted.org/packages/29/00/4864119668d71a5fa45678f380b5923ff410701565821925c69780356ffa/ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a", size = 132011 },
{ url = "https://files.pythonhosted.org/packages/7f/5e/212f473a93ae78c669ffa0cb051e3fee1139cb2d385d2ae1653d64281507/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475", size = 642488 },
{ url = "https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef", size = 745066 },
{ url = "https://files.pythonhosted.org/packages/e2/a9/28f60726d29dfc01b8decdb385de4ced2ced9faeb37a847bd5cf26836815/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6", size = 701785 },
{ url = "https://files.pythonhosted.org/packages/84/7e/8e7ec45920daa7f76046578e4f677a3215fe8f18ee30a9cb7627a19d9b4c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf", size = 693017 },
{ url = "https://files.pythonhosted.org/packages/c5/b3/d650eaade4ca225f02a648321e1ab835b9d361c60d51150bac49063b83fa/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1", size = 741270 },
{ url = "https://files.pythonhosted.org/packages/87/b8/01c29b924dcbbed75cc45b30c30d565d763b9c4d540545a0eeecffb8f09c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01", size = 709059 },
{ url = "https://files.pythonhosted.org/packages/30/8c/ed73f047a73638257aa9377ad356bea4d96125b305c34a28766f4445cc0f/ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6", size = 98583 },
{ url = "https://files.pythonhosted.org/packages/b0/85/e8e751d8791564dd333d5d9a4eab0a7a115f7e349595417fd50ecae3395c/ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3", size = 115190 },
]
[[package]]
name = "ruff"
version = "0.9.9"

View File

@@ -67,7 +67,7 @@ custom_lint:
- lib/entities/*.entity.dart
- lib/repositories/{album,asset,backup,database,etag,exif_info,user,timeline,partner}.repository.dart
- lib/infrastructure/entities/*.entity.dart
- lib/infrastructure/repositories/*.repository.dart
- lib/infrastructure/repositories/{store,db,log,exif}.repository.dart
- lib/providers/infrastructure/db.provider.dart
# acceptable exceptions for the time being (until Isar is fully replaced)
- lib/providers/app_life_cycle.provider.dart
@@ -93,7 +93,6 @@ custom_lint:
- lib/infrastructure/utils/*.converter.dart
# acceptable exceptions for the time being
- lib/entities/{album,asset,exif_info,user}.entity.dart # to convert DTOs to entities
- lib/infrastructure/utils/*.converter.dart
- lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine
- test/modules/utils/openapi_patching_test.dart # filename is self-explanatory...
- lib/domain/services/sync_stream.service.dart # Making sure to comply with the type from database

View File

@@ -1,24 +0,0 @@
import 'package:immich_mobile/domain/interfaces/db.interface.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
abstract interface class IUserRepository implements IDatabaseRepository {
Future<bool> insert(UserDto user);
Future<UserDto?> get(int id);
Future<UserDto?> getByUserId(String id);
Future<List<UserDto?>> getByUserIds(List<String> ids);
Future<List<UserDto>> getAll({SortUserBy? sortBy});
Future<bool> updateAll(List<UserDto> users);
Future<UserDto> update(UserDto user);
Future<void> delete(List<int> ids);
Future<void> deleteAll();
}
enum SortUserBy { id }

View File

@@ -1,15 +0,0 @@
import 'dart:typed_data';
import 'package:immich_mobile/domain/models/user.model.dart';
abstract interface class IUserApiRepository {
Future<UserDto?> getMyUser();
Future<List<UserDto>> getAll();
/// Saves the [data] in the server and uses it as the current users profile image
Future<String> createProfileImage({
required String name,
required Uint8List data,
});
}

View File

@@ -1,11 +1,11 @@
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/user.entity.dart';
/// Key for each possible value in the `Store`.
/// Defines the data type for each value
enum StoreKey<T> {
version<int>._(0),
assetETag<String>._(1),
currentUser<UserDto>._(2),
currentUser<User>._(2),
deviceIdHash<int>._(3),
deviceId<String>._(4),
backupFailedSince<DateTime>._(5),

View File

@@ -1,157 +0,0 @@
import 'dart:ui';
import 'package:immich_mobile/utils/hash.dart';
enum AvatarColor {
// do not change this order or reuse indices for other purposes, adding is OK
primary,
pink,
red,
yellow,
blue,
green,
purple,
orange,
gray,
amber;
Color toColor({bool isDarkTheme = false}) => switch (this) {
AvatarColor.primary =>
isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF),
AvatarColor.pink => const Color.fromARGB(255, 244, 114, 182),
AvatarColor.red => const Color.fromARGB(255, 239, 68, 68),
AvatarColor.yellow => const Color.fromARGB(255, 234, 179, 8),
AvatarColor.blue => const Color.fromARGB(255, 59, 130, 246),
AvatarColor.green => const Color.fromARGB(255, 22, 163, 74),
AvatarColor.purple => const Color.fromARGB(255, 147, 51, 234),
AvatarColor.orange => const Color.fromARGB(255, 234, 88, 12),
AvatarColor.gray => const Color.fromARGB(255, 75, 85, 99),
AvatarColor.amber => const Color.fromARGB(255, 217, 119, 6),
};
}
// TODO: Rename to User once Isar is removed
class UserDto {
final String uid;
final String email;
final String name;
final bool isAdmin;
final DateTime updatedAt;
final String? profileImagePath;
final AvatarColor avatarColor;
final bool memoryEnabled;
final bool inTimeline;
final bool isPartnerSharedBy;
final bool isPartnerSharedWith;
final int quotaUsageInBytes;
final int quotaSizeInBytes;
int get id => fastHash(uid);
bool get hasQuota => quotaSizeInBytes > 0;
const UserDto({
required this.uid,
required this.email,
required this.name,
required this.isAdmin,
required this.updatedAt,
this.profileImagePath,
this.avatarColor = AvatarColor.primary,
this.memoryEnabled = true,
this.inTimeline = false,
this.isPartnerSharedBy = false,
this.isPartnerSharedWith = false,
this.quotaUsageInBytes = 0,
this.quotaSizeInBytes = 0,
});
@override
String toString() {
return '''User: {
id: $id,
uid: $uid,
email: $email,
name: $name,
isAdmin: $isAdmin,
updatedAt: $updatedAt,
profileImagePath: ${profileImagePath ?? '<NA>'},
avatarColor: $avatarColor,
memoryEnabled: $memoryEnabled,
inTimeline: $inTimeline,
isPartnerSharedBy: $isPartnerSharedBy,
isPartnerSharedWith: $isPartnerSharedWith,
quotaUsageInBytes: $quotaUsageInBytes,
quotaSizeInBytes: $quotaSizeInBytes,
}''';
}
UserDto copyWith({
String? uid,
String? email,
String? name,
bool? isAdmin,
DateTime? updatedAt,
String? profileImagePath,
AvatarColor? avatarColor,
bool? memoryEnabled,
bool? inTimeline,
bool? isPartnerSharedBy,
bool? isPartnerSharedWith,
int? quotaUsageInBytes,
int? quotaSizeInBytes,
}) =>
UserDto(
uid: uid ?? this.uid,
email: email ?? this.email,
name: name ?? this.name,
isAdmin: isAdmin ?? this.isAdmin,
updatedAt: updatedAt ?? this.updatedAt,
profileImagePath: profileImagePath ?? this.profileImagePath,
avatarColor: avatarColor ?? this.avatarColor,
memoryEnabled: memoryEnabled ?? this.memoryEnabled,
inTimeline: inTimeline ?? this.inTimeline,
isPartnerSharedBy: isPartnerSharedBy ?? this.isPartnerSharedBy,
isPartnerSharedWith: isPartnerSharedWith ?? this.isPartnerSharedWith,
quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes,
quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes,
);
@override
bool operator ==(covariant UserDto other) {
if (identical(this, other)) return true;
return other.uid == uid &&
other.updatedAt.isAtSameMomentAs(updatedAt) &&
other.avatarColor == avatarColor &&
other.email == email &&
other.name == name &&
other.isPartnerSharedBy == isPartnerSharedBy &&
other.isPartnerSharedWith == isPartnerSharedWith &&
other.profileImagePath == profileImagePath &&
other.isAdmin == isAdmin &&
other.memoryEnabled == memoryEnabled &&
other.inTimeline == inTimeline &&
other.quotaUsageInBytes == quotaUsageInBytes &&
other.quotaSizeInBytes == quotaSizeInBytes;
}
@override
int get hashCode =>
uid.hashCode ^
name.hashCode ^
email.hashCode ^
updatedAt.hashCode ^
isAdmin.hashCode ^
profileImagePath.hashCode ^
avatarColor.hashCode ^
memoryEnabled.hashCode ^
inTimeline.hashCode ^
isPartnerSharedBy.hashCode ^
isPartnerSharedWith.hashCode ^
quotaUsageInBytes.hashCode ^
quotaSizeInBytes.hashCode;
}

View File

@@ -74,7 +74,7 @@ class StoreService {
return value;
}
/// Asynchronously stores the value in the Store
/// Asynchronously stores the value in the DB and synchronously in the cache
Future<void> put<U extends StoreKey<T>, T>(U key, T value) async {
if (_cache[key.id] == value) return;
await _storeRepository.insert(key, value);
@@ -84,7 +84,7 @@ class StoreService {
/// Watches a specific key for changes
Stream<T?> watch<T>(StoreKey<T> key) => _storeRepository.watch(key);
/// Removes the value asynchronously from the Store
/// Removes the value asynchronously from the DB and synchronously from the cache
Future<void> delete<T>(StoreKey<T> key) async {
await _storeRepository.delete(key);
_cache.remove(key.id);

View File

@@ -1,64 +0,0 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/interfaces/user_api.repository.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:logging/logging.dart';
class UserService {
final Logger _log = Logger("UserService");
final IUserRepository _userRepository;
final IUserApiRepository _userApiRepository;
final StoreService _storeService;
UserService({
required IUserRepository userRepository,
required IUserApiRepository userApiRepository,
required StoreService storeService,
}) : _userRepository = userRepository,
_userApiRepository = userApiRepository,
_storeService = storeService;
UserDto getMyUser() {
return _storeService.get(StoreKey.currentUser);
}
UserDto? tryGetMyUser() {
return _storeService.tryGet(StoreKey.currentUser);
}
Stream<UserDto?> watchMyUser() {
return _storeService.watch(StoreKey.currentUser);
}
Future<UserDto?> refreshMyUser() async {
final user = await _userApiRepository.getMyUser();
if (user == null) return null;
await _storeService.put(StoreKey.currentUser, user);
await _userRepository.update(user);
return user;
}
Future<String?> createProfileImage(String name, Uint8List image) async {
try {
return await _userApiRepository.createProfileImage(
name: name,
data: image,
);
} catch (e) {
_log.warning("Failed to upload profile image", e);
return null;
}
}
Future<List<UserDto>> getAll() async {
return await _userRepository.getAll();
}
Future<void> deleteAll() {
return _userRepository.deleteAll();
}
}

View File

@@ -1,7 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/utils/datetime_comparison.dart';
import 'package:isar/isar.dart';
// ignore: implementation_imports

View File

@@ -0,0 +1,181 @@
import 'dart:ui';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:isar/isar.dart';
import 'package:openapi/api.dart';
part 'user.entity.g.dart';
@Collection(inheritance: false)
class User {
User({
required this.id,
required this.updatedAt,
required this.email,
required this.name,
required this.isAdmin,
this.isPartnerSharedBy = false,
this.isPartnerSharedWith = false,
this.profileImagePath = '',
this.avatarColor = AvatarColorEnum.primary,
this.memoryEnabled = true,
this.inTimeline = false,
this.quotaUsageInBytes = 0,
this.quotaSizeInBytes = 0,
});
Id get isarId => fastHash(id);
User.fromUserDto(
UserAdminResponseDto dto,
UserPreferencesResponseDto? preferences,
) : id = dto.id,
updatedAt = dto.updatedAt,
email = dto.email,
name = dto.name,
isPartnerSharedBy = false,
isPartnerSharedWith = false,
profileImagePath = dto.profileImagePath,
isAdmin = dto.isAdmin,
memoryEnabled = preferences?.memories.enabled ?? false,
avatarColor = dto.avatarColor.toAvatarColor(),
inTimeline = false,
quotaUsageInBytes = dto.quotaUsageInBytes ?? 0,
quotaSizeInBytes = dto.quotaSizeInBytes ?? 0;
User.fromPartnerDto(PartnerResponseDto dto)
: id = dto.id,
updatedAt = DateTime.now(),
email = dto.email,
name = dto.name,
isPartnerSharedBy = false,
isPartnerSharedWith = false,
profileImagePath = dto.profileImagePath,
isAdmin = false,
memoryEnabled = false,
avatarColor = dto.avatarColor.toAvatarColor(),
inTimeline = dto.inTimeline ?? false,
quotaUsageInBytes = 0,
quotaSizeInBytes = 0;
/// Base user dto used where the complete user object is not required
User.fromSimpleUserDto(UserResponseDto dto)
: id = dto.id,
email = dto.email,
name = dto.name,
profileImagePath = dto.profileImagePath,
avatarColor = dto.avatarColor.toAvatarColor(),
// Fill the remaining fields with placeholders
isAdmin = false,
inTimeline = false,
memoryEnabled = false,
isPartnerSharedBy = false,
isPartnerSharedWith = false,
updatedAt = DateTime.now(),
quotaUsageInBytes = 0,
quotaSizeInBytes = 0;
@Index(unique: true, replace: false, type: IndexType.hash)
String id;
DateTime updatedAt;
String email;
String name;
bool isPartnerSharedBy;
bool isPartnerSharedWith;
bool isAdmin;
String profileImagePath;
@Enumerated(EnumType.ordinal)
AvatarColorEnum avatarColor;
bool memoryEnabled;
bool inTimeline;
int quotaUsageInBytes;
int quotaSizeInBytes;
bool get hasQuota => quotaSizeInBytes > 0;
@Backlink(to: 'owner')
final IsarLinks<Album> albums = IsarLinks<Album>();
@Backlink(to: 'sharedUsers')
final IsarLinks<Album> sharedAlbums = IsarLinks<Album>();
@override
bool operator ==(other) {
if (other is! User) return false;
return id == other.id &&
updatedAt.isAtSameMomentAs(other.updatedAt) &&
avatarColor == other.avatarColor &&
email == other.email &&
name == other.name &&
isPartnerSharedBy == other.isPartnerSharedBy &&
isPartnerSharedWith == other.isPartnerSharedWith &&
profileImagePath == other.profileImagePath &&
isAdmin == other.isAdmin &&
memoryEnabled == other.memoryEnabled &&
inTimeline == other.inTimeline &&
quotaUsageInBytes == other.quotaUsageInBytes &&
quotaSizeInBytes == other.quotaSizeInBytes;
}
@override
@ignore
int get hashCode =>
id.hashCode ^
updatedAt.hashCode ^
email.hashCode ^
name.hashCode ^
isPartnerSharedBy.hashCode ^
isPartnerSharedWith.hashCode ^
profileImagePath.hashCode ^
avatarColor.hashCode ^
isAdmin.hashCode ^
memoryEnabled.hashCode ^
inTimeline.hashCode ^
quotaUsageInBytes.hashCode ^
quotaSizeInBytes.hashCode;
}
enum AvatarColorEnum {
// do not change this order or reuse indices for other purposes, adding is OK
primary,
pink,
red,
yellow,
blue,
green,
purple,
orange,
gray,
amber,
}
extension AvatarColorEnumHelper on UserAvatarColor {
AvatarColorEnum toAvatarColor() => switch (this) {
UserAvatarColor.primary => AvatarColorEnum.primary,
UserAvatarColor.pink => AvatarColorEnum.pink,
UserAvatarColor.red => AvatarColorEnum.red,
UserAvatarColor.yellow => AvatarColorEnum.yellow,
UserAvatarColor.blue => AvatarColorEnum.blue,
UserAvatarColor.green => AvatarColorEnum.green,
UserAvatarColor.purple => AvatarColorEnum.purple,
UserAvatarColor.orange => AvatarColorEnum.orange,
UserAvatarColor.gray => AvatarColorEnum.gray,
UserAvatarColor.amber => AvatarColorEnum.amber,
_ => AvatarColorEnum.primary,
};
}
extension AvatarColorToColorHelper on AvatarColorEnum {
Color toColor([bool isDarkTheme = false]) => switch (this) {
AvatarColorEnum.primary =>
isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF),
AvatarColorEnum.pink => const Color.fromARGB(255, 244, 114, 182),
AvatarColorEnum.red => const Color.fromARGB(255, 239, 68, 68),
AvatarColorEnum.yellow => const Color.fromARGB(255, 234, 179, 8),
AvatarColorEnum.blue => const Color.fromARGB(255, 59, 130, 246),
AvatarColorEnum.green => const Color.fromARGB(255, 22, 163, 74),
AvatarColorEnum.purple => const Color.fromARGB(255, 147, 51, 234),
AvatarColorEnum.orange => const Color.fromARGB(255, 234, 88, 12),
AvatarColorEnum.gray => const Color.fromARGB(255, 75, 85, 99),
AvatarColorEnum.amber => const Color.fromARGB(255, 217, 119, 6),
};
}

View File

@@ -28,58 +28,63 @@ const UserSchema = CollectionSchema(
name: r'email',
type: IsarType.string,
),
r'id': PropertySchema(
r'hasQuota': PropertySchema(
id: 2,
name: r'hasQuota',
type: IsarType.bool,
),
r'id': PropertySchema(
id: 3,
name: r'id',
type: IsarType.string,
),
r'inTimeline': PropertySchema(
id: 3,
id: 4,
name: r'inTimeline',
type: IsarType.bool,
),
r'isAdmin': PropertySchema(
id: 4,
id: 5,
name: r'isAdmin',
type: IsarType.bool,
),
r'isPartnerSharedBy': PropertySchema(
id: 5,
id: 6,
name: r'isPartnerSharedBy',
type: IsarType.bool,
),
r'isPartnerSharedWith': PropertySchema(
id: 6,
id: 7,
name: r'isPartnerSharedWith',
type: IsarType.bool,
),
r'memoryEnabled': PropertySchema(
id: 7,
id: 8,
name: r'memoryEnabled',
type: IsarType.bool,
),
r'name': PropertySchema(
id: 8,
id: 9,
name: r'name',
type: IsarType.string,
),
r'profileImagePath': PropertySchema(
id: 9,
id: 10,
name: r'profileImagePath',
type: IsarType.string,
),
r'quotaSizeInBytes': PropertySchema(
id: 10,
id: 11,
name: r'quotaSizeInBytes',
type: IsarType.long,
),
r'quotaUsageInBytes': PropertySchema(
id: 11,
id: 12,
name: r'quotaUsageInBytes',
type: IsarType.long,
),
r'updatedAt': PropertySchema(
id: 12,
id: 13,
name: r'updatedAt',
type: IsarType.dateTime,
)
@@ -104,7 +109,22 @@ const UserSchema = CollectionSchema(
],
)
},
links: {},
links: {
r'albums': LinkSchema(
id: -8764917375410137318,
name: r'albums',
target: r'Album',
single: false,
linkName: r'owner',
),
r'sharedAlbums': LinkSchema(
id: -7037628715076287024,
name: r'sharedAlbums',
target: r'Album',
single: false,
linkName: r'sharedUsers',
)
},
embeddedSchemas: {},
getId: _userGetId,
getLinks: _userGetLinks,
@@ -133,17 +153,18 @@ void _userSerialize(
) {
writer.writeByte(offsets[0], object.avatarColor.index);
writer.writeString(offsets[1], object.email);
writer.writeString(offsets[2], object.id);
writer.writeBool(offsets[3], object.inTimeline);
writer.writeBool(offsets[4], object.isAdmin);
writer.writeBool(offsets[5], object.isPartnerSharedBy);
writer.writeBool(offsets[6], object.isPartnerSharedWith);
writer.writeBool(offsets[7], object.memoryEnabled);
writer.writeString(offsets[8], object.name);
writer.writeString(offsets[9], object.profileImagePath);
writer.writeLong(offsets[10], object.quotaSizeInBytes);
writer.writeLong(offsets[11], object.quotaUsageInBytes);
writer.writeDateTime(offsets[12], object.updatedAt);
writer.writeBool(offsets[2], object.hasQuota);
writer.writeString(offsets[3], object.id);
writer.writeBool(offsets[4], object.inTimeline);
writer.writeBool(offsets[5], object.isAdmin);
writer.writeBool(offsets[6], object.isPartnerSharedBy);
writer.writeBool(offsets[7], object.isPartnerSharedWith);
writer.writeBool(offsets[8], object.memoryEnabled);
writer.writeString(offsets[9], object.name);
writer.writeString(offsets[10], object.profileImagePath);
writer.writeLong(offsets[11], object.quotaSizeInBytes);
writer.writeLong(offsets[12], object.quotaUsageInBytes);
writer.writeDateTime(offsets[13], object.updatedAt);
}
User _userDeserialize(
@@ -155,19 +176,19 @@ User _userDeserialize(
final object = User(
avatarColor:
_UseravatarColorValueEnumMap[reader.readByteOrNull(offsets[0])] ??
AvatarColor.primary,
AvatarColorEnum.primary,
email: reader.readString(offsets[1]),
id: reader.readString(offsets[2]),
inTimeline: reader.readBoolOrNull(offsets[3]) ?? false,
isAdmin: reader.readBool(offsets[4]),
isPartnerSharedBy: reader.readBoolOrNull(offsets[5]) ?? false,
isPartnerSharedWith: reader.readBoolOrNull(offsets[6]) ?? false,
memoryEnabled: reader.readBoolOrNull(offsets[7]) ?? true,
name: reader.readString(offsets[8]),
profileImagePath: reader.readStringOrNull(offsets[9]) ?? '',
quotaSizeInBytes: reader.readLongOrNull(offsets[10]) ?? 0,
quotaUsageInBytes: reader.readLongOrNull(offsets[11]) ?? 0,
updatedAt: reader.readDateTime(offsets[12]),
id: reader.readString(offsets[3]),
inTimeline: reader.readBoolOrNull(offsets[4]) ?? false,
isAdmin: reader.readBool(offsets[5]),
isPartnerSharedBy: reader.readBoolOrNull(offsets[6]) ?? false,
isPartnerSharedWith: reader.readBoolOrNull(offsets[7]) ?? false,
memoryEnabled: reader.readBoolOrNull(offsets[8]) ?? true,
name: reader.readString(offsets[9]),
profileImagePath: reader.readStringOrNull(offsets[10]) ?? '',
quotaSizeInBytes: reader.readLongOrNull(offsets[11]) ?? 0,
quotaUsageInBytes: reader.readLongOrNull(offsets[12]) ?? 0,
updatedAt: reader.readDateTime(offsets[13]),
);
return object;
}
@@ -181,30 +202,32 @@ P _userDeserializeProp<P>(
switch (propertyId) {
case 0:
return (_UseravatarColorValueEnumMap[reader.readByteOrNull(offset)] ??
AvatarColor.primary) as P;
AvatarColorEnum.primary) as P;
case 1:
return (reader.readString(offset)) as P;
case 2:
return (reader.readString(offset)) as P;
case 3:
return (reader.readBoolOrNull(offset) ?? false) as P;
case 4:
return (reader.readBool(offset)) as P;
case 5:
case 3:
return (reader.readString(offset)) as P;
case 4:
return (reader.readBoolOrNull(offset) ?? false) as P;
case 5:
return (reader.readBool(offset)) as P;
case 6:
return (reader.readBoolOrNull(offset) ?? false) as P;
case 7:
return (reader.readBoolOrNull(offset) ?? true) as P;
return (reader.readBoolOrNull(offset) ?? false) as P;
case 8:
return (reader.readString(offset)) as P;
return (reader.readBoolOrNull(offset) ?? true) as P;
case 9:
return (reader.readStringOrNull(offset) ?? '') as P;
return (reader.readString(offset)) as P;
case 10:
return (reader.readLongOrNull(offset) ?? 0) as P;
return (reader.readStringOrNull(offset) ?? '') as P;
case 11:
return (reader.readLongOrNull(offset) ?? 0) as P;
case 12:
return (reader.readLongOrNull(offset) ?? 0) as P;
case 13:
return (reader.readDateTime(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
@@ -224,16 +247,16 @@ const _UseravatarColorEnumValueMap = {
'amber': 9,
};
const _UseravatarColorValueEnumMap = {
0: AvatarColor.primary,
1: AvatarColor.pink,
2: AvatarColor.red,
3: AvatarColor.yellow,
4: AvatarColor.blue,
5: AvatarColor.green,
6: AvatarColor.purple,
7: AvatarColor.orange,
8: AvatarColor.gray,
9: AvatarColor.amber,
0: AvatarColorEnum.primary,
1: AvatarColorEnum.pink,
2: AvatarColorEnum.red,
3: AvatarColorEnum.yellow,
4: AvatarColorEnum.blue,
5: AvatarColorEnum.green,
6: AvatarColorEnum.purple,
7: AvatarColorEnum.orange,
8: AvatarColorEnum.gray,
9: AvatarColorEnum.amber,
};
Id _userGetId(User object) {
@@ -241,10 +264,14 @@ Id _userGetId(User object) {
}
List<IsarLinkBase<dynamic>> _userGetLinks(User object) {
return [];
return [object.albums, object.sharedAlbums];
}
void _userAttach(IsarCollection<dynamic> col, Id id, User object) {}
void _userAttach(IsarCollection<dynamic> col, Id id, User object) {
object.albums.attach(col, col.isar.collection<Album>(), r'albums', id);
object.sharedAlbums
.attach(col, col.isar.collection<Album>(), r'sharedAlbums', id);
}
extension UserByIndex on IsarCollection<User> {
Future<User?> getById(String id) {
@@ -420,7 +447,7 @@ extension UserQueryWhere on QueryBuilder<User, User, QWhereClause> {
extension UserQueryFilter on QueryBuilder<User, User, QFilterCondition> {
QueryBuilder<User, User, QAfterFilterCondition> avatarColorEqualTo(
AvatarColor value) {
AvatarColorEnum value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'avatarColor',
@@ -430,7 +457,7 @@ extension UserQueryFilter on QueryBuilder<User, User, QFilterCondition> {
}
QueryBuilder<User, User, QAfterFilterCondition> avatarColorGreaterThan(
AvatarColor value, {
AvatarColorEnum value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
@@ -443,7 +470,7 @@ extension UserQueryFilter on QueryBuilder<User, User, QFilterCondition> {
}
QueryBuilder<User, User, QAfterFilterCondition> avatarColorLessThan(
AvatarColor value, {
AvatarColorEnum value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
@@ -456,8 +483,8 @@ extension UserQueryFilter on QueryBuilder<User, User, QFilterCondition> {
}
QueryBuilder<User, User, QAfterFilterCondition> avatarColorBetween(
AvatarColor lower,
AvatarColor upper, {
AvatarColorEnum lower,
AvatarColorEnum upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
@@ -600,6 +627,15 @@ extension UserQueryFilter on QueryBuilder<User, User, QFilterCondition> {
});
}
QueryBuilder<User, User, QAfterFilterCondition> hasQuotaEqualTo(bool value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'hasQuota',
value: value,
));
});
}
QueryBuilder<User, User, QAfterFilterCondition> idEqualTo(
String value, {
bool caseSensitive = true,
@@ -1249,7 +1285,118 @@ extension UserQueryFilter on QueryBuilder<User, User, QFilterCondition> {
extension UserQueryObject on QueryBuilder<User, User, QFilterCondition> {}
extension UserQueryLinks on QueryBuilder<User, User, QFilterCondition> {}
extension UserQueryLinks on QueryBuilder<User, User, QFilterCondition> {
QueryBuilder<User, User, QAfterFilterCondition> albums(FilterQuery<Album> q) {
return QueryBuilder.apply(this, (query) {
return query.link(q, r'albums');
});
}
QueryBuilder<User, User, QAfterFilterCondition> albumsLengthEqualTo(
int length) {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'albums', length, true, length, true);
});
}
QueryBuilder<User, User, QAfterFilterCondition> albumsIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'albums', 0, true, 0, true);
});
}
QueryBuilder<User, User, QAfterFilterCondition> albumsIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'albums', 0, false, 999999, true);
});
}
QueryBuilder<User, User, QAfterFilterCondition> albumsLengthLessThan(
int length, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'albums', 0, true, length, include);
});
}
QueryBuilder<User, User, QAfterFilterCondition> albumsLengthGreaterThan(
int length, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'albums', length, include, 999999, true);
});
}
QueryBuilder<User, User, QAfterFilterCondition> albumsLengthBetween(
int lower,
int upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.linkLength(
r'albums', lower, includeLower, upper, includeUpper);
});
}
QueryBuilder<User, User, QAfterFilterCondition> sharedAlbums(
FilterQuery<Album> q) {
return QueryBuilder.apply(this, (query) {
return query.link(q, r'sharedAlbums');
});
}
QueryBuilder<User, User, QAfterFilterCondition> sharedAlbumsLengthEqualTo(
int length) {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'sharedAlbums', length, true, length, true);
});
}
QueryBuilder<User, User, QAfterFilterCondition> sharedAlbumsIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'sharedAlbums', 0, true, 0, true);
});
}
QueryBuilder<User, User, QAfterFilterCondition> sharedAlbumsIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'sharedAlbums', 0, false, 999999, true);
});
}
QueryBuilder<User, User, QAfterFilterCondition> sharedAlbumsLengthLessThan(
int length, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'sharedAlbums', 0, true, length, include);
});
}
QueryBuilder<User, User, QAfterFilterCondition> sharedAlbumsLengthGreaterThan(
int length, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'sharedAlbums', length, include, 999999, true);
});
}
QueryBuilder<User, User, QAfterFilterCondition> sharedAlbumsLengthBetween(
int lower,
int upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.linkLength(
r'sharedAlbums', lower, includeLower, upper, includeUpper);
});
}
}
extension UserQuerySortBy on QueryBuilder<User, User, QSortBy> {
QueryBuilder<User, User, QAfterSortBy> sortByAvatarColor() {
@@ -1276,6 +1423,18 @@ extension UserQuerySortBy on QueryBuilder<User, User, QSortBy> {
});
}
QueryBuilder<User, User, QAfterSortBy> sortByHasQuota() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'hasQuota', Sort.asc);
});
}
QueryBuilder<User, User, QAfterSortBy> sortByHasQuotaDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'hasQuota', Sort.desc);
});
}
QueryBuilder<User, User, QAfterSortBy> sortById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
@@ -1434,6 +1593,18 @@ extension UserQuerySortThenBy on QueryBuilder<User, User, QSortThenBy> {
});
}
QueryBuilder<User, User, QAfterSortBy> thenByHasQuota() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'hasQuota', Sort.asc);
});
}
QueryBuilder<User, User, QAfterSortBy> thenByHasQuotaDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'hasQuota', Sort.desc);
});
}
QueryBuilder<User, User, QAfterSortBy> thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
@@ -1593,6 +1764,12 @@ extension UserQueryWhereDistinct on QueryBuilder<User, User, QDistinct> {
});
}
QueryBuilder<User, User, QDistinct> distinctByHasQuota() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'hasQuota');
});
}
QueryBuilder<User, User, QDistinct> distinctById(
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
@@ -1671,7 +1848,7 @@ extension UserQueryProperty on QueryBuilder<User, User, QQueryProperty> {
});
}
QueryBuilder<User, AvatarColor, QQueryOperations> avatarColorProperty() {
QueryBuilder<User, AvatarColorEnum, QQueryOperations> avatarColorProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'avatarColor');
});
@@ -1683,6 +1860,12 @@ extension UserQueryProperty on QueryBuilder<User, User, QQueryProperty> {
});
}
QueryBuilder<User, bool, QQueryOperations> hasQuotaProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'hasQuota');
});
}
QueryBuilder<User, String, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');

View File

@@ -1,8 +1,8 @@
import 'dart:typed_data';
import 'package:collection/collection.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
extension ListExtension<E> on List<E> {
List<E> uniqueConsecutive({
@@ -58,11 +58,11 @@ extension AssetListExtension on Iterable<Asset> {
/// Returns the assets that are owned by the user passed to the [owner] param
/// If [owner] is null, an empty list is returned
Iterable<Asset> ownedOnly(
UserDto? owner, {
User? owner, {
void Function()? errorCallback,
}) {
if (owner == null) return [];
final userId = owner.id;
final userId = owner.isarId;
final bool onlyOwned = every((e) => e.ownerId == userId);
if (!onlyOwned) {
if (errorCallback != null) errorCallback();

View File

@@ -1,73 +0,0 @@
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:isar/isar.dart';
part 'user.entity.g.dart';
@Collection(inheritance: false)
class User {
Id get isarId => fastHash(id);
@Index(unique: true, replace: false, type: IndexType.hash)
final String id;
final DateTime updatedAt;
final String email;
final String name;
final bool isPartnerSharedBy;
final bool isPartnerSharedWith;
final bool isAdmin;
final String profileImagePath;
@Enumerated(EnumType.ordinal)
final AvatarColor avatarColor;
final bool memoryEnabled;
final bool inTimeline;
final int quotaUsageInBytes;
final int quotaSizeInBytes;
const User({
required this.id,
required this.updatedAt,
required this.email,
required this.name,
required this.isAdmin,
this.isPartnerSharedBy = false,
this.isPartnerSharedWith = false,
this.profileImagePath = '',
this.avatarColor = AvatarColor.primary,
this.memoryEnabled = true,
this.inTimeline = false,
this.quotaUsageInBytes = 0,
this.quotaSizeInBytes = 0,
});
static User fromDto(UserDto dto) => User(
id: dto.uid,
updatedAt: dto.updatedAt,
email: dto.email,
name: dto.name,
isAdmin: dto.isAdmin,
isPartnerSharedBy: dto.isPartnerSharedBy,
isPartnerSharedWith: dto.isPartnerSharedWith,
profileImagePath: dto.profileImagePath ?? "",
avatarColor: dto.avatarColor,
memoryEnabled: dto.memoryEnabled,
inTimeline: dto.inTimeline,
quotaUsageInBytes: dto.quotaUsageInBytes,
quotaSizeInBytes: dto.quotaSizeInBytes,
);
UserDto toDto() => UserDto(
uid: id,
email: email,
name: name,
isAdmin: isAdmin,
updatedAt: updatedAt,
profileImagePath: profileImagePath.isEmpty ? null : profileImagePath,
avatarColor: avatarColor,
memoryEnabled: memoryEnabled,
inTimeline: inTimeline,
isPartnerSharedBy: isPartnerSharedBy,
isPartnerSharedWith: isPartnerSharedWith,
quotaUsageInBytes: quotaUsageInBytes,
quotaSizeInBytes: quotaSizeInBytes,
);
}

View File

@@ -1,11 +0,0 @@
import 'package:immich_mobile/constants/errors.dart';
class ApiRepository {
const ApiRepository();
Future<T> checkNull<T>(Future<T?> future) async {
final response = await future;
if (response == null) throw NoResponseDtoError();
return response;
}
}

View File

@@ -1,9 +1,9 @@
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/repositories/user.repository.dart';
import 'package:isar/isar.dart';
class IsarStoreRepository extends IsarDatabaseRepository
@@ -78,7 +78,7 @@ class IsarStoreRepository extends IsarDatabaseRepository
const (DateTime) => entity.intValue == null
? null
: DateTime.fromMillisecondsSinceEpoch(entity.intValue!),
const (UserDto) => await IsarUserRepository(_db).get(entity.intValue!),
const (User) => await UserRepository(_db).getByDbId(entity.intValue!),
_ => null,
} as T?;
@@ -88,8 +88,8 @@ class IsarStoreRepository extends IsarDatabaseRepository
const (String) => (null, value as String),
const (bool) => ((value as bool) ? 1 : 0, null),
const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null),
const (UserDto) => (
(await IsarUserRepository(_db).update(value as UserDto)).id,
const (User) => (
(await UserRepository(_db).update(value as User)).isarId,
null,
),
_ => throw UnsupportedError(

View File

@@ -1,80 +0,0 @@
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
as entity;
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:isar/isar.dart';
class IsarUserRepository extends IsarDatabaseRepository
implements IUserRepository {
final Isar _db;
const IsarUserRepository(super.db) : _db = db;
@override
Future<void> delete(List<int> ids) async {
await transaction(() async {
await _db.users.deleteAll(ids);
});
}
@override
Future<void> deleteAll() async {
await transaction(() async {
await _db.users.clear();
});
}
@override
Future<UserDto?> get(int id) async {
return (await _db.users.get(id))?.toDto();
}
@override
Future<List<UserDto>> getAll({SortUserBy? sortBy}) async {
return (await _db.users
.where()
.optional(
sortBy != null,
(query) => switch (sortBy!) {
SortUserBy.id => query.sortById(),
},
)
.findAll())
.map((u) => u.toDto())
.toList();
}
@override
Future<UserDto?> getByUserId(String id) async {
return (await _db.users.getById(id))?.toDto();
}
@override
Future<List<UserDto?>> getByUserIds(List<String> ids) async {
return (await _db.users.getAllById(ids)).map((u) => u?.toDto()).toList();
}
@override
Future<bool> insert(UserDto user) async {
await transaction(() async {
await _db.users.put(entity.User.fromDto(user));
});
return true;
}
@override
Future<UserDto> update(UserDto user) async {
await transaction(() async {
await _db.users.put(entity.User.fromDto(user));
});
return user;
}
@override
Future<bool> updateAll(List<UserDto> users) async {
await transaction(() async {
await _db.users.putAll(users.map(entity.User.fromDto).toList());
});
return true;
}
}

View File

@@ -1,41 +0,0 @@
import 'dart:typed_data';
import 'package:http/http.dart';
import 'package:immich_mobile/domain/interfaces/user_api.repository.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/repositories/api.repository.dart';
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
import 'package:openapi/api.dart';
class UserApiRepository extends ApiRepository implements IUserApiRepository {
final UsersApi _api;
const UserApiRepository(this._api);
@override
Future<UserDto?> getMyUser() async {
final (adminDto, preferenceDto) =
await (_api.getMyUser(), _api.getMyPreferences()).wait;
if (adminDto == null) return null;
return UserConverter.fromAdminDto(adminDto, preferenceDto);
}
@override
Future<String> createProfileImage({
required String name,
required Uint8List data,
}) async {
final res = await checkNull(
_api.createProfileImage(
MultipartFile.fromBytes('file', data, filename: name),
),
);
return res.profileImagePath;
}
@override
Future<List<UserDto>> getAll() async {
final dto = await checkNull(_api.searchUsers());
return dto.map(UserConverter.fromSimpleUserDto).toList();
}
}

View File

@@ -1,66 +0,0 @@
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:openapi/api.dart';
abstract final class UserConverter {
/// Base user dto used where the complete user object is not required
static UserDto fromSimpleUserDto(UserResponseDto dto) => UserDto(
uid: dto.id,
email: dto.email,
name: dto.name,
isAdmin: false,
updatedAt: DateTime.now(),
profileImagePath: dto.profileImagePath,
avatarColor: dto.avatarColor.toAvatarColor(),
);
static UserDto fromAdminDto(
UserAdminResponseDto adminDto, [
UserPreferencesResponseDto? preferenceDto,
]) =>
UserDto(
uid: adminDto.id,
email: adminDto.email,
name: adminDto.name,
isAdmin: adminDto.isAdmin,
updatedAt: adminDto.updatedAt,
profileImagePath: adminDto.profileImagePath,
avatarColor: adminDto.avatarColor.toAvatarColor(),
memoryEnabled: preferenceDto?.memories.enabled ?? true,
inTimeline: false,
isPartnerSharedBy: false,
isPartnerSharedWith: false,
quotaUsageInBytes: adminDto.quotaUsageInBytes ?? 0,
quotaSizeInBytes: adminDto.quotaSizeInBytes ?? 0,
);
static UserDto fromPartnerDto(PartnerResponseDto dto) => UserDto(
uid: dto.id,
email: dto.email,
name: dto.name,
isAdmin: false,
updatedAt: DateTime.now(),
profileImagePath: dto.profileImagePath,
avatarColor: dto.avatarColor.toAvatarColor(),
memoryEnabled: false,
inTimeline: dto.inTimeline ?? false,
isPartnerSharedBy: false,
isPartnerSharedWith: false,
quotaUsageInBytes: 0,
quotaSizeInBytes: 0,
);
}
extension on UserAvatarColor {
AvatarColor toAvatarColor() => switch (this) {
UserAvatarColor.red => AvatarColor.red,
UserAvatarColor.green => AvatarColor.green,
UserAvatarColor.blue => AvatarColor.blue,
UserAvatarColor.purple => AvatarColor.purple,
UserAvatarColor.orange => AvatarColor.orange,
UserAvatarColor.pink => AvatarColor.pink,
UserAvatarColor.amber => AvatarColor.amber,
UserAvatarColor.yellow => AvatarColor.yellow,
UserAvatarColor.gray => AvatarColor.gray,
UserAvatarColor.primary || _ => AvatarColor.primary,
};
}

View File

@@ -1,6 +1,6 @@
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/database.interface.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart';
@@ -31,9 +31,9 @@ abstract interface class IAlbumRepository implements IDatabaseRepository {
Future<int> count({bool? local});
Future<void> addUsers(Album album, List<UserDto> users);
Future<void> addUsers(Album album, List<User> users);
Future<void> removeUsers(Album album, List<UserDto> users);
Future<void> removeUsers(Album album, List<User> users);
Future<void> addAssets(Album album, List<Asset> assets);

View File

@@ -1,8 +1,8 @@
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/user.entity.dart';
abstract class IPartnerRepository {
Future<List<UserDto>> getSharedWith();
Future<List<UserDto>> getSharedBy();
Stream<List<UserDto>> watchSharedWith();
Stream<List<UserDto>> watchSharedBy();
Future<List<User>> getSharedWith();
Future<List<User>> getSharedBy();
Stream<List<User>> watchSharedWith();
Stream<List<User>> watchSharedBy();
}

View File

@@ -1,9 +1,9 @@
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/user.entity.dart';
abstract interface class IPartnerApiRepository {
Future<List<UserDto>> getAll(Direction direction);
Future<UserDto> create(String id);
Future<UserDto> update(String id, {required bool inTimeline});
Future<List<User>> getAll(Direction direction);
Future<User> create(String id);
Future<User> update(String id, {required bool inTimeline});
Future<void> delete(String id);
}

View File

@@ -0,0 +1,27 @@
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/database.interface.dart';
abstract interface class IUserRepository implements IDatabaseRepository {
Future<User?> get(String id);
Future<User?> getByDbId(int id);
Future<List<User>> getByIds(List<String> ids);
Future<List<User>> getAll({bool self = true, UserSort? sortBy});
/// Returns all users whose assets can be accessed (self+partners)
Future<List<User>> getAllAccessible();
Future<List<User>> upsertAll(List<User> users);
Future<User> update(User user);
Future<void> deleteById(List<int> ids);
Future<User> me();
Future<void> clearTable();
}
enum UserSort { id }

View File

@@ -0,0 +1,11 @@
import 'dart:typed_data';
import 'package:immich_mobile/entities/user.entity.dart';
abstract interface class IUserApiRepository {
Future<List<User>> getAll();
Future<({String profileImagePath})> createProfileImage({
required String name,
required Uint8List data,
});
}

View File

@@ -1,4 +1,4 @@
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/user.entity.dart';
enum ActivityType { comment, like }
@@ -8,7 +8,7 @@ class Activity {
final String? comment;
final DateTime createdAt;
final ActivityType type;
final UserDto user;
final User user;
const Activity({
required this.id,
@@ -25,7 +25,7 @@ class Activity {
String? comment,
DateTime? createdAt,
ActivityType? type,
UserDto? user,
User? user,
}) {
return Activity(
id: id ?? this.id,

View File

@@ -3,11 +3,11 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
@RoutePage()
@@ -21,15 +21,15 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final AsyncValue<List<UserDto>> suggestedShareUsers =
final AsyncValue<List<User>> suggestedShareUsers =
ref.watch(otherUsersProvider);
final sharedUsersList = useState<Set<UserDto>>({});
final sharedUsersList = useState<Set<User>>({});
addNewUsersHandler() {
context.maybePop(sharedUsersList.value.map((e) => e.uid).toList());
context.maybePop(sharedUsersList.value.map((e) => e.id).toList());
}
buildTileIcon(UserDto user) {
buildTileIcon(User user) {
if (sharedUsersList.value.contains(user)) {
return CircleAvatar(
backgroundColor: context.primaryColor,
@@ -45,7 +45,7 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget {
}
}
buildUserList(List<UserDto> users) {
buildUserList(List<User> users) {
List<Widget> usersChip = [];
for (var user in sharedUsersList.value) {
@@ -151,7 +151,7 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget {
onData: (users) {
for (var sharedUsers in album.sharedUsers) {
users.removeWhere(
(u) => u.uid == sharedUsers.id || u.uid == album.ownerId,
(u) => u.id == sharedUsers.id || u.id == album.ownerId,
);
}

View File

@@ -4,16 +4,14 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
as entity;
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/providers/auth.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
@@ -28,8 +26,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
return const SizedBox();
}
final sharedUsers =
useState(album.sharedUsers.map((u) => u.toDto()).toList());
final sharedUsers = useState(album.sharedUsers.toList());
final owner = album.owner.value;
final userId = ref.watch(authProvider).userId;
final activityEnabled = useState(album.activityEnabled);
@@ -67,13 +64,13 @@ class AlbumOptionsPage extends HookConsumerWidget {
isProcessing.value = false;
}
void removeUserFromAlbum(UserDto user) async {
void removeUserFromAlbum(User user) async {
isProcessing.value = true;
try {
await ref.read(albumProvider.notifier).removeUser(album, user);
album.sharedUsers.remove(entity.User.fromDto(user));
sharedUsers.value = album.sharedUsers.map((u) => u.toDto()).toList();
album.sharedUsers.remove(user);
sharedUsers.value = album.sharedUsers.toList();
} catch (error) {
showErrorMessage();
}
@@ -82,10 +79,10 @@ class AlbumOptionsPage extends HookConsumerWidget {
isProcessing.value = false;
}
void handleUserClick(UserDto user) {
void handleUserClick(User user) {
var actions = [];
if (user.uid == userId) {
if (user.id == userId) {
actions = [
ListTile(
leading: const Icon(Icons.exit_to_app_rounded),
@@ -126,9 +123,8 @@ class AlbumOptionsPage extends HookConsumerWidget {
buildOwnerInfo() {
return ListTile(
leading: owner != null
? UserCircleAvatar(user: owner.toDto())
: const SizedBox(),
leading:
owner != null ? UserCircleAvatar(user: owner) : const SizedBox(),
title: Text(
album.owner.value?.name ?? "",
style: const TextStyle(
@@ -170,10 +166,10 @@ class AlbumOptionsPage extends HookConsumerWidget {
color: context.colorScheme.onSurfaceSecondary,
),
),
trailing: userId == user.uid || isOwner
trailing: userId == user.id || isOwner
? const Icon(Icons.more_horiz_rounded)
: const SizedBox(),
onTap: userId == user.uid || isOwner
onTap: userId == user.id || isOwner
? () => handleUserClick(user)
: null,
);

View File

@@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
@@ -12,7 +12,7 @@ class AlbumSharedUserIcons extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final sharedUsers = useRef<List<UserDto>>(const []);
final sharedUsers = useRef<List<User>>(const []);
sharedUsers.value = ref.watch(
currentAlbumProvider.select((album) {
if (album == null) {
@@ -23,7 +23,7 @@ class AlbumSharedUserIcons extends HookConsumerWidget {
return sharedUsers.value;
}
return album.sharedUsers.map((u) => u.toDto()).toList(growable: false);
return album.sharedUsers.toList(growable: false);
}),
);

View File

@@ -3,14 +3,14 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/album/album_title.provider.dart';
import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
@RoutePage()
@@ -21,7 +21,7 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final sharedUsersList = useState<Set<UserDto>>({});
final sharedUsersList = useState<Set<User>>({});
final suggestedShareUsers = ref.watch(otherUsersProvider);
createSharedAlbum() async {
@@ -48,7 +48,7 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget {
);
}
buildTileIcon(UserDto user) {
buildTileIcon(User user) {
if (sharedUsersList.value.contains(user)) {
return CircleAvatar(
backgroundColor: context.primaryColor,
@@ -64,7 +64,7 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget {
}
}
buildUserList(List<UserDto> users) {
buildUserList(List<User> users) {
List<Widget> usersChip = [];
for (var user in sharedUsersList.value) {

View File

@@ -33,7 +33,7 @@ class AlbumsPage extends HookConsumerWidget {
final searchController = useTextEditingController();
final debounceTimer = useRef<Timer?>(null);
final filterMode = useState(QuickFilterMode.all);
final userId = ref.watch(currentUserProvider)?.uid;
final userId = ref.watch(currentUserProvider)?.id;
final searchFocusNode = useFocusNode();
toggleViewMode() {

View File

@@ -7,12 +7,12 @@ import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/activities/activity.model.dart';
import 'package:immich_mobile/providers/activity.provider.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/widgets/activities/activity_text_field.dart';
import 'package:immich_mobile/widgets/activities/activity_tile.dart';
import 'package:immich_mobile/widgets/activities/dismissible_activity.dart';
import 'package:immich_mobile/providers/album/current_album.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
@RoutePage()
class ActivitiesPage extends HookConsumerWidget {
@@ -72,7 +72,7 @@ class ActivitiesPage extends HookConsumerWidget {
final activity = data[index];
final canDelete = activity.user.id == user?.id ||
album.ownerId == user?.uid;
album.ownerId == user?.id;
return Padding(
padding: const EdgeInsets.all(5),

View File

@@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
@@ -163,7 +163,7 @@ class QuickAccessButtons extends ConsumerWidget {
class PartnerList extends ConsumerWidget {
const PartnerList({super.key, required this.partners});
final List<UserDto> partners;
final List<User> partners;
@override
Widget build(BuildContext context, WidgetRef ref) {

View File

@@ -2,10 +2,10 @@ import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/partner.provider.dart';
import 'package:immich_mobile/services/partner.service.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/widgets/common/user_avatar.dart';
@@ -16,7 +16,7 @@ class PartnerPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final List<UserDto> partners = ref.watch(partnerSharedByProvider);
final List<User> partners = ref.watch(partnerSharedByProvider);
final availableUsers = ref.watch(partnerAvailableProvider);
addNewUsersHandler() async {
@@ -29,13 +29,13 @@ class PartnerPage extends HookConsumerWidget {
return;
}
final selectedUser = await showDialog<UserDto>(
final selectedUser = await showDialog<User>(
context: context,
builder: (context) {
return SimpleDialog(
title: const Text("partner_page_select_partner").tr(),
children: [
for (UserDto u in users)
for (User u in users)
SimpleDialogOption(
onPressed: () => context.pop(u),
child: Row(
@@ -67,7 +67,7 @@ class PartnerPage extends HookConsumerWidget {
}
}
onDeleteUser(UserDto u) {
onDeleteUser(User u) {
return showDialog(
context: context,
builder: (BuildContext context) {
@@ -80,7 +80,7 @@ class PartnerPage extends HookConsumerWidget {
);
}
buildUserList(List<UserDto> users) {
buildUserList(List<User> users) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [

View File

@@ -2,11 +2,11 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/multiselect.provider.dart';
import 'package:immich_mobile/providers/partner.provider.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/timeline.provider.dart';
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
@@ -15,7 +15,7 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
class PartnerDetailPage extends HookConsumerWidget {
const PartnerDetailPage({super.key, required this.partner});
final UserDto partner;
final User partner;
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -111,7 +111,7 @@ class PartnerDetailPage extends HookConsumerWidget {
),
),
),
renderListProvider: singleUserTimelineProvider(partner.id),
renderListProvider: singleUserTimelineProvider(partner.isarId),
onRefresh: () => ref.read(assetProvider.notifier).getAllAsset(),
deleteEnabled: false,
favoriteEnabled: false,

View File

@@ -7,16 +7,16 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/multiselect.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/timeline.provider.dart';
import 'package:immich_mobile/widgets/memories/memory_lane.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/providers/websocket.provider.dart';
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
import 'package:immich_mobile/widgets/common/immich_app_bar.dart';
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
import 'package:immich_mobile/widgets/memories/memory_lane.dart';
@RoutePage()
class PhotosPage extends HookConsumerWidget {
@@ -110,7 +110,7 @@ class PhotosPage extends HookConsumerWidget {
: const SizedBox(),
renderListProvider: timelineUsers.length > 1
? multiUsersTimelineProvider(timelineUsers)
: singleUserTimelineProvider(currentUser?.id),
: singleUserTimelineProvider(currentUser?.isarId),
buildLoadingIndicator: buildLoadingIndicator,
onRefresh: refreshAssets,
stackEnabled: true,

View File

@@ -2,11 +2,11 @@ import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/album.entity.dart';
final isRefreshingRemoteAlbumProvider = StateProvider<bool>((ref) => false);
@@ -88,7 +88,7 @@ class AlbumNotifier extends StateNotifier<List<Album>> {
await albumService.addUsers(album, userIds);
}
Future<bool> removeUser(Album album, UserDto user) async {
Future<bool> removeUser(Album album, User user) async {
final isRemoved = await albumService.removeUser(album, user);
if (isRemoved && album.sharedUsers.isEmpty) {

View File

@@ -1,15 +1,9 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/services/user.service.dart';
final otherUsersProvider =
FutureProvider.autoDispose<List<UserDto>>((ref) async {
final otherUsersProvider = FutureProvider.autoDispose<List<User>>((ref) {
UserService userService = ref.watch(userServiceProvider);
final currentUser = ref.watch(currentUserProvider);
final allUsers = await userService.getAll();
allUsers.removeWhere((u) => currentUser?.id == u.id);
return allUsers;
return userService.getUsers();
});

View File

@@ -1,16 +1,15 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/providers/memory.provider.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/services/asset.service.dart';
import 'package:immich_mobile/services/etag.service.dart';
import 'package:immich_mobile/services/exif.service.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:immich_mobile/services/user.service.dart';
import 'package:logging/logging.dart';
final assetProvider = StateNotifierProvider<AssetNotifier, bool>((ref) {
@@ -60,7 +59,7 @@ class AssetNotifier extends StateNotifier<bool> {
await clearAllAssets();
log.info("Manual refresh requested, cleared assets and albums from db");
}
final users = await _syncService.getUsersFromServer();
final users = await _userService.getUsersFromServer();
bool changedUsers = false;
if (users != null) {
changedUsers = await _syncService.syncUsersFromServer(users);
@@ -87,7 +86,7 @@ class AssetNotifier extends StateNotifier<bool> {
_assetService.clearTable(),
_exifService.clearTable(),
_albumService.clearTable(),
_userService.deleteAll(),
_userService.clearTable(),
_etagService.clearTable(),
]);
}

View File

@@ -2,9 +2,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_udid/flutter_udid.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/models/auth/auth_state.model.dart';
import 'package:immich_mobile/models/auth/login_response.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
@@ -106,7 +105,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
String deviceId =
Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid;
UserDto? user = Store.tryGet(StoreKey.currentUser);
User? user = Store.tryGet(StoreKey.currentUser);
UserAdminResponseDto? userResponse;
UserPreferencesResponseDto? userPreferences;
@@ -142,18 +141,18 @@ class AuthNotifier extends StateNotifier<AuthState> {
// If the user information is successfully retrieved, update the store
// Due to the flow of the code, this will always happen on first login
if (userResponse == null) {
_log.severe("Unable to get user information from the server.");
} else {
if (userResponse != null) {
await Store.put(StoreKey.deviceId, deviceId);
await Store.put(StoreKey.deviceIdHash, fastHash(deviceId));
await Store.put(
StoreKey.currentUser,
UserConverter.fromAdminDto(userResponse, userPreferences),
User.fromUserDto(userResponse, userPreferences),
);
await Store.put(StoreKey.accessToken, accessToken);
user = UserConverter.fromAdminDto(userResponse, userPreferences);
user = User.fromUserDto(userResponse, userPreferences);
} else {
_log.severe("Unable to get user information from the server.");
}
// If the user is null, the login was not successful
@@ -164,7 +163,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
state = state.copyWith(
isAuthenticated: true,
userId: user.uid,
userId: user.id,
userEmail: user.email,
name: user.name,
profileImagePath: user.profileImagePath,

View File

@@ -1,15 +1,11 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'store.provider.g.dart';
@Riverpod(keepAlive: true)
@riverpod
IStoreRepository storeRepository(Ref ref) =>
IsarStoreRepository(ref.watch(isarProvider));
@Riverpod(keepAlive: true)
StoreService storeService(Ref _) => StoreService.I;

View File

@@ -6,11 +6,11 @@ part of 'store.provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$storeRepositoryHash() => r'99d24875d30c5e86b1c6caa352a0026167114e62';
String _$storeRepositoryHash() => r'9f378b96e552151fa14a8c8ce2c30a5f38f436ed';
/// See also [storeRepository].
@ProviderFor(storeRepository)
final storeRepositoryProvider = Provider<IStoreRepository>.internal(
final storeRepositoryProvider = AutoDisposeProvider<IStoreRepository>.internal(
storeRepository,
name: r'storeRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
@@ -22,22 +22,6 @@ final storeRepositoryProvider = Provider<IStoreRepository>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef StoreRepositoryRef = ProviderRef<IStoreRepository>;
String _$storeServiceHash() => r'250e10497c42df360e9e1f9a618d0b19c1b5b0a0';
/// See also [storeService].
@ProviderFor(storeService)
final storeServiceProvider = Provider<StoreService>.internal(
storeService,
name: r'storeServiceProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$storeServiceHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef StoreServiceRef = ProviderRef<StoreService>;
typedef StoreRepositoryRef = AutoDisposeProviderRef<IStoreRepository>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -1,27 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/interfaces/user_api.repository.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/store.provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'user.provider.g.dart';
@Riverpod(keepAlive: true)
IUserRepository userRepository(Ref ref) =>
IsarUserRepository(ref.watch(isarProvider));
@Riverpod(keepAlive: true)
IUserApiRepository userApiRepository(Ref ref) =>
UserApiRepository(ref.watch(apiServiceProvider).usersApi);
@Riverpod(keepAlive: true)
UserService userService(Ref ref) => UserService(
userRepository: ref.watch(userRepositoryProvider),
userApiRepository: ref.watch(userApiRepositoryProvider),
storeService: ref.watch(storeServiceProvider),
);

View File

@@ -1,60 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user.provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$userRepositoryHash() => r'1a2ac726bcc44397dcaecf449084fefd336696d4';
/// See also [userRepository].
@ProviderFor(userRepository)
final userRepositoryProvider = Provider<IUserRepository>.internal(
userRepository,
name: r'userRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$userRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef UserRepositoryRef = ProviderRef<IUserRepository>;
String _$userApiRepositoryHash() => r'6b19f2c99fb83162a5ceb91adb8589eaae01bc92';
/// See also [userApiRepository].
@ProviderFor(userApiRepository)
final userApiRepositoryProvider = Provider<IUserApiRepository>.internal(
userApiRepository,
name: r'userApiRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$userApiRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef UserApiRepositoryRef = ProviderRef<IUserApiRepository>;
String _$userServiceHash() => r'4a0873357b7115b4d6bfa8e89b847c0b74ce0d93';
/// See also [userService].
@ProviderFor(userService)
final userServiceProvider = Provider<UserService>.internal(
userService,
name: r'userServiceProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$userServiceHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef UserServiceRef = ProviderRef<UserService>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@@ -2,16 +2,16 @@ import 'dart:async';
import 'package:collection/collection.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/providers/album/suggested_shared_users.provider.dart';
import 'package:immich_mobile/services/partner.service.dart';
import 'package:immich_mobile/entities/user.entity.dart';
class PartnerSharedWithNotifier extends StateNotifier<List<UserDto>> {
class PartnerSharedWithNotifier extends StateNotifier<List<User>> {
final PartnerService _partnerService;
late final StreamSubscription<List<UserDto>> streamSub;
late final StreamSubscription<List<User>> streamSub;
PartnerSharedWithNotifier(this._partnerService) : super([]) {
Function eq = const ListEquality<UserDto>().equals;
Function eq = const ListEquality<User>().equals;
_partnerService.getSharedWith().then((partners) {
if (!eq(state, partners)) {
state = partners;
@@ -25,7 +25,7 @@ class PartnerSharedWithNotifier extends StateNotifier<List<UserDto>> {
});
}
Future<bool> updatePartner(UserDto partner, {required bool inTimeline}) {
Future<bool> updatePartner(User partner, {required bool inTimeline}) {
return _partnerService.updatePartner(partner, inTimeline: inTimeline);
}
@@ -39,18 +39,18 @@ class PartnerSharedWithNotifier extends StateNotifier<List<UserDto>> {
}
final partnerSharedWithProvider =
StateNotifierProvider<PartnerSharedWithNotifier, List<UserDto>>((ref) {
StateNotifierProvider<PartnerSharedWithNotifier, List<User>>((ref) {
return PartnerSharedWithNotifier(
ref.watch(partnerServiceProvider),
);
});
class PartnerSharedByNotifier extends StateNotifier<List<UserDto>> {
class PartnerSharedByNotifier extends StateNotifier<List<User>> {
final PartnerService _partnerService;
late final StreamSubscription<List<UserDto>> streamSub;
late final StreamSubscription<List<User>> streamSub;
PartnerSharedByNotifier(this._partnerService) : super([]) {
Function eq = const ListEquality<UserDto>().equals;
Function eq = const ListEquality<User>().equals;
_partnerService.getSharedBy().then((partners) {
if (!eq(state, partners)) {
state = partners;
@@ -74,15 +74,15 @@ class PartnerSharedByNotifier extends StateNotifier<List<UserDto>> {
}
final partnerSharedByProvider =
StateNotifierProvider<PartnerSharedByNotifier, List<UserDto>>((ref) {
StateNotifierProvider<PartnerSharedByNotifier, List<User>>((ref) {
return PartnerSharedByNotifier(ref.watch(partnerServiceProvider));
});
final partnerAvailableProvider =
FutureProvider.autoDispose<List<UserDto>>((ref) async {
FutureProvider.autoDispose<List<User>>((ref) async {
final otherUsers = await ref.watch(otherUsersProvider.future);
final currentPartners = ref.watch(partnerSharedByProvider);
final available = Set<UserDto>.of(otherUsers);
final available = Set<User>.of(otherUsers);
available.removeAll(currentPartners);
return available.toList();
});

View File

@@ -3,8 +3,8 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/domain/services/user.service.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/services/user.service.dart';
enum UploadProfileStatus {
idle,
@@ -72,7 +72,7 @@ class UploadProfileImageState {
class UploadProfileImageNotifier
extends StateNotifier<UploadProfileImageState> {
UploadProfileImageNotifier(this._userService)
UploadProfileImageNotifier(this._userSErvice)
: super(
UploadProfileImageState(
profileImagePath: '',
@@ -80,21 +80,18 @@ class UploadProfileImageNotifier
),
);
final UserService _userService;
final UserService _userSErvice;
Future<bool> upload(XFile file) async {
state = state.copyWith(status: UploadProfileStatus.loading);
var profileImagePath = await _userService.createProfileImage(
file.name,
await file.readAsBytes(),
);
var res = await _userSErvice.uploadProfileImage(file);
if (profileImagePath != null) {
if (res != null) {
debugPrint("Successfully upload profile image");
state = state.copyWith(
status: UploadProfileStatus.success,
profileImagePath: profileImagePath,
profileImagePath: res.profileImagePath,
);
return true;
}

View File

@@ -2,14 +2,13 @@ import 'dart:async';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/timeline.service.dart';
class CurrentUserProvider extends StateNotifier<UserDto?> {
class CurrentUserProvider extends StateNotifier<User?> {
CurrentUserProvider(this._apiService) : super(null) {
state = Store.tryGet(StoreKey.currentUser);
streamSub =
@@ -17,7 +16,7 @@ class CurrentUserProvider extends StateNotifier<UserDto?> {
}
final ApiService _apiService;
late final StreamSubscription<UserDto?> streamSub;
late final StreamSubscription<User?> streamSub;
refresh() async {
try {
@@ -26,7 +25,7 @@ class CurrentUserProvider extends StateNotifier<UserDto?> {
if (user != null) {
await Store.put(
StoreKey.currentUser,
UserConverter.fromAdminDto(user, userPreferences),
User.fromUserDto(user, userPreferences),
);
}
} catch (_) {}
@@ -40,7 +39,7 @@ class CurrentUserProvider extends StateNotifier<UserDto?> {
}
final currentUserProvider =
StateNotifierProvider<CurrentUserProvider, UserDto?>((ref) {
StateNotifierProvider<CurrentUserProvider, User?>((ref) {
return CurrentUserProvider(
ref.watch(apiServiceProvider),
);

View File

@@ -1,5 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/activity_api.interface.dart';
import 'package:immich_mobile/models/activities/activity.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
@@ -60,7 +60,7 @@ class ActivityApiRepository extends ApiRepository
type: dto.type == ReactionType.comment
? ActivityType.comment
: ActivityType.like,
user: UserConverter.fromSimpleUserDto(dto.user),
user: User.fromSimpleUserDto(dto.user),
assetId: dto.assetId,
comment: dto.comment,
);

View File

@@ -1,11 +1,9 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
as entity;
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/album.interface.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart';
import 'package:immich_mobile/providers/db.provider.dart';
@@ -45,11 +43,11 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
}
if (owner == true) {
query = query.owner(
(q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).id),
(q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId),
);
} else if (owner == false) {
query = query.owner(
(q) => q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).id),
(q) => q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).isarId),
);
}
if (remote == true) {
@@ -102,9 +100,8 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
Future<Album?> get(int id) => db.albums.get(id);
@override
Future<void> removeUsers(Album album, List<UserDto> users) => txn(
() => album.sharedUsers.update(unlink: users.map(entity.User.fromDto)),
);
Future<void> removeUsers(Album album, List<User> users) =>
txn(() => album.sharedUsers.update(unlink: users));
@override
Future<void> addAssets(Album album, List<Asset> assets) =>
@@ -124,8 +121,8 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
}
@override
Future<void> addUsers(Album album, List<UserDto> users) =>
txn(() => album.sharedUsers.update(link: users.map(entity.User.fromDto)));
Future<void> addUsers(Album album, List<User> users) =>
txn(() => album.sharedUsers.update(link: users));
@override
Future<void> deleteAllLocal() =>
@@ -144,11 +141,11 @@ class AlbumRepository extends DatabaseRepository implements IAlbumRepository {
switch (filterMode) {
case QuickFilterMode.sharedWithMe:
query = query.owner(
(q) => q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).id),
(q) => q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).isarId),
);
case QuickFilterMode.myAlbums:
query = query.owner(
(q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).id),
(q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId),
);
case QuickFilterMode.all:
break;

View File

@@ -2,9 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
as entity;
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/album_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart';
@@ -166,12 +164,11 @@ class AlbumApiRepository extends ApiRepository implements IAlbumApiRepository {
sortOrder: dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc,
);
album.remoteAssetCount = dto.assetCount;
album.owner.value =
entity.User.fromDto(UserConverter.fromSimpleUserDto(dto.owner));
album.owner.value = User.fromSimpleUserDto(dto.owner);
album.remoteThumbnailAssetId = dto.albumThumbnailAssetId;
final users = dto.albumUsers
.map((albumUser) => UserConverter.fromSimpleUserDto(albumUser.user));
album.sharedUsers.addAll(users.map(entity.User.fromDto));
.map((albumUser) => User.fromSimpleUserDto(albumUser.user));
album.sharedUsers.addAll(users);
final assets = dto.assets.map(Asset.remote).toList();
album.assets.addAll(assets);
return album;

View File

@@ -3,7 +3,6 @@ import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:photo_manager/photo_manager.dart' hide AssetType;
@@ -87,7 +86,7 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
shared: false,
activityEnabled: false,
);
album.owner.value = User.fromDto(Store.get(StoreKey.currentUser));
album.owner.value = Store.get(StoreKey.currentUser);
album.localId = assetPathEntity.id;
album.isAll = assetPathEntity.isAll;
return album;

View File

@@ -24,7 +24,7 @@ class AssetMediaRepository implements IAssetMediaRepository {
final Asset asset = Asset(
checksum: "",
localId: local.id,
ownerId: Store.get(StoreKey.currentUser).id,
ownerId: Store.get(StoreKey.currentUser).isarId,
fileCreatedAt: local.createDateTime,
fileModifiedAt: local.modifiedDateTime,
updatedAt: local.modifiedDateTime,

View File

@@ -6,8 +6,8 @@ import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/auth.interface.dart';
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
import 'package:immich_mobile/providers/db.provider.dart';

View File

@@ -1,7 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'
as entity;
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/partner.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';
@@ -16,40 +14,34 @@ class PartnerRepository extends DatabaseRepository
PartnerRepository(super.db);
@override
Future<List<UserDto>> getSharedBy() async {
return (await db.users
.filter()
.isPartnerSharedByEqualTo(true)
.sortById()
.findAll())
.map((u) => u.toDto())
.toList();
Future<List<User>> getSharedBy() {
return db.users
.filter()
.isPartnerSharedByEqualTo(true)
.sortById()
.findAll();
}
@override
Future<List<UserDto>> getSharedWith() async {
return (await db.users
.filter()
.isPartnerSharedWithEqualTo(true)
.sortById()
.findAll())
.map((u) => u.toDto())
.toList();
Future<List<User>> getSharedWith() {
return db.users
.filter()
.isPartnerSharedWithEqualTo(true)
.sortById()
.findAll();
}
@override
Stream<List<UserDto>> watchSharedBy() {
return (db.users.filter().isPartnerSharedByEqualTo(true).sortById().watch())
.map((users) => users.map((u) => u.toDto()).toList());
Stream<List<User>> watchSharedBy() {
return db.users.filter().isPartnerSharedByEqualTo(true).sortById().watch();
}
@override
Stream<List<UserDto>> watchSharedWith() {
return (db.users
.filter()
.isPartnerSharedWithEqualTo(true)
.sortById()
.watch())
.map((users) => users.map((u) => u.toDto()).toList());
Stream<List<User>> watchSharedWith() {
return db.users
.filter()
.isPartnerSharedWithEqualTo(true)
.sortById()
.watch();
}
}

View File

@@ -1,6 +1,5 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/utils/user.converter.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/api.repository.dart';
@@ -19,7 +18,7 @@ class PartnerApiRepository extends ApiRepository
PartnerApiRepository(this._api);
@override
Future<List<UserDto>> getAll(Direction direction) async {
Future<List<User>> getAll(Direction direction) async {
final response = await checkNull(
_api.getPartners(
direction == Direction.sharedByMe
@@ -27,26 +26,26 @@ class PartnerApiRepository extends ApiRepository
: PartnerDirection.with_,
),
);
return response.map(UserConverter.fromPartnerDto).toList();
return response.map(User.fromPartnerDto).toList();
}
@override
Future<UserDto> create(String id) async {
Future<User> create(String id) async {
final dto = await checkNull(_api.createPartner(id));
return UserConverter.fromPartnerDto(dto);
return User.fromPartnerDto(dto);
}
@override
Future<void> delete(String id) => _api.removePartner(id);
@override
Future<UserDto> update(String id, {required bool inTimeline}) async {
Future<User> update(String id, {required bool inTimeline}) async {
final dto = await checkNull(
_api.updatePartner(
id,
UpdatePartnerDto(inTimeline: inTimeline),
),
);
return UserConverter.fromPartnerDto(dto);
return User.fromPartnerDto(dto);
}
}

View File

@@ -2,7 +2,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/timeline.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/database.repository.dart';

Some files were not shown because too many files have changed in this diff Show More