Compare commits

...

129 Commits

Author SHA1 Message Date
Alex Tran e04d25d8f5 chore(mobile): better second to first assets shown 2024-06-12 16:58:58 -05:00
Alex c642150b85 chore(mobile): post release task (#10228) 2024-06-12 14:17:58 -05:00
Alex The Bot a8a7d29891 Version v1.106.3 2024-06-12 18:26:10 +00:00
Alex 67e98ed313 fix(mobile): video player not updating state (#10220)
* fix(mobile): video player not updating state

* unused code
2024-06-12 12:43:01 -05:00
renovate[bot] 47ef48e3c2 chore(deps): update base-image to v20240611 (major) (#10118)
chore(deps): update base-image to v20240611

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-12 12:51:46 -04:00
waclaw66 376feadb76 fix(web): missing svelte translations (#10199)
* fix(web): missing svelte translations

* fixes

* format fix

* translation keys fix

* "merge" key fix

* Update web/src/lib/components/shared-components/side-bar/more-information-albums.svelte

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>

* Update web/src/lib/i18n/en.json

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>

* suggestion fix

* trash pluralization

* video+photo count fix

* format fix

* unused removal

* translation key fix

* duplicate key removal

* format fix

---------

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
2024-06-12 17:37:46 +01:00
Jason Rasmussen 3d82005797 fix: no floats (replace with doubles) (#10218)
* fix: no floats (replace with doubles)

* Update server/src/utils/misc.ts

Co-authored-by: Zack Pollard <zackpollard@ymail.com>

---------

Co-authored-by: Zack Pollard <zackpollard@ymail.com>
2024-06-12 17:36:24 +01:00
Weblate (bot) 10aa00af21 chore(web): update translations (#10216)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translation: Immich/immich

Co-authored-by: Mario <17320863+myanesp@users.noreply.github.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: opl- <jakub.trzy@op.pl>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
2024-06-12 17:35:04 +01:00
Zack Pollard 1f8bdcdce7 chore: renovate shouldn't update mobile native dependencies (#10217) 2024-06-12 17:00:54 +01:00
Jason Rasmussen 98ebfc22f8 chore: translations from mobile (#10214) 2024-06-12 15:47:51 +01:00
Weblate (bot) 032b99fe93 chore(web): update translations (#10203)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_devel/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: Alexandr Zhytnyk <oper.kh@gmail.com>
Co-authored-by: Amadeous <am4d3ous@users.noreply.hosted.weblate.org>
Co-authored-by: Beniamin Iorga <beniiorga@gmail.com>
Co-authored-by: Denis Pacquier <denis.pacquier@gmail.com>
Co-authored-by: Eero Jääskeläinen <eero.jaaskelainen@gmail.com>
Co-authored-by: Fanfouer <fanfouer@outlook.com>
Co-authored-by: Jan <jan.widmer.ch@gmail.com>
Co-authored-by: Kentai Radiquum <kentai.waah@gmail.com>
Co-authored-by: Kim <shnukoms@users.noreply.hosted.weblate.org>
Co-authored-by: Maximilian Waidelich <44324946+maxwai@users.noreply.github.com>
Co-authored-by: Maximilian Waidelich <maximilian.waidelich@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: PolarisYHNL <polarisyhnl@yeah.net>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Thomas <thomas.ceccato.02@gmail.com>
Co-authored-by: Yves ANDOLFATTO <register@yves.aleeas.com>
Co-authored-by: ZtereoHYPE <me@ztereohype.dev>
Co-authored-by: carcawey <dacarva@gmail.com>
Co-authored-by: clementdelestre <clementdelestre@gmail.com>
Co-authored-by: eav5jhl0 <eav5jhl0@users.noreply.hosted.weblate.org>
Co-authored-by: mgabor <mgabor@users.noreply.hosted.weblate.org>
Co-authored-by: opl- <jakub.trzy@op.pl>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: wariw <wariwpl@gmail.com>
Co-authored-by: Владислав Потаенко <vipotaenko02@gmail.com>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
2024-06-12 15:38:18 +01:00
Zack Pollard 07156135c2 fix(server): double counting cores when processor name includes the word "processor" (#10211) 2024-06-12 13:49:20 +00:00
Michel Heusschen 9dbf5db72e fix(server): checkExistingAssets (#10192)
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-06-12 08:48:44 -05:00
Daniel Heppner 52170423be feat(web): select all duplicates (#10189)
* feat(web): select all duplicates

Allows users to select or deselect all duplicate photos when removing duplicates

* styling

* chore(web): add more translations to duplicates page

* color

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
Co-authored-by: Zack Pollard <zackpollard@ymail.com>
2024-06-12 13:01:55 +00:00
Zack Pollard ae095baad3 fix(server): only run healthchecks when api worker is running on immich-server (#10204)
fix: only run healthchecks when api worker is running on immich-server
2024-06-12 12:44:30 +01:00
Michel Heusschen f99f289f74 fix(web): small translation issues + remove unused (#10200)
* fix(web): small translation issues + remove unused

* more unused keys

* formatting

* fix(web): incorrectly used translations

* fix and remove unused translations

---------

Co-authored-by: Zack Pollard <zackpollard@ymail.com>
2024-06-12 12:13:10 +01:00
Alex 476eea44df chore(web): remove thumbnail usage for places card (#10142)
* chore(web): remove thumbnail usage for places

* remove href attribute from Thumbnail

* linting
2024-06-12 11:12:58 +00:00
Jason Rasmussen e84657192c refactor: config caching (#10168) 2024-06-12 11:07:35 +00:00
Mert 5dda5d93f5 chore(docs): remove microservices from hwa docs (#10188)
remove microservices from hwa docs
2024-06-12 11:57:40 +01:00
Michel Heusschen 6260caf649 fix(web): multi file upload in albums (#10190) 2024-06-12 11:57:11 +01:00
Michel Heusschen 9e5c52b7b7 chore(web): more translations for user settings and admin pages (#10161)
* chore(web): more translations for user settings and admin pages

* JobSettings translations

* feedback

* missed one

* feedback
2024-06-12 11:54:40 +01:00
Weblate (bot) 0e1311e3d3 chore(web): update translations (#10152)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: 94tiger <94tiger@naver.com>
Co-authored-by: Adrian <adrian.hundseth@gmail.com>
Co-authored-by: Andrej Kralj <andrej.kralj@gmail.com>
Co-authored-by: AngelaDMerkel <personal@caduffy.com>
Co-authored-by: Anton <ajp_anton@hotmail.com>
Co-authored-by: Beniamin Iorga <beniiorga@gmail.com>
Co-authored-by: CanbiZ <mickey.leskowitz@gmail.com>
Co-authored-by: Eero Jääskeläinen <eero.jaaskelainen@gmail.com>
Co-authored-by: Eryk Michalak <gnu.ewm@protonmail.com>
Co-authored-by: Flowake <weblate.cx6on@passmail.net>
Co-authored-by: Immich <immich@futo.org>
Co-authored-by: Jakub <jakubula.jm@gmail.com>
Co-authored-by: Jan <account@thebraker.net>
Co-authored-by: Jan <jan.widmer.ch@gmail.com>
Co-authored-by: Jason Dean Lessenich <jasonlessenich@gmail.com>
Co-authored-by: Joachim Klahr <joachim@klahr.se>
Co-authored-by: Joseph <josephlegrand33+hosted.weblate.org@gmail.com>
Co-authored-by: Julien Deveaux <julien.deveaux@hotmail.com>
Co-authored-by: Kentai Radiquum <kentai.waah@gmail.com>
Co-authored-by: Kim <shnukoms@users.noreply.hosted.weblate.org>
Co-authored-by: Kyle Park <mysky3056@gmail.com>
Co-authored-by: League2EB <info@league2eb.me>
Co-authored-by: Londoneye02 <jcdelcaz@gmail.com>
Co-authored-by: Luca Kröger <l.kroeger01@gmail.com>
Co-authored-by: Manic87 <nicolas@familie-mach.net>
Co-authored-by: Marcos Besteiro López (MarcosBL) <marcosbl@gmail.com>
Co-authored-by: MeisterEder286 <walbrun.johann@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Miko-Matias Grönvall <matias.gronvall@gmail.com>
Co-authored-by: MozPri <primoz.arh@gmail.com>
Co-authored-by: Nathan <bonnemainsnathan@gmail.com>
Co-authored-by: Ole Morten Didriksen <code@oledid.com>
Co-authored-by: Pavel Shamshin <odan@selaz.org>
Co-authored-by: Peter Suba <peter.suba@gmail.com>
Co-authored-by: Pheggas <petko252@gmail.com>
Co-authored-by: PolarisYHNL <polarisyhnl@yeah.net>
Co-authored-by: Ptsa Daniel <ptsa1987@gmail.com>
Co-authored-by: RWDai <869759838@qq.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Simmer Lajos <weblate.linguini033@passinbox.com>
Co-authored-by: SisyphusMD <guardian.note2892@fastmail.com>
Co-authored-by: Smiehoo <github@pocz.net>
Co-authored-by: Thomas <thomas.ceccato.02@gmail.com>
Co-authored-by: Tomas Babej <web+weblate@tbabej.com>
Co-authored-by: Tomek <tjomek@gmail.com>
Co-authored-by: VB <Victor2B@protonmail.com>
Co-authored-by: Vojtěch Bargl <bargl.vojtech@gmail.com>
Co-authored-by: YFrendo <yann.frendo@live.fr>
Co-authored-by: Yves ANDOLFATTO <register@yves.aleeas.com>
Co-authored-by: ZtereoHYPE <me@ztereohype.dev>
Co-authored-by: biglate <bigtech+weblate@aleeas.com>
Co-authored-by: carcawey <dacarva@gmail.com>
Co-authored-by: clementdelestre <clementdelestre@gmail.com>
Co-authored-by: eav5jhl0 <eav5jhl0@users.noreply.hosted.weblate.org>
Co-authored-by: ferrets <ferrets@live.cn>
Co-authored-by: frauhottelmann <frauhottelmann@gmail.com>
Co-authored-by: gilo <giantlolli@proton.me>
Co-authored-by: grgergo <gergo_g@proton.me>
Co-authored-by: guillezcurra <guillezcurra@gmail.com>
Co-authored-by: ingria <codefuhrer@gmail.com>
Co-authored-by: jie65535 <jie65535@qq.com>
Co-authored-by: myurar1a <sirometroid1235@outlook.jp>
Co-authored-by: sephrat <florian.dupret@gmail.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
Co-authored-by: Кирилл Москатов <kirillmoskatov@gmail.com>
2024-06-12 11:52:33 +01:00
Stephen Smith 216cca4383 fix(server): exiftool largefilesupport only set for the first call (#10167)
* Revert "feat(server): enable exiftool largefilesupport (#9894)"

This reverts commit afa10ebcb2.

* feat(server): enable exiftool largefilesupport by passing options to read
2024-06-12 05:43:38 -05:00
Mert cdc98de848 fix(server): increase pixel limit for thumbnail generation (#10181)
disable input limit
2024-06-11 22:11:03 -04:00
Mert 126cbeabe8 feat(server): add av1 support for vaapi (#10180)
add av1
2024-06-12 00:24:06 +00:00
Mert 2e0c6f6fff fix: postgres health check reporting any db without checksums as unhealthy (#10178)
handle disabled checksumming
2024-06-12 00:18:24 +00:00
Alex The Bot 81790ab166 Version v1.106.2 2024-06-11 19:09:13 +00:00
Alejandro Armas 69b948f3d0 fix(mobile): Motion Photos stopping music (#10151)
Add videoPlayer opt to prevent motionPhotos pausing music
2024-06-11 11:14:49 -05:00
Weblate (bot) 4b2ed28b1a chore: update translations (#10141)
chore(web): update translations

Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: Alex van den Hoogen <alex3305@gmail.com>
Co-authored-by: CanbiZ <mickey.leskowitz@gmail.com>
Co-authored-by: Eryk Michalak <gnu.ewm@protonmail.com>
Co-authored-by: Jordi Masip <jordi@masip.cat>
Co-authored-by: Joseph <josephlegrand33+hosted.weblate.org@gmail.com>
Co-authored-by: Kentai Radiquum <kentai.waah@gmail.com>
Co-authored-by: Manic87 <nicolas@familie-mach.net>
Co-authored-by: Marcos Besteiro López (MarcosBL) <marcosbl@gmail.com>
Co-authored-by: Mario <shopping.uncate@aleeas.com>
Co-authored-by: Michał Kulik <michal.kulik91@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: PolairsYHNL-Immich <polarisyhnl@gmail.com>
Co-authored-by: PolarisYHNL <polarisyhnl@yeah.net>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Sophie <mail@sopht.li>
Co-authored-by: Zack Pollard <zack@futo.org>
Co-authored-by: upsetdog <upsetdog@proton.me>
Co-authored-by: Łukasz Kierepka <lukasz_kierepka@hotmail.com>
2024-06-11 17:06:53 +01:00
Michel Heusschen b8e6ae65b1 fix(web): backwards asset navigation in GalleryViewer (#10132)
* fix(web): backwards asset navigation in GalleryViewer

* fix ctrl/cmd click

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-06-11 15:27:18 +00:00
renovate[bot] 36bdbf93a7 fix(deps): update machine-learning (#10099)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-11 11:24:10 -04:00
Alex 3eee6c4dcf fix(web): cannot view image when metadata sharing is turned off for public sharing (#10145)
* fix(web): cannot view image when metadata sharing is turned off for public sharing

* Update web/src/lib/utils/asset-utils.ts

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-06-11 15:23:48 +00:00
Michel Heusschen 3a3676bc82 fix(server): cache-control header missing from / requests (#10131)
* fix(server): cache-control header missing from / requests

* disable extension fallback
2024-06-11 10:18:52 -05:00
Zack Pollard 34fc572276 chore: update translations (#10140)
* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 81.0% (631 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 81.0% (631 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

---------

Co-authored-by: PolairsYHNL-Immich <polarisyhnl@gmail.com>
Co-authored-by: Zack Pollard <zack@futo.org>
2024-06-11 15:28:03 +01:00
Zack Pollard ef17c257ef chore: update translations (#10138)
* chore:  (German)

Currently translated at 8.9% (70 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 8.9% (70 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Spanish)

Currently translated at 4.2% (33 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/

* chore:  (French)

Currently translated at 2.4% (19 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (French)

Currently translated at 2.4% (19 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (Russian)

Currently translated at 0.5% (4 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 75.6% (589 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 75.6% (589 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (French)

Currently translated at 4.1% (32 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (French)

Currently translated at 4.1% (32 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (French)

Currently translated at 4.1% (32 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (Italian)

Currently translated at 3.9% (31 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/

* chore:  (Italian)

Currently translated at 3.9% (31 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/

* chore:  (Czech)

Currently translated at 2.6% (21 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 75.7% (590 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 75.7% (590 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Russian)

Currently translated at 0.7% (6 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Dutch)

Currently translated at 5.9% (46 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/

* chore:  (Italian)

Currently translated at 4.4% (35 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/

* chore:  (Polish)

Currently translated at 0.6% (5 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/

* chore:  (Russian)

Currently translated at 1.9% (15 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/

* chore:  (Russian)

Currently translated at 1.9% (15 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Dutch)

Currently translated at 6.0% (47 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Russian)

Currently translated at 2.0% (16 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 80.2% (625 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 80.2% (625 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Vietnamese)

Currently translated at 0.5% (4 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/

---------

Co-authored-by: CanbiZ <mickey.leskowitz@gmail.com>
Co-authored-by: Luca Kröger <l.kroeger01@gmail.com>
Co-authored-by: Héctor Martínez Juste <hectorzin@hotmail.com>
Co-authored-by: Nathan <bonnemainsnathan@gmail.com>
Co-authored-by: Fanfouer <fanfouer@outlook.com>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
Co-authored-by: PolarisYHNL <polarisyhnl@yeah.net>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Sophie <mail@sopht.li>
Co-authored-by: Stefan Gries <stefan@gries.nrw>
Co-authored-by: Bouchet Mateo <mateo.bouchet+hosted.weblate.org@mhaz42.fr>
Co-authored-by: Alessandro Saglia <webslate.eskimo0977@bear-d.me>
Co-authored-by: ZtereoHYPE <me@ztereohype.dev>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: jie65535 <jie65535@qq.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Eryk Michalak <gnu.ewm@protonmail.com>
Co-authored-by: Pavel Shamshin <odan@selaz.org>
Co-authored-by: Quan <weiyideai520@hotmail.com>
Co-authored-by: PolairsYHNL-Immich <polarisyhnl@gmail.com>
Co-authored-by: Zack Pollard <zack@futo.org>
2024-06-11 15:16:23 +01:00
Weblate (bot) 4c69cb89d7 chore: update translations (#10125)
* chore:  (German)

Currently translated at 8.9% (70 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 8.9% (70 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Spanish)

Currently translated at 4.2% (33 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/

* chore:  (French)

Currently translated at 2.4% (19 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (French)

Currently translated at 2.4% (19 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (Russian)

Currently translated at 0.5% (4 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 75.6% (589 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 75.6% (589 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (French)

Currently translated at 4.1% (32 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (French)

Currently translated at 4.1% (32 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (French)

Currently translated at 4.1% (32 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (Italian)

Currently translated at 3.9% (31 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/

* chore:  (Italian)

Currently translated at 3.9% (31 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/

* chore:  (Czech)

Currently translated at 2.6% (21 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 75.7% (590 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 75.7% (590 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Russian)

Currently translated at 0.7% (6 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Dutch)

Currently translated at 5.9% (46 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/

* chore:  (Italian)

Currently translated at 4.4% (35 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/

* chore:  (Polish)

Currently translated at 0.6% (5 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/

* chore:  (Russian)

Currently translated at 1.9% (15 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/

* chore:  (Russian)

Currently translated at 1.9% (15 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Dutch)

Currently translated at 6.0% (47 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

---------

Co-authored-by: CanbiZ <mickey.leskowitz@gmail.com>
Co-authored-by: Luca Kröger <l.kroeger01@gmail.com>
Co-authored-by: Héctor Martínez Juste <hectorzin@hotmail.com>
Co-authored-by: Nathan <bonnemainsnathan@gmail.com>
Co-authored-by: Fanfouer <fanfouer@outlook.com>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
Co-authored-by: PolarisYHNL <polarisyhnl@yeah.net>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Sophie <mail@sopht.li>
Co-authored-by: Stefan Gries <stefan@gries.nrw>
Co-authored-by: Bouchet Mateo <mateo.bouchet+hosted.weblate.org@mhaz42.fr>
Co-authored-by: Alessandro Saglia <webslate.eskimo0977@bear-d.me>
Co-authored-by: ZtereoHYPE <me@ztereohype.dev>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: jie65535 <jie65535@qq.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Eryk Michalak <gnu.ewm@protonmail.com>
Co-authored-by: Pavel Shamshin <odan@selaz.org>
Co-authored-by: Quan <weiyideai520@hotmail.com>
Co-authored-by: PolairsYHNL-Immich <polarisyhnl@gmail.com>
2024-06-11 13:42:57 +00:00
Jason Rasmussen 735455508c feat(cli): auto-release (#10127) 2024-06-11 08:33:36 -05:00
Alex eba166a2f1 fix(web): cannot click on explore place (#10121) 2024-06-11 08:32:39 -05:00
Jason Rasmussen 8cf8a2cb35 chore(cli): prepare release (#10124) 2024-06-11 12:16:49 +00:00
Zack Pollard 1767ed2192 chore(web): enable prettier json key sorting recursively (#10120) 2024-06-11 12:52:20 +01:00
Zack Pollard 3c15dae341 docs: fix archive script labels and change to variable to nextVersion (#10119) 2024-06-11 12:37:20 +01:00
Zack Pollard 8568c2e8b9 docs: add archived docs back to v1.100.0 (#10116)
chore: add archived docs back to v1.100.0
2024-06-11 12:36:59 +01:00
Alex d558ea819a fix(web): cannot perform duplication actions as normal user (#10115)
* fix(web): cannot perform duplication actions as normal user

* use immich dialog
2024-06-11 11:30:42 +00:00
Alex 60701d131e chore(mobile): post release pump (#10114) 2024-06-11 06:26:52 -05:00
Alex 04808f8b5c fix(mobile): warning message not resetting when changing server URL (#10112) 2024-06-11 10:48:49 +00:00
Zack Pollard 8a866297f7 docs: fix archive version url to include v prefix (#10111)
* docs: fix archive version url to include v prefix

* docs: fix archived versions to add missing v to 1.106.1
2024-06-11 05:43:39 -05:00
Alex The Bot b5991c908e Version v1.106.1 2024-06-11 09:39:23 +00:00
Jason Rasmussen 321c3ccfc6 docs: version switcher (#10091)
* docs: version switcher

* chore: pump script

* chore: fix linting on bash script

* chore: remove 1.106.0 from archived versions

* chore: change version archive script to take next server version not current version

---------

Co-authored-by: Zack Pollard <zackpollard@ymail.com>
2024-06-11 10:33:45 +01:00
Weblate (bot) 05874bd84e chore: update translations (#10096)
* chore:  (German)

Currently translated at 5.9% (46 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Italian)

Currently translated at 3.5% (28 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/

* chore:  (German)

Currently translated at 8.7% (68 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 74.9% (584 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Dutch)

Currently translated at 5.7% (45 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/

* chore:  (English (Developer))

Currently translated at 100.0% (779 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_devel/

---------

Co-authored-by: Martin Bosner <martin@bosner.de>
Co-authored-by: Alessandro Saglia <webslate.eskimo0977@bear-d.me>
Co-authored-by: Sophie <mail@sopht.li>
Co-authored-by: jie65535 <jie65535@qq.com>
Co-authored-by: Zack Pollard <zack@futo.org>
2024-06-11 10:09:58 +01:00
Michel Heusschen 79705dc58d fix(web): language selector for chinese and norwegian (#10107)
* fix(web): language selector for chinese and norwegian

* add unit test

* formatter

* undo name change
2024-06-11 09:07:42 +00:00
aviv926 71a132b0b8 docs: Update the system settings page (#10094)
* updating

* npm run format

* fix \ > /
2024-06-11 03:52:29 -05:00
Alex d14f23497c fix(server): album update disable no effect (#10090) 2024-06-11 03:51:58 -05:00
Jason Rasmussen a916df56ee fix: roadmap docs (#10095) 2024-06-10 22:11:50 +00:00
aviv926 73dcb9b452 docs: add detect duplicate assets to roadmap (#10093)
* updating

* uncomment
2024-06-10 21:49:10 +00:00
Alex The Bot f32c02bd25 Version v1.106.0 2024-06-10 17:50:00 +00:00
Zack Pollard b16c9405d8 docs: otel metrics port worker split (#10085) 2024-06-10 12:44:10 -05:00
Alex 46df165ef2 feat(mobile): compatibility message warning (#10065)
* feat(mobile): compatibility message warning

* refactor and better signature
2024-06-10 12:43:54 -05:00
Zack Pollard 19e35d8d3f chore(server): remove unused imagemin type dependency (#10084) 2024-06-10 17:08:25 +00:00
Alex c4c070569f fix(web): mouse-wheel scrolling on detail panel is disabled (#10080) 2024-06-10 12:05:52 -05:00
Jason Rasmussen 7651f70c88 fix(server): asset delete logic (#10077)
* fix(server): asset delete logic

* test: e2e
2024-06-10 13:04:34 -04:00
Alex 4698c39855 chore: remove pr labeler requirement (#10081) 2024-06-10 12:59:19 -04:00
Zack Pollard 2f2aecfb47 fix(server): otel not working due to port conflicts after combining containers (#10078)
fix: otel not working due to port conflicts after combining containers

Fixes #9759
2024-06-10 16:01:04 +00:00
dependabot[bot] 20efd82461 chore(deps): bump docker/build-push-action from 5.3.0 to 5.4.0 (#10069)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5.3.0 to 5.4.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5.3.0...v5.4.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 14:52:50 +01:00
Zack Pollard 22a0b4d900 chore(web): order json files alphabetically (#10076) 2024-06-10 09:37:21 -04:00
Weblate (bot) 2f25a8a437 chore: update translations (#10075)
chore:  (Vietnamese)

Currently translated at 0.3% (3 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/

Co-authored-by: Zack Pollard <zack@futo.org>
2024-06-10 14:16:21 +01:00
Weblate (bot) 7a0bc0ea87 chore: update translations (#10074)
chore:  (Vietnamese)

Currently translated at 0.2% (2 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/

Co-authored-by: Zack Pollard <zack@futo.org>
2024-06-10 14:09:12 +01:00
Weblate (bot) a564c80193 chore: update translations (#10073)
chore:  (Vietnamese)

Currently translated at 0.1% (1 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/

Co-authored-by: Zack Pollard <zack@futo.org>
2024-06-10 14:06:20 +01:00
Weblate (bot) f4671617d1 chore: update translations (#10072)
chore:  (Vietnamese)

Currently translated at 0.1% (1 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/

Co-authored-by: Zack Pollard <zack@futo.org>
2024-06-10 14:03:48 +01:00
Zack Pollard d331da0ced chore(web): fix weblate conflicts (#10071)
* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 29.2% (228 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 29.2% (228 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 29.2% (228 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Dutch)

Currently translated at 5.8% (46 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 51.2% (400 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 51.2% (400 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 51.2% (400 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (German)

Currently translated at 5.7% (45 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 5.7% (45 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Hungarian)

Currently translated at 0.1% (1 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 55.3% (432 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 55.3% (432 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (German)

Currently translated at 5.7% (45 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Dutch)

Currently translated at 5.8% (46 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/

* chore:  (Spanish)

Currently translated at 0.1% (1 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/

* chore:  (Arabic)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/

* chore:  (Catalan)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/

* chore:  (Danish)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/

* chore:  (Finnish)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/

* chore:  (French)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (Hebrew)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/

* chore:  (Hindi)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hi/

* chore:  (Hungarian)

Currently translated at 0.1% (1 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/

* chore:  (Italian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/

* chore:  (Japanese)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/

* chore:  (Korean)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/

* chore:  (Lithuanian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/

* chore:  (Latvian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lv/

* chore:  (Mongolian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/mn/

* chore:  (Norwegian Bokmål)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nb_NO/

* chore:  (Polish)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/

* chore:  (Portuguese)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt/

* chore:  (Romanian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/

* chore:  (Russian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/

* chore:  (Slovak)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/

* chore:  (Slovenian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/

* chore:  (Serbian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr/

* chore:  (Swedish)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/

* chore:  (Thai)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/th/

* chore:  (Ukrainian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/

* chore:  (Vietnamese)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/

* chore:  (Czech)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 55.3% (432 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore(web): enable prettier for json files in web

---------

Co-authored-by: PolarisYHNL <polarisyhnl@yeah.net>
Co-authored-by: LLL <326867814@qq.com>
Co-authored-by: jie65535 <jie65535@qq.com>
Co-authored-by: bo0tzz <git@bo0tzz.me>
Co-authored-by: CanbiZ <mickey.leskowitz@gmail.com>
Co-authored-by: Manic87 <nicolas@familie-mach.net>
Co-authored-by: Peter Suba <peter.suba@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
2024-06-10 13:59:54 +01:00
aviv926 84da9abcbc docs: Add Email Notifications info (#9930)
* Add Email Notifications info

* remove spaces

* Add ` ` to smtp link

* Small fixes

* PR feedback

* npm run format:fix

* PR feedback

* update docs

* formatting

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-06-09 20:07:08 +00:00
Robert Schäfer 48eede59b9 refactor: dedicated icon for permanently delete (#10052)
Motivation
----------
It's a follow up to #10028. I think it would be better user experience if one can tell by the icon what the delete button is about to do.

I hope I caught all the occurences where one can permanently delete assets.

How to test
-----------
1. Visit e.g. `/trash`
2. If you select some assets, the delete button in the top right corner
   looks different.
2024-06-09 14:25:27 -05:00
Fynn Petersen-Frey 972c66d467 fix(server): proper asset sync (#10019)
* fix(server,mobile): proper asset sync

* fix CI issues

* only use id instead of createdAt+id

* remove createdAt index

* fix typo

* cleanup createdAt usage

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-06-09 14:19:28 -05:00
Robert Schäfer 69795a3763 refactor: remove dead code from Makefile (#10061)
Motivation
----------
I guess these make targets should have been deleted in 57136e48fb.

How to test
-----------
1. Nothing really, this removes dead code
2024-06-09 19:18:41 +00:00
Robert Schäfer 9c337223e6 ci: automatically apply PR labels (#10064)
Motivation
----------
For me as a new contributor it is frustrating to submit a PR and it will always fail. Even worse: I have to wait for another contributor with more power to assign the label for me.

This will improve developer experience, as some of the labels can be assigned automatically based on changed files.

How to test
-----------
1. Merge this PR
2. Submit a couple of PRs with changes in the respective directories
3. Labels should be automatically applied
4. "Enforce PR labels" github workflow will re-run when "Pull Request Labeler" completes
2024-06-09 14:18:02 -05:00
George Shao 4d862525bc feat(web): allow ctrl-click / cmd-click on photos (#9954)
* feat(web): allow ctrl-click / cmd-click on photos

* fix: photo opening when deselected bug

* fix: consistent naming

* remove redundant code

* fix: disabled picture is clickable in "add to album" grid

* remove unnecessary code

* cleanup

* fix file permissions

* fix: album selection bug

* fix: stack slideshow bug & search gallery viewer bug

* cleanup

* fix dark mode stack slideshow bug

* cleanup

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-06-08 15:33:23 -05:00
Robert Schäfer d8d64ecc45 fix: prevent trashing of trashed assets (#10028)
* fix: prevent trashing of trashed assets

Motivation
----------
This will improve user experience by hiding a pointless action.

You can not trash a trashed asset again. It won't get any trashier than it already is.

How to test
-----------
1. Visit route `/trash`
2. Click on an asset
3. Press "Delete" on your keyboard
4. Nothing happens
5. Try to find the trash button in the top right
6. You can't find it

* refactor: follow @michelheusschen's review

See:
https://github.com/immich-app/immich/pull/10028#pullrequestreview-2105296755

* refactor:  follow @michelheusschen's 2nd review

See: https://github.com/immich-app/immich/pull/10028#discussion_r1632057833
2024-06-08 15:03:39 -05:00
Alex e1e7de4d4c chore(mobile): translation update (#10053) 2024-06-08 19:51:36 +00:00
Mert df412d60c5 fix(server): min face detection score not being passed correctly (#10050)
fix min score not being passed correctly
2024-06-08 13:55:19 -04:00
Alex dbd9782763 fix(mobile): viewAsset URL string (#10044) 2024-06-08 10:50:14 +00:00
klahr 2519922dd3 fix(doc):language links updates (#10035)
* Added Swedish translation of README.

* Updated all READMEs.
2024-06-08 09:59:30 +00:00
Michel Heusschen 78b10bbcc6 fix(web): drag and drop with non english language (#10040) 2024-06-08 04:57:46 -05:00
Michel Heusschen 4ec47b4186 fix(web): storage migration description (#10041) 2024-06-08 04:57:18 -05:00
Alejandro Armas 3c5ba77e86 fix(server): server stats showing wrong quota usage (#10036)
* Add filter to exclude external libraries from the user quota usage

* Add filter to exclude external libraries from the user quota usage

* fix sql syntax
2024-06-08 04:56:11 -05:00
Mert 62f8bd80f4 fix(server): add fallback for video thumbnail generation (#10034)
they called me a madman
2024-06-08 04:55:05 -05:00
Weblate (bot) 69193598cb chore: update translations (#10038)
* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 29.2% (228 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 29.2% (228 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 29.2% (228 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Dutch)

Currently translated at 5.8% (46 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 51.2% (400 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 51.2% (400 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 51.2% (400 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

---------

Co-authored-by: PolarisYHNL <polarisyhnl@yeah.net>
Co-authored-by: LLL <326867814@qq.com>
Co-authored-by: jie65535 <jie65535@qq.com>
Co-authored-by: bo0tzz <git@bo0tzz.me>
2024-06-08 04:54:23 -05:00
Immich f92513be7e chore: remove translations (Spanish (es_ES@new)) 2024-06-07 15:00:16 -05:00
Immich b5e0a1cec8 chore: add translations (Spanish (es_ES@new)) 2024-06-07 15:00:04 -05:00
Daniel Dietzler 2ffde69caf fix: weblate (#10029)
* chore: add translations  (Spanish (es_ES@new))

* chore: remove translations (Spanish (es_ES@new))

---------

Co-authored-by: Immich <immich@futo.org>
2024-06-07 14:52:40 -05:00
Alex 9291c782d5 fix(web): weblate merge conflict (#10027)
* chore: add translations  (German)

* chore: add translations  (Dutch)

* chore: add translations  (Spanish)

* chore: add translations  (Spanish (es_ES@new))

* chore:  (Spanish)

Currently translated at 0.1% (1 of 769 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/

* chore: remove translations (Spanish (es_ES@new))

* chore: add translations  (Arabic)

* chore: add translations  (Catalan)

* chore: add translations  (Danish)

* chore: add translations  (Finnish)

* chore: add translations  (French)

* chore: add translations  (Hebrew)

* chore: add translations  (Hindi)

* chore: add translations  (Hungarian)

* chore: add translations  (Italian)

* chore: add translations  (Japanese)

* chore: add translations  (Korean)

* chore: add translations  (Lithuanian)

* chore: add translations  (Latvian)

* chore: add translations  (Mongolian)

* chore: add translations  (Norwegian Bokmål)

* chore: add translations  (Polish)

* chore: add translations  (Portuguese)

* chore: add translations  (Romanian)

* chore: add translations  (Russian)

* chore: add translations  (Slovak)

* chore: add translations  (Slovenian)

* chore: add translations  (Serbian)

* chore: add translations  (Swedish)

* chore: add translations  (Thai)

* chore: add translations  (Ukrainian)

* chore: add translations  (Vietnamese)

* chore: add translations  (Czech)

* chore: add translations  (Chinese (Simplified) (zh_SIMPLIFIED))

* chore:  (German)

Currently translated at 1.3% (10 of 769 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 25.7% (198 of 769 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 25.7% (198 of 769 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

---------

Co-authored-by: Immich <immich@futo.org>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
Co-authored-by: Manic87 <nicolas@familie-mach.net>
Co-authored-by: PolarisYHNL <polarisyhnl@yeah.net>
Co-authored-by: LLL <326867814@qq.com>
2024-06-07 14:50:23 -05:00
Alex b595881084 fix(web): translation (#10026) 2024-06-07 13:54:22 -05:00
Min Idzelis 4b49d3a85d feat: photo-viewer; use <img> instead of blob urls, simplify/refactor, avoid window.events (#9883)
* Photoviewer

* make copyImage/zoomToggle optional

* Add e2e test

* lint

* Accept bo0tzz suggestion

Co-authored-by: bo0tzz <git@bo0tzz.me>

* Bad merge and review comments

* unused import

---------

Co-authored-by: bo0tzz <git@bo0tzz.me>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-06-07 13:22:46 -05:00
Michel Heusschen def5f59242 fix(web): language setting (#10024) 2024-06-07 11:35:05 -05:00
Alex 9ac2ac2fcb feat(web): send test email button (#10011)
* feat(web): test email button

* openapi

* UI button

* Show notification

* pr feedback

* remove jobs

* send email directly from repository and add feedback

* avoid sending many emails

* linter

* pr feedback

* lint

* lint

* lint
2024-06-07 11:34:09 -05:00
Michel Heusschen d5f3d98dfc chore(web): use development lang for tests (#10025) 2024-06-07 16:12:39 +01:00
Zack Pollard 3e118793de chore(web): missing notification settings translations (#10022)
* chore: missing notification settings translations

* chore: admin library tasks description translation
2024-06-07 10:01:41 -05:00
Michel Heusschen c8f2d994c6 fix(web): translations (#10021) 2024-06-07 12:23:13 +01:00
Mert f2148ddf03 fix(server): video thumbnail colors when using webp (#10018)
use gbrpf32le
2024-06-07 02:43:10 -04:00
Mert 2b1b43a7e4 feat(ml): composable ml (#9973)
* modularize model classes

* various fixes

* expose port

* change response

* round coordinates

* simplify preload

* update server

* simplify interface

simplify

* update tests

* composable endpoint

* cleanup

fixes

remove unnecessary interface

support text input, cleanup

* ew camelcase

* update server

server fixes

fix typing

* ml fixes

update locustfile

fixes

* cleaner response

* better repo response

* update tests

formatting and typing

rename

* undo compose change

* linting

fix type

actually fix typing

* stricter typing

fix detection-only response

no need for defaultdict

* update spec file

update api

linting

* update e2e

* unnecessary dimension

* remove commented code

* remove duplicate code

* remove unused imports

* add batch dim
2024-06-07 03:09:47 +00:00
Snowknight26 7a46f80ddc feat(web): add archive shortcut to grid (#9499)
* feat(web): add archive shortcut to grid

* Fix error

* Don't unnecessarily pass parameter

* Use an existing function to close the menu

* Deduplicate type

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-06-06 19:23:49 -04:00
Weblate (bot) c6c480c882 chore: update translations (#10003)
* chore: add translations  (German)

* chore: add translations  (Dutch)

* chore: add translations  (Spanish)

* chore: add translations  (Spanish (es_ES@new))

* chore:  (Spanish)

Currently translated at 0.1% (1 of 769 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/

* chore: remove translations (Spanish (es_ES@new))

* chore: add translations  (Arabic)

* chore: add translations  (Catalan)

* chore: add translations  (Danish)

* chore: add translations  (Finnish)

* chore: add translations  (French)

* chore: add translations  (Hebrew)

* chore: add translations  (Hindi)

* chore: add translations  (Hungarian)

* chore: add translations  (Italian)

* chore: add translations  (Japanese)

* chore: add translations  (Korean)

* chore: add translations  (Lithuanian)

* chore: add translations  (Latvian)

* chore: add translations  (Mongolian)

* chore: add translations  (Norwegian Bokmål)

* chore: add translations  (Polish)

* chore: add translations  (Portuguese)

* chore: add translations  (Romanian)

* chore: add translations  (Russian)

* chore: add translations  (Slovak)

* chore: add translations  (Slovenian)

* chore: add translations  (Serbian)

* chore: add translations  (Swedish)

* chore: add translations  (Thai)

* chore: add translations  (Ukrainian)

* chore: add translations  (Vietnamese)

* chore: add translations  (Czech)

* chore: add translations  (Chinese (Simplified) (zh_SIMPLIFIED))

* chore: add base languages

---------

Co-authored-by: Immich <immich@futo.org>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-06-06 17:20:41 +00:00
Jason Rasmussen 4ad97ccded fix(server): closed connections (#10013) 2024-06-06 09:09:42 -05:00
renovate[bot] ca12f3b15f chore(deps): update grafana/grafana:11.0.0-ubuntu docker digest to dcd3ae7 (#9770)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-06 13:15:39 +01:00
renovate[bot] 86eb2525d7 chore(deps): update redis:6.2-alpine docker digest to d6c2911 (#9843)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-06 13:13:39 +01:00
renovate[bot] 079864dfbe chore(deps): update docker.io/redis:6.2-alpine docker digest to d6c2911 (#9842)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-06 13:13:15 +01:00
Michel Heusschen 051e6cfc0b fix(web): clear locale setting (#10008) 2024-06-06 07:47:22 -04:00
Zack Pollard 8f42766979 feat: seperate sub-process for api worker (#10000) 2024-06-06 11:56:57 +01:00
renovate[bot] 7e2a03a8d9 chore(deps): update base-image to v20240604 (major) (#10004)
chore(deps): update base-image to v20240604

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-05 19:20:36 -04:00
Daniel Dietzler 1947316b0b refactor: one locales file for all english translations (#10006)
one locales file for all english translations
2024-06-05 18:57:44 -04:00
Jason Rasmussen 0f976edf96 feat(server): log http exceptions (#9996) 2024-06-05 17:07:47 -04:00
Min Idzelis ce985ef8f8 fix: AssetInterceptor "can't set headers after they are sent" (#9987)
* fix: AssetInterceptor "can't set headers after they are sent"

* chore: remove long comment

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-06-05 09:30:19 -04:00
Jason Rasmussen cf223dc98c fix(web): show duplicate message (#9992) 2024-06-05 09:29:52 -04:00
Snowknight26 97ffddee7c feat(web): add an empty placeholder to the explore page (#9990)
* feat(web): add an empty placeholder to the explore page

* Change the message wording per suggestion

* fix: test

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-06-05 12:19:57 +00:00
Zack Pollard abf6fc25f7 chore: change default thumbnail concurrency and auto-detect container core count (#9981)
* feat: automatically detect amount of CPU cores and allow overriding with CPU_CORES env var

* chore: change default thumbnail concurrency to 3
2024-06-05 11:45:53 +01:00
Jan108 b2761b12d1 feat(web): Option to assign people to unassigned faces (#9773)
* added unassigned faces to people edit

* svelte fix

* fix format

* Captialized unassigned person name, removed person id from alttext, fixed problem with multiple faces per person

* Added faces to the getAssetInfo API endpoint

* Updated openApi clients

* Readded the photoeditor dependency

* fixed lint/format

* fixed photoViewer type

* changes getAssetInfo.faces to only include unassigned faces

* fix: bad merge

* title

* logic

---------

Co-authored-by: Jan108 <dasJan108@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-06-05 07:26:00 +00:00
Lukas 588860455f test(server): check motion asset create arguments (#9826) 2024-06-05 01:55:04 -05:00
renovate[bot] 643309b27f chore(deps): update node.js to 696ae41 (#9986)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-04 20:21:28 -04:00
renovate[bot] e0ec75119f chore(deps): update node.js to db6fa52 (#9984)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-04 15:56:12 -04:00
Manic-87 f446bc8caa feat(web): translations (#9854)
* First test

* Added translation using Weblate (French)

* Translated using Weblate (German)

Currently translated at 100.0% (4 of 4 strings)

Translation: immich/web
Translate-URL: http://familie-mach.net/projects/immich/web/de/

* Translated using Weblate (French)

Currently translated at 100.0% (4 of 4 strings)

Translation: immich/web
Translate-URL: http://familie-mach.net/projects/immich/web/fr/

* Further testing

* Further testing

* Translated using Weblate (German)

Currently translated at 100.0% (18 of 18 strings)

Translation: immich/web
Translate-URL: http://familie-mach.net/projects/immich/web/de/

* Further work

* Update string file.

* More strings

* Automatically changed strings

* Add automatically translated german file for testing purposes

* Fix merge-face-selector component

* Make server stats strings uppercase

* Fix uppercase string

* Fix some strings in jobs-panel

* Fix lower and uppercase strings. Add a few additional string. Fix a few unnecessary replacements

* Update german test translations

* Fix typo in locales file

* Change string keys

* Extract more strings

* Extract and replace some more strings

* Update testtranslationfile

* Change translation keys

* Fix rebase errors

* Fix one more rebase error

* Remove german translation file

* Co-authored-by: Daniel Dietzler <danieldietzler@users.noreply.github.com>

* chore: clean up translations

* chore: add new line

* fix formatting

* chore: fixes

* fix: loading and tests

---------

Co-authored-by: root <root@Blacki>
Co-authored-by: admin <admin@example.com>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2024-06-04 15:53:00 -04:00
renovate[bot] a2bccf23c9 chore(deps): update dependency @swc/core to v1.5.24 (#9983)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-04 09:39:43 -04:00
dependabot[bot] 6937440772 chore(deps): bump stumpylog/image-cleaner-action from 0.6.0 to 0.7.0 (#9979)
Bumps [stumpylog/image-cleaner-action](https://github.com/stumpylog/image-cleaner-action) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/stumpylog/image-cleaner-action/releases)
- [Changelog](https://github.com/stumpylog/image-cleaner-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stumpylog/image-cleaner-action/compare/v0.6.0...v0.7.0)

---
updated-dependencies:
- dependency-name: stumpylog/image-cleaner-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-04 10:17:58 +00:00
renovate[bot] 69bce6680f fix(deps): update typescript-projects (#9971)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-04 10:35:24 +01:00
renovate[bot] 454c995e90 chore(deps): update machine-learning (#9969) 2024-06-03 22:41:10 -04:00
renovate[bot] bcff21f72b fix(deps): update dependency exiftool-vendored to v26.1.0 (#9972)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-03 21:38:03 -04:00
renovate[bot] 47ec6c41ec chore(deps): update node.js to 2d0ce60 (#9970)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-03 21:35:41 -04:00
Snowknight26 471cf5eaf7 fix(web): fix sidebar tooltip pluralization (#9952)
* fix(web): fix sidebar tooltip pluralization

* Rename property

* Remove data-testid attribute

* Fix variable type
2024-06-03 21:35:17 -04:00
Alex b3ee394fdc feat(web): email notification preference settings (#9934)
* feat(web): email notification preference settings

* Update

* remove failed api generation file

* fix handle album invite return value

* Update web/src/lib/components/user-settings-page/notifications-settings.svelte

Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>

* wording

* test

---------

Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2024-06-03 16:00:20 -05:00
aviv926 15474e81b2 docs: Update Authentik example (#9950)
* Update

* npm run format:fix

* more npm run format:fix
2024-06-03 10:57:09 +01:00
renovate[bot] bb9e18247b chore(deps): update terraform cloudflare to v4.34.0 (#9953)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-03 10:07:10 +01:00
Mathias Remshardt e7dc1f7968 fix(web): empty album stored (#9771)
fix(web): delete album when created empty
2024-06-02 16:08:48 -05:00
505 changed files with 35213 additions and 4825 deletions
+1
View File
@@ -4,6 +4,7 @@
design/ design/
docker/ docker/
!docker/scripts
docs/ docs/
e2e/ e2e/
fastlane/ fastlane/
+23
View File
@@ -0,0 +1,23 @@
cli:
- changed-files:
- any-glob-to-any-file: cli/**
documentation:
- changed-files:
- any-glob-to-any-file: docs/**
🖥️web:
- changed-files:
- any-glob-to-any-file: web/**
📱mobile:
- changed-files:
- any-glob-to-any-file: mobile/**
🗄️server:
- changed-files:
- any-glob-to-any-file: server/**
🧠machine-learning:
- changed-files:
- any-glob-to-any-file: machine-learning/**
+13 -12
View File
@@ -1,16 +1,17 @@
name: CLI Build name: CLI Build
on: on:
workflow_dispatch:
push: push:
branches: [main] branches: [main]
paths: paths:
- "cli/**" - 'cli/**'
- ".github/workflows/cli.yml" - '.github/workflows/cli.yml'
pull_request: pull_request:
branches: [main] branches: [main]
paths: paths:
- "cli/**" - 'cli/**'
- ".github/workflows/cli.yml" - '.github/workflows/cli.yml'
release:
types: [published]
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
@@ -32,8 +33,8 @@ jobs:
# Setup .npmrc file to publish to npm # Setup .npmrc file to publish to npm
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: "20.x" node-version: '20.x'
registry-url: "https://registry.npmjs.org" registry-url: 'https://registry.npmjs.org'
- name: Prepare SDK - name: Prepare SDK
run: npm ci --prefix ../open-api/typescript-sdk/ run: npm ci --prefix ../open-api/typescript-sdk/
- name: Build SDK - name: Build SDK
@@ -41,7 +42,7 @@ jobs:
- run: npm ci - run: npm ci
- run: npm run build - run: npm run build
- run: npm publish - run: npm publish
if: ${{ github.event_name == 'workflow_dispatch' }} if: ${{ github.event_name == 'release' }}
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -83,15 +84,15 @@ jobs:
images: | images: |
name=ghcr.io/${{ github.repository_owner }}/immich-cli name=ghcr.io/${{ github.repository_owner }}/immich-cli
tags: | tags: |
type=raw,value=${{ steps.package-version.outputs.version }},enable=${{ github.event_name == 'workflow_dispatch' }} type=raw,value=${{ steps.package-version.outputs.version }},enable=${{ github.event_name == 'release' }}
type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' }} type=raw,value=latest,enable=${{ github.event_name == 'release' }}
- name: Build and push image - name: Build and push image
uses: docker/build-push-action@v5.3.0 uses: docker/build-push-action@v5.4.0
with: with:
file: cli/Dockerfile file: cli/Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: ${{ github.event_name == 'workflow_dispatch' }} push: ${{ github.event_name == 'release' }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
tags: ${{ steps.metadata.outputs.tags }} tags: ${{ steps.metadata.outputs.tags }}
+2 -2
View File
@@ -35,7 +35,7 @@ jobs:
steps: steps:
- name: Clean temporary images - name: Clean temporary images
if: "${{ env.TOKEN != '' }}" if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@v0.6.0 uses: stumpylog/image-cleaner-action/ephemeral@v0.7.0
with: with:
token: "${{ env.TOKEN }}" token: "${{ env.TOKEN }}"
owner: "immich-app" owner: "immich-app"
@@ -64,7 +64,7 @@ jobs:
steps: steps:
- name: Clean untagged images - name: Clean untagged images
if: "${{ env.TOKEN != '' }}" if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@v0.6.0 uses: stumpylog/image-cleaner-action/untagged@v0.7.0
with: with:
token: "${{ env.TOKEN }}" token: "${{ env.TOKEN }}"
owner: "immich-app" owner: "immich-app"
+1 -1
View File
@@ -115,7 +115,7 @@ jobs:
fi fi
- name: Build and push image - name: Build and push image
uses: docker/build-push-action@v5.3.0 uses: docker/build-push-action@v5.4.0
with: with:
context: ${{ matrix.context }} context: ${{ matrix.context }}
file: ${{ matrix.file }} file: ${{ matrix.file }}
+12
View File
@@ -0,0 +1,12 @@
name: "Pull Request Labeler"
on:
- pull_request_target
jobs:
labeler:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5
-13
View File
@@ -1,13 +0,0 @@
name: Enforce PR labels
on:
pull_request:
types: [labeled, unlabeled, opened, edited, synchronize]
jobs:
enforce-label:
name: Enforce label
runs-on: ubuntu-latest
steps:
- if: toJson(github.event.pull_request.labels) == '[]'
run: exit 1
-6
View File
@@ -10,12 +10,6 @@ dev-update:
dev-scale: dev-scale:
docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
stage:
docker compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans
pull-stage:
docker compose -f ./docker/docker-compose.staging.yml pull
.PHONY: e2e .PHONY: e2e
e2e: e2e:
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
+1
View File
@@ -31,6 +31,7 @@
<a href="readme_i18n/README_zh_CN.md">中文</a> <a href="readme_i18n/README_zh_CN.md">中文</a>
<a href="readme_i18n/README_ru_RU.md">Русский</a> <a href="readme_i18n/README_ru_RU.md">Русский</a>
<a href="readme_i18n/README_pt_BR.md">Português Brasileiro</a> <a href="readme_i18n/README_pt_BR.md">Português Brasileiro</a>
<a href="readme_i18n/README_sv_SE.md">Svenska</a>
<a href="readme_i18n/README_ar_JO.md">العربية</a> <a href="readme_i18n/README_ar_JO.md">العربية</a>
</p> </p>
+1 -1
View File
@@ -1 +1 @@
20.13 20.14
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:20-alpine3.19@sha256:291e84d956f1aff38454bbd3da38941461ad569a185c20aa289f71f37ea08e23 as core FROM node:20-alpine3.19@sha256:696ae41fb5880949a15ade7879a2deae93b3f0723f757bdb5b8a9e4a744ce27f as core
WORKDIR /usr/src/open-api/typescript-sdk WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
+52 -51
View File
@@ -1,12 +1,12 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.0", "version": "2.2.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.0", "version": "2.2.3",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
@@ -47,7 +47,7 @@
}, },
"../open-api/typescript-sdk": { "../open-api/typescript-sdk": {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.105.1", "version": "1.106.3",
"dev": true, "dev": true,
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
@@ -1138,9 +1138,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.12.12", "version": "20.12.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.13.tgz",
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", "integrity": "sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1154,17 +1154,17 @@
"dev": true "dev": true
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.10.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.10.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz",
"integrity": "sha512-PzCr+a/KAef5ZawX7nbyNwBDtM1HdLIT53aSA2DDlxmxMngZ43O8SIePOeX8H5S+FHXeI6t97mTt/dDdzY4Fyw==", "integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.10.0", "@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/type-utils": "7.10.0", "@typescript-eslint/type-utils": "7.11.0",
"@typescript-eslint/utils": "7.10.0", "@typescript-eslint/utils": "7.11.0",
"@typescript-eslint/visitor-keys": "7.10.0", "@typescript-eslint/visitor-keys": "7.11.0",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^5.3.1", "ignore": "^5.3.1",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@@ -1188,16 +1188,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "7.10.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.10.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz",
"integrity": "sha512-2EjZMA0LUW5V5tGQiaa2Gys+nKdfrn2xiTIBLR4fxmPmVSvgPcKNW+AE/ln9k0A4zDUti0J/GZXMDupQoI+e1w==", "integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "7.10.0", "@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/types": "7.10.0", "@typescript-eslint/types": "7.11.0",
"@typescript-eslint/typescript-estree": "7.10.0", "@typescript-eslint/typescript-estree": "7.11.0",
"@typescript-eslint/visitor-keys": "7.10.0", "@typescript-eslint/visitor-keys": "7.11.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -1217,14 +1217,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "7.10.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.10.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz",
"integrity": "sha512-7L01/K8W/VGl7noe2mgH0K7BE29Sq6KAbVmxurj8GGaPDZXPr8EEQ2seOeAS+mEV9DnzxBQB6ax6qQQ5C6P4xg==", "integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.10.0", "@typescript-eslint/types": "7.11.0",
"@typescript-eslint/visitor-keys": "7.10.0" "@typescript-eslint/visitor-keys": "7.11.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@@ -1235,14 +1235,14 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "7.10.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.10.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz",
"integrity": "sha512-D7tS4WDkJWrVkuzgm90qYw9RdgBcrWmbbRkrLA4d7Pg3w0ttVGDsvYGV19SH8gPR5L7OtcN5J1hTtyenO9xE9g==", "integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "7.10.0", "@typescript-eslint/typescript-estree": "7.11.0",
"@typescript-eslint/utils": "7.10.0", "@typescript-eslint/utils": "7.11.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^1.3.0" "ts-api-utils": "^1.3.0"
}, },
@@ -1263,9 +1263,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "7.10.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.10.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz",
"integrity": "sha512-7fNj+Ya35aNyhuqrA1E/VayQX9Elwr8NKZ4WueClR3KwJ7Xx9jcCdOrLW04h51de/+gNbyFMs+IDxh5xIwfbNg==", "integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -1277,14 +1277,14 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "7.10.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.10.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz",
"integrity": "sha512-LXFnQJjL9XIcxeVfqmNj60YhatpRLt6UhdlFwAkjNc6jSUlK8zQOl1oktAP8PlWFzPQC1jny/8Bai3/HPuvN5g==", "integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.10.0", "@typescript-eslint/types": "7.11.0",
"@typescript-eslint/visitor-keys": "7.10.0", "@typescript-eslint/visitor-keys": "7.11.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -1306,16 +1306,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "7.10.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.10.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz",
"integrity": "sha512-olzif1Fuo8R8m/qKkzJqT7qwy16CzPRWBvERS0uvyc+DHd8AKbO4Jb7kpAvVzMmZm8TrHnI7hvjN4I05zow+tg==", "integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.10.0", "@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/types": "7.10.0", "@typescript-eslint/types": "7.11.0",
"@typescript-eslint/typescript-estree": "7.10.0" "@typescript-eslint/typescript-estree": "7.11.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@@ -1329,13 +1329,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "7.10.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.10.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz",
"integrity": "sha512-9ntIVgsi6gg6FIq9xjEO4VQJvwOqA3jaBFQJ/6TK5AvEup2+cECI6Fh7QiBxmfMHXU0V0J4RyPeOU1VDNzl9cg==", "integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.10.0", "@typescript-eslint/types": "7.11.0",
"eslint-visitor-keys": "^3.4.3" "eslint-visitor-keys": "^3.4.3"
}, },
"engines": { "engines": {
@@ -4281,10 +4281,11 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.2.11", "version": "5.2.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz",
"integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", "integrity": "sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.20.1", "esbuild": "^0.20.1",
"postcss": "^8.4.38", "postcss": "^8.4.38",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.0", "version": "2.2.3",
"description": "Command Line Interface (CLI) for Immich", "description": "Command Line Interface (CLI) for Immich",
"type": "module", "type": "module",
"exports": "./dist/index.js", "exports": "./dist/index.js",
@@ -62,6 +62,6 @@
"lodash-es": "^4.17.21" "lodash-es": "^4.17.21"
}, },
"volta": { "volta": {
"node": "20.13.1" "node": "20.14.0"
} }
} }
+30 -17
View File
@@ -2,24 +2,37 @@
# Manual edits may be lost in future updates. # Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" { provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.33.0" version = "4.34.0"
constraints = "4.33.0" constraints = "4.34.0"
hashes = [ hashes = [
"h1:jfvnxO1kjxUquizrBlswsytWQeHXjvNz6IZwPTuixJ4=", "h1:+W0+Xe1AUh7yvHjDbgR9T7CY1UbBC3Y6U7Eo+ucLnJM=",
"zh:1839d03c8c30d3eac4f18c78c5c095d44348eb06cc9d758136d16517094b64e3", "h1:2+1lKObDDdFZRluvROF3RKtXD66CFT3PfnHOvR6CmfA=",
"zh:1fd94992cbb8ce2943cf4fe3deff01b9b1a0e913d2cdd484ceb96a859d46dc5d", "h1:7vluN2wmw8D9nI11YwTgoGv3hGDXlkt8xqQ4L/JABeQ=",
"zh:2b4f37c16a7f6d3712b03980b52b149d4ef6a544917bfe1d1c2ca2d40468daa5", "h1:B0Urm8ZKTJ8cXzSCtEpJ+o+LsD8MXaD6LU59qVbh50Q=",
"zh:32ecb8017be0a34f72b9b0fcd43d944b99cdc903c79892a68b48719824fb194c", "h1:FpGLCm5oF12FaRti3E4iQJlkVbdCC7toyGVuH8og7KY=",
"zh:6468dee137fa07b0b43f742cfdaabb6620883b00773af370e10755ba579eb7f7", "h1:FunTmrCMDy+rom7YskY0WiL5/Y164zFrrD9xnBxU5NY=",
"zh:766504de95a418fd763d9474f39fb147053201d6c4efa3efa456fb39a559b28b", "h1:GrxZhEb+5HzmHF/BvZBdGKBJy6Wyjme0+ABVDz/63to=",
"zh:800342078c0c04a36cb2558d0c5c6bf050a8b4c231abecac59e56c9868b9fa7e", "h1:J36dda2K42/oTfHuZ4jKkW5+nI6BTWFRUvo60P17NJg=",
"h1:Kq0Wyn+j6zoQeghMYixbnfnyP9ZSIEJbOCzMbaCiAQQ=",
"h1:TKxunXCiS/z105sN/kBNFwU6tIKD67JKJ3ZKjwzoCuI=",
"h1:TR0URKFQxsRO5/v7bKm5hkD/CTTjsG7aVGllL/Mf25c=",
"h1:V+3Qs0Reb6r+8p4XjE5ZFDWYrOIN0x5SwORz4wvHOJ4=",
"h1:mZB3Ui7V/lPQMQK53eBOjIHcrul74252dT06Kgn3J+s=",
"h1:wJwZrIXxoki8omXLJ7XA7B1KaSrtcLMJp090fRtFRAc=",
"zh:02aa46743c1585ada8faa7db23af68ea614053a506f88f05d1090ff5e0e68076",
"zh:1e1a545e83e6457a0e15357b23139bc288fb4fbd5e9a5ddfedc95a6a0216b08c",
"zh:29eef2621e0b1501f620e615bf73b1b90d5417d745e38af63634bc03250faf87",
"zh:3c20989d7e1e141882e6091384bf85fdc83f70f3d29e3e047c493a07de992095",
"zh:3d39619379ba29c7ffb15196f0ea72a04c84cfcdf4b39ac42ac4cf4c19f3eae2",
"zh:805f4a2774e9279c590b8214aabe6df9dcc22bb995df2530513f2f78c647ce75",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:94420269672dc5bef37e1a8efab656ada5c2d6f162b52cc59ea74cf0cf35d633", "zh:8af716f8655a57aa986861a8a7fa1d724594a284bd77c870eaea4db5f8b9732d",
"zh:972481b86636f00771c2b5f3e408ffd9b66d42942645c8b8e11c4f4bf52285f1", "zh:a3d13c93b4e6ee6004782debaa9a17f990f2fe8ec8ba545c232818bb6064aba9",
"zh:ac52d6698fa8b2a8ab820ffb59381da27684a205f5a78529b56358efab67fe06", "zh:bfa136acf82d3719473c0064446cc16d1b0303d98b06f55f503b7abeebceadb1",
"zh:c924c7ac3a8fc08eff7588627be68fc94958c1aaa65928a9fd73cf1d610a0dbf", "zh:ca6cf9254ae5436f2efbc01a0e3f7e4aa3c08b45182037b3eb3eb9539b2f7aec",
"zh:cfdfaeab3fcb522a806fc5b71cbd6096df0fafa06cea2131c0db6074b3b76eed", "zh:cba32d5de02674004e0a5955bd5222016d9991ca0553d4bd3bea517cd9def6ab",
"zh:d554393736b99bd1f0b60e210e276531bcd8df79f435924879eeecc1a2100a0d", "zh:d22c8cd527c6d0e84567f57be5911792e2fcd5969e3bba3747489f18bb16705b",
"zh:df827b0e00c9e2d666cfe6409f61446908e5983a07ae32c822ef193f6b56c37c", "zh:e4eeede9b3e72cdadd6cc252d4cbcf41baee6ecfd12bacd927e2dcbe733ab210",
"zh:facdaa787a69f86203cd3cc6922baea0b4a18bd9c36b0a8162e2e88ef6c90655",
] ]
} }
@@ -5,7 +5,7 @@ terraform {
required_providers { required_providers {
cloudflare = { cloudflare = {
source = "cloudflare/cloudflare" source = "cloudflare/cloudflare"
version = "4.33.0" version = "4.34.0"
} }
} }
} }
+30 -17
View File
@@ -2,24 +2,37 @@
# Manual edits may be lost in future updates. # Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" { provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.33.0" version = "4.34.0"
constraints = "4.33.0" constraints = "4.34.0"
hashes = [ hashes = [
"h1:jfvnxO1kjxUquizrBlswsytWQeHXjvNz6IZwPTuixJ4=", "h1:+W0+Xe1AUh7yvHjDbgR9T7CY1UbBC3Y6U7Eo+ucLnJM=",
"zh:1839d03c8c30d3eac4f18c78c5c095d44348eb06cc9d758136d16517094b64e3", "h1:2+1lKObDDdFZRluvROF3RKtXD66CFT3PfnHOvR6CmfA=",
"zh:1fd94992cbb8ce2943cf4fe3deff01b9b1a0e913d2cdd484ceb96a859d46dc5d", "h1:7vluN2wmw8D9nI11YwTgoGv3hGDXlkt8xqQ4L/JABeQ=",
"zh:2b4f37c16a7f6d3712b03980b52b149d4ef6a544917bfe1d1c2ca2d40468daa5", "h1:B0Urm8ZKTJ8cXzSCtEpJ+o+LsD8MXaD6LU59qVbh50Q=",
"zh:32ecb8017be0a34f72b9b0fcd43d944b99cdc903c79892a68b48719824fb194c", "h1:FpGLCm5oF12FaRti3E4iQJlkVbdCC7toyGVuH8og7KY=",
"zh:6468dee137fa07b0b43f742cfdaabb6620883b00773af370e10755ba579eb7f7", "h1:FunTmrCMDy+rom7YskY0WiL5/Y164zFrrD9xnBxU5NY=",
"zh:766504de95a418fd763d9474f39fb147053201d6c4efa3efa456fb39a559b28b", "h1:GrxZhEb+5HzmHF/BvZBdGKBJy6Wyjme0+ABVDz/63to=",
"zh:800342078c0c04a36cb2558d0c5c6bf050a8b4c231abecac59e56c9868b9fa7e", "h1:J36dda2K42/oTfHuZ4jKkW5+nI6BTWFRUvo60P17NJg=",
"h1:Kq0Wyn+j6zoQeghMYixbnfnyP9ZSIEJbOCzMbaCiAQQ=",
"h1:TKxunXCiS/z105sN/kBNFwU6tIKD67JKJ3ZKjwzoCuI=",
"h1:TR0URKFQxsRO5/v7bKm5hkD/CTTjsG7aVGllL/Mf25c=",
"h1:V+3Qs0Reb6r+8p4XjE5ZFDWYrOIN0x5SwORz4wvHOJ4=",
"h1:mZB3Ui7V/lPQMQK53eBOjIHcrul74252dT06Kgn3J+s=",
"h1:wJwZrIXxoki8omXLJ7XA7B1KaSrtcLMJp090fRtFRAc=",
"zh:02aa46743c1585ada8faa7db23af68ea614053a506f88f05d1090ff5e0e68076",
"zh:1e1a545e83e6457a0e15357b23139bc288fb4fbd5e9a5ddfedc95a6a0216b08c",
"zh:29eef2621e0b1501f620e615bf73b1b90d5417d745e38af63634bc03250faf87",
"zh:3c20989d7e1e141882e6091384bf85fdc83f70f3d29e3e047c493a07de992095",
"zh:3d39619379ba29c7ffb15196f0ea72a04c84cfcdf4b39ac42ac4cf4c19f3eae2",
"zh:805f4a2774e9279c590b8214aabe6df9dcc22bb995df2530513f2f78c647ce75",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:94420269672dc5bef37e1a8efab656ada5c2d6f162b52cc59ea74cf0cf35d633", "zh:8af716f8655a57aa986861a8a7fa1d724594a284bd77c870eaea4db5f8b9732d",
"zh:972481b86636f00771c2b5f3e408ffd9b66d42942645c8b8e11c4f4bf52285f1", "zh:a3d13c93b4e6ee6004782debaa9a17f990f2fe8ec8ba545c232818bb6064aba9",
"zh:ac52d6698fa8b2a8ab820ffb59381da27684a205f5a78529b56358efab67fe06", "zh:bfa136acf82d3719473c0064446cc16d1b0303d98b06f55f503b7abeebceadb1",
"zh:c924c7ac3a8fc08eff7588627be68fc94958c1aaa65928a9fd73cf1d610a0dbf", "zh:ca6cf9254ae5436f2efbc01a0e3f7e4aa3c08b45182037b3eb3eb9539b2f7aec",
"zh:cfdfaeab3fcb522a806fc5b71cbd6096df0fafa06cea2131c0db6074b3b76eed", "zh:cba32d5de02674004e0a5955bd5222016d9991ca0553d4bd3bea517cd9def6ab",
"zh:d554393736b99bd1f0b60e210e276531bcd8df79f435924879eeecc1a2100a0d", "zh:d22c8cd527c6d0e84567f57be5911792e2fcd5969e3bba3747489f18bb16705b",
"zh:df827b0e00c9e2d666cfe6409f61446908e5983a07ae32c822ef193f6b56c37c", "zh:e4eeede9b3e72cdadd6cc252d4cbcf41baee6ecfd12bacd927e2dcbe733ab210",
"zh:facdaa787a69f86203cd3cc6922baea0b4a18bd9c36b0a8162e2e88ef6c90655",
] ]
} }
+1 -1
View File
@@ -5,7 +5,7 @@ terraform {
required_providers { required_providers {
cloudflare = { cloudflare = {
source = "cloudflare/cloudflare" source = "cloudflare/cloudflare"
version = "4.33.0" version = "4.34.0"
} }
} }
} }
+2 -2
View File
@@ -84,7 +84,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93 image: redis:6.2-alpine@sha256:d6c2911ac51b289db208767581a5d154544f2b2fe4914ea5056443f62dc6e900
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
@@ -103,7 +103,7 @@ services:
ports: ports:
- 5432:5432 - 5432:5432
healthcheck: healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1 test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --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 interval: 5m
start_interval: 30s start_interval: 30s
start_period: 5m start_period: 5m
+3 -3
View File
@@ -41,7 +41,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93 image: redis:6.2-alpine@sha256:d6c2911ac51b289db208767581a5d154544f2b2fe4914ea5056443f62dc6e900
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
restart: always restart: always
@@ -61,7 +61,7 @@ services:
ports: ports:
- 5432:5432 - 5432:5432
healthcheck: healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1 test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --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 interval: 5m
start_interval: 30s start_interval: 30s
start_period: 5m start_period: 5m
@@ -85,7 +85,7 @@ services:
command: ['./run.sh', '-disable-reporting'] command: ['./run.sh', '-disable-reporting']
ports: ports:
- 3000:3000 - 3000:3000
image: grafana/grafana:11.0.0-ubuntu@sha256:02e99d1ee0b52dc9d3000c7b5314e7a07e0dfd69cc49bb3f8ce323491ed3406b image: grafana/grafana:11.0.0-ubuntu@sha256:dcd3ae78713958a862732c3608d32c03f0c279c35a2032d74b80b12c5cdc47b8
volumes: volumes:
- grafana-data:/var/lib/grafana - grafana-data:/var/lib/grafana
+2 -2
View File
@@ -43,7 +43,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: docker.io/redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93 image: docker.io/redis:6.2-alpine@sha256:d6c2911ac51b289db208767581a5d154544f2b2fe4914ea5056443f62dc6e900
healthcheck: healthcheck:
test: redis-cli ping || exit 1 test: redis-cli ping || exit 1
restart: always restart: always
@@ -59,7 +59,7 @@ services:
volumes: volumes:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data - ${DB_DATA_LOCATION}:/var/lib/postgresql/data
healthcheck: healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1 test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --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 interval: 5m
start_interval: 30s start_interval: 30s
start_period: 5m start_period: 5m
+3 -3
View File
@@ -3,10 +3,10 @@ global:
evaluation_interval: 15s evaluation_interval: 15s
scrape_configs: scrape_configs:
- job_name: immich_server - job_name: immich_api
static_configs: static_configs:
- targets: ['immich-server:8081'] - targets: ['immich-server:8081']
- job_name: immich_microservices - job_name: immich_microservices
static_configs: static_configs:
- targets: ['immich-microservices:8081'] - targets: ['immich-server:8082']
+49
View File
@@ -0,0 +1,49 @@
#!/bin/sh
set -eu
LOG_LEVEL="${IMMICH_LOG_LEVEL:='info'}"
logDebug() {
if [ "$LOG_LEVEL" = "debug" ] || [ "$LOG_LEVEL" = "verbose" ]; then
echo "DEBUG: $1" >&2
fi
}
if [ -f /sys/fs/cgroup/cgroup.controllers ]; then
logDebug "cgroup v2 detected."
if [ -f /sys/fs/cgroup/cpu.max ]; then
read -r quota period </sys/fs/cgroup/cpu.max
if [ "$quota" = "max" ]; then
logDebug "No CPU limits set."
unset quota period
fi
else
logDebug "/sys/fs/cgroup/cpu.max not found."
fi
else
logDebug "cgroup v1 detected."
if [ -f /sys/fs/cgroup/cpu/cpu.cfs_quota_us ] && [ -f /sys/fs/cgroup/cpu/cpu.cfs_period_us ]; then
quota=$(cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us)
period=$(cat /sys/fs/cgroup/cpu/cpu.cfs_period_us)
if [ "$quota" = "-1" ]; then
logDebug "No CPU limits set."
unset quota period
fi
else
logDebug "/sys/fs/cgroup/cpu/cpu.cfs_quota_us or /sys/fs/cgroup/cpu/cpu.cfs_period_us not found."
fi
fi
if [ -n "${quota:-}" ] && [ -n "${period:-}" ]; then
cpus=$((quota / period))
if [ "$cpus" -eq 0 ]; then
cpus=1
fi
else
cpus=$(grep -c ^processor /proc/cpuinfo)
fi
echo "$cpus"
+1 -1
View File
@@ -1 +1 @@
20.13 20.14
@@ -0,0 +1,23 @@
# Email Notifications
Immich supports the option to send notifications via Email for the following events:
- Creating a new user
- Notifying a user when they get added to a shared album
- Informing other users about the addition of new assets to a shared album
## SMTP settings
You can access the settings panel from the web at `Administration -> Settings -> Notification settings`
Under Email, enter the following details to connect with SMTP servers.
You can use the following [guide](/docs/guides/smtp-gmail) to use Gmail's SMTP server.
<img src={require('./img/email-settings.png').default} width="80%" title="SMTP settings" />
## User's notifications settings
Users can manage their email notification settings from their account settings page on the web. They can choose to turn email notifications on or off for the following events:
<img src={require('./img/user-notifications-settings.png').default} width="80%" title="User notification settings" />
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

+25 -3
View File
@@ -117,7 +117,27 @@ Immich has a route (`/api/oauth/mobile-redirect`) that is already configured to
Here's an example of OAuth configured for Authentik: Here's an example of OAuth configured for Authentik:
<img src={require('./img/oauth-settings.png').default} title="OAuth settings" /> Configuration of Authorised redirect URIs (Authentik OAuth2/OpenID Provider)
<img src={require('./img/authentik-redirect-uris-example.webp').default} width='70%' title="Authentik authorised redirect URIs" />
Configuration of OAuth in Immich System Settings
| Setting | Value |
| ---------------------------- | ---------------------------------------------------------------------------------- |
| Issuer URL | `https://example.immich.app/application/o/immich/.well-known/openid-configuration` |
| Client ID | AFCj2rM1f4rps**\*\*\*\***\***\*\*\*\***lCLEum6hH9... |
| Client Secret | 0v89FXkQOWO\***\*\*\*\*\***\*\*\***\*\*\*\*\***mprbvXD549HH6s1iw... |
| Scope | openid email profile |
| Signing Algorithm | RS256 |
| Storage Label Claim | preferred_username |
| Storage Quota Claim | immich_quota |
| Default Storage Quota (GiB) | 0 (0 for unlimited quota) |
| Button Text | Sign in with Authentik (optional) |
| Auto Register | Enabled (optional) |
| Auto Launch | Enabled (optional) |
| Mobile Redirect URI Override | Disable |
| Mobile Redirect URI | |
</details> </details>
@@ -126,11 +146,13 @@ Here's an example of OAuth configured for Authentik:
### Google Example ### Google Example
Here's an example of OAuth configured for Google:
Configuration of Authorised redirect URIs (Google Console) Configuration of Authorised redirect URIs (Google Console)
<img src={require('./img/google-example.webp').default} width='50%' title="Authorised redirect URIs" /> <img src={require('./img/google-redirect-uris-example.webp').default} width='50%' title="Google authorised redirect URIs" />
Configuration of OAuth in System Settings Configuration of OAuth in Immich System Settings
| Setting | Value | | Setting | Value |
| ---------------------------- | ------------------------------------------------------------------------------------------------------ | | ---------------------------- | ------------------------------------------------------------------------------------------------------ |
+60 -31
View File
@@ -10,6 +10,59 @@ Viewing and modifying the system settings is restricted to the Administrator.
You can always return to the default settings by clicking the `Reset to default` button. You can always return to the default settings by clicking the `Reset to default` button.
::: :::
## Authentication Settings
Manage password, OAuth, and other authentication settings
### OAuth Authentication
Immich supports OAuth Authentication. Read more about this feature and its configuration [here](/docs/administration/oauth).
### Password Authentication
The administrator can choose to disable login with username and password for the entire instance. This means that **no one**, including the system administrator, will be able to log using this method. If [OAuth Authentication](/docs/administration/oauth) is also disabled, no users will be able to login using **any** method. Changing this setting does not affect existing sessions, just new login attempts.
:::tip
You can always use the [Server CLI](/docs/administration/server-commands) to re-enable password login.
:::
## Image Settings (thumbnails and previews)
- Thumbnails - Used in the main timeline.
- Previews - Used in the asset viewer.
By default Immich creates 3 thumbnails for each asset,
Blurred (thumbhash) , Small - thumbnails (webp) , and Large - previews (jpeg/webp), using these settings you can change the quality for the thumbnails and previews files that are created.
**Thumbnail format**
Allows you to choose the type of format you want for the Thumbnail images, Webp produces smaller files than jpeg, but is slower to encode.
:::tip
You can read in detail about the advantages and disadvantages of using webp over jpeg on [Adobe's website](https://www.adobe.com/creativecloud/file-types/image/raster/webp-file.html)
:::
**Thumbnail resolution**
Used when viewing groups of photos (main timeline, album view, etc.). Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness.
**Preview format**
Allows you to choose the type of format you want for the Preview images, Webp produces smaller files than jpeg, but is slower to encode.
**Preview resolution**
Used when viewing a single photo and for machine learning. Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness.
**Quality**
Image quality from 1-100. Higher is better for quality but produces larger files, this option affects the Preview and Thumbnail images.
**Prefer wide gamut**
Use Display P3 for thumbnails. This better preserves the vibrance of images with wide colorspaces, but images may appear differently on old devices with an old browser version. sRGB images are kept as sRGB to avoid color shifts.
**Prefer embedded preview**
Use embedded previews in RAW photos as the input to image processing when available. This can produce more accurate colors for some images, but the quality of the preview is camera-dependent and the image may have more compression artifacts.
:::tip
The default resolution for Large thumbnails can be lowered from 1440p (default) to 1080p or 720p to save storage space.
:::
## Job Settings ## Job Settings
Using these settings, you can determine the amount of work that will run concurrently for each task in microservices. Some tasks can be set to higher values on computers with powerful hardware and storage with good I/O capabilities. Using these settings, you can determine the amount of work that will run concurrently for each task in microservices. Some tasks can be set to higher values on computers with powerful hardware and storage with good I/O capabilities.
@@ -19,6 +72,11 @@ this advice improves throughput, not latency, for example, it will make Smart Se
It is important to remember that jobs like Smart Search, Face Detection, Facial Recognition, and Transcode Videos require a **lot** of processing power and therefore do not exaggerate the amount of jobs because you're probably thoroughly overloading the server. It is important to remember that jobs like Smart Search, Face Detection, Facial Recognition, and Transcode Videos require a **lot** of processing power and therefore do not exaggerate the amount of jobs because you're probably thoroughly overloading the server.
:::danger IMPORTANT
If you increase the concurrency from the defaults we set, especially for thumbnail generation, make sure you do not increase them past the amount of CPU cores you have available.
Doing so can impact API responsiveness with no gain in thumbnail generation speed.
:::
:::info Facial Recognition Concurrency :::info Facial Recognition Concurrency
The Facial Recognition Concurrency value cannot be changed because The Facial Recognition Concurrency value cannot be changed because
[DBSCAN](https://www.youtube.com/watch?v=RDZUdRSDOok) is traditionally sequential, but there are parallel implementations of it out there. Our implementation isn't parallel. [DBSCAN](https://www.youtube.com/watch?v=RDZUdRSDOok) is traditionally sequential, but there are parallel implementations of it out there. Our implementation isn't parallel.
@@ -87,17 +145,9 @@ The map can be adjusted via [OpenMapTiles](https://openmaptiles.org/styles/) for
Immich supports [Reverse Geocoding](/docs/features/reverse-geocoding) using data from the [GeoNames](https://www.geonames.org/) geographical database. Immich supports [Reverse Geocoding](/docs/features/reverse-geocoding) using data from the [GeoNames](https://www.geonames.org/) geographical database.
## OAuth Authentication ## Notification Settings
Immich supports OAuth Authentication. Read more about this feature and its configuration [here](/docs/administration/oauth). SMTP server setup, for user creation notifications, new albums, etc. More information can be found [here](/docs/administration/email-notification)
## Password Authentication
The administrator can choose to disable login with username and password for the entire instance. This means that **no one**, including the system administrator, will be able to log using this method. If [OAuth Authentication](/docs/administration/oauth) is also disabled, no users will be able to login using **any** method. Changing this setting does not affect existing sessions, just new login attempts.
:::tip
You can always use the [Server CLI](/docs/administration/server-commands) to re-enable password login.
:::
## Server Settings ## Server Settings
@@ -125,27 +175,6 @@ p {
} }
``` ```
## Thumbnail Settings
By default Immich creates 3 thumbnails for each asset,
Blurred (thumbhash) , Small (webp) , and Large (jpeg), using these settings you can change the quality for the thumbnail files that are created.
**Small thumbnail resolution**
Used when viewing groups of photos (main timeline, album view, etc.). Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness.
**Large thumbnail resolution**
Used when viewing a single photo and for machine learning. Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness.
**Quality**
Thumbnail quality from 1-100. Higher is better for quality but produces larger files.
**Prefer wide gamut**
Use display p3 for thumbnails. This better preserves the vibrance of images with wide color spaces, but images may appear differently on old devices with an old browser version. Srgb images are kept as srgb to avoid color shifts.
:::tip
The default resolution for Large thumbnails can be lowered from 1440p (default) to 1080p or 720p to save storage space.
:::
## Trash Settings ## Trash Settings
In the system administrator's option to set a trash for deleted files, these files will remain in the trash until the deletion date 30 days (default) or as defined by the system administrator. In the system administrator's option to set a trash for deleted files, these files will remain in the trash until the deletion date 30 days (default) or as defined by the system administrator.
@@ -13,6 +13,20 @@ Immich supports multiple users, each with their own library.
<UserCreate /> <UserCreate />
## Send new user email notification
:::note
This option is only available if an SMTP server has been configured in the administrator settings.
:::
Admin can send a welcome email if the Email option is set, you can learn here how to set up the SMTP server in Immich.
<img
src={require('./img/send-user-email-notification.webp').default}
width="40%"
title="Send user email notification"
/>
## Set Storage Quota For User ## Set Storage Quota For User
Admin can specify the storage quota for the user as the instance's admin; once the limit is reached, the user won't be able to upload to the instance anymore. Admin can specify the storage quota for the user as the instance's admin; once the limit is reached, the user won't be able to upload to the instance anymore.
+8 -7
View File
@@ -60,17 +60,17 @@ For RKMPP to work:
#### Basic Setup #### Basic Setup
1. If you do not already have it, download the latest [`hwaccel.transcoding.yml`][hw-file] file and ensure it's in the same folder as the `docker-compose.yml`. 1. If you do not already have it, download the latest [`hwaccel.transcoding.yml`][hw-file] file and ensure it's in the same folder as the `docker-compose.yml`.
2. In the `docker-compose.yml` under `immich-microservices`, uncomment the `extends` section and change `cpu` to the appropriate backend. 2. In the `docker-compose.yml` under `immich-server`, uncomment the `extends` section and change `cpu` to the appropriate backend.
- For VAAPI on WSL2, be sure to use `vaapi-wsl` rather than `vaapi` - For VAAPI on WSL2, be sure to use `vaapi-wsl` rather than `vaapi`
3. Redeploy the `immich-microservices` container with these updated settings. 3. Redeploy the `immich-server` container with these updated settings.
4. In the Admin page under `Video transcoding settings`, change the hardware acceleration setting to the appropriate option and save. 4. In the Admin page under `Video transcoding settings`, change the hardware acceleration setting to the appropriate option and save.
5. (Optional) If using a compatible backend, you may enable hardware decoding for optimal performance. 5. (Optional) If using a compatible backend, you may enable hardware decoding for optimal performance.
#### Single Compose File #### Single Compose File
Some platforms, including Unraid and Portainer, do not support multiple Compose files as of writing. As an alternative, you can "inline" the relevant contents of the [`hwaccel.transcoding.yml`][hw-file] file into the `immich-microservices` service directly. Some platforms, including Unraid and Portainer, do not support multiple Compose files as of writing. As an alternative, you can "inline" the relevant contents of the [`hwaccel.transcoding.yml`][hw-file] file into the `immich-server` service directly.
For example, the `qsv` section in this file is: For example, the `qsv` section in this file is:
@@ -79,21 +79,22 @@ devices:
- /dev/dri:/dev/dri - /dev/dri:/dev/dri
``` ```
You can add this to the `immich-microservices` service instead of extending from `hwaccel.transcoding.yml`: You can add this to the `immich-server` service instead of extending from `hwaccel.transcoding.yml`:
```yaml ```yaml
immich-microservices: immich-server:
container_name: immich_microservices container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release} image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
# Note the lack of an `extends` section # Note the lack of an `extends` section
devices: devices:
- /dev/dri:/dev/dri - /dev/dri:/dev/dri
command: ['start.sh', 'microservices']
volumes: volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload - ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
env_file: env_file:
- .env - .env
ports:
- 2283:3001
depends_on: depends_on:
- redis - redis
- database - database
Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

+20
View File
@@ -0,0 +1,20 @@
# SMTP settings using Gmail
This guide walks you through how to get the information you need to set up your Immich instance to send emails using Gmail's SMTP server.
## Create an app password
From your Google account settings
- Add [2-Step Verification](https://support.google.com/accounts/answer/185839) to your Google account (Required)
- [Create an app password](https://myaccount.google.com/apppasswords).
At the end of creating your app passwords, a password will be displayed; save it, it will be used for the password field when setting up the SMTP server in Immich.
<img src={require('./img/google-app-password.webp').default} title="Authorised redirect URIs" />
## Entering the SMTP credential in Immich
Entering your credential in Immich's email notification settings at `Administration -> Settings -> Notification Settings`
<img src={require('./img/email-settings.png').default} width="80%" title="SMTP settings" />
+13 -10
View File
@@ -38,16 +38,19 @@ Regardless of filesystem, it is not recommended to use a network share for your
## General ## General
| Variable | Description | Default | Containers | Workers | | Variable | Description | Default | Containers | Workers |
| :------------------------------ | :---------------------------------------------- | :----------------------: | :----------------------- | :----------------- | | :---------------------------------- | :---------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
| `TZ` | Timezone | | server | microservices | | `TZ` | Timezone | | server | microservices |
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices | | `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices | | `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server | api, microservices | | `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices | | `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server | api | | `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server | api |
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | server | microservices | | `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | server | microservices |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | | | `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
| `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | |
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
\*1: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`. \*1: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`.
It only need to be set if the Immich deployment method is changing. It only need to be set if the Immich deployment method is changing.
+4
View File
@@ -94,6 +94,10 @@ const config = {
srcDark: 'img/immich-logo-inline-dark.png', srcDark: 'img/immich-logo-inline-dark.png',
}, },
items: [ items: [
{
type: 'custom-versionSwitcher',
position: 'right',
},
{ {
to: '/docs/overview/introduction', to: '/docs/overview/introduction',
position: 'right', position: 'right',
+1 -1
View File
@@ -56,6 +56,6 @@
"node": ">=20" "node": ">=20"
}, },
"volta": { "volta": {
"node": "20.13.1" "node": "20.14.0"
} }
} }
+59
View File
@@ -0,0 +1,59 @@
import '@docusaurus/theme-classic/lib/theme/Unlisted/index';
import { useWindowSize } from '@docusaurus/theme-common';
import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem';
import React, { useEffect, useState } from 'react';
export default function VersionSwitcher(): JSX.Element {
const [versions, setVersions] = useState([]);
const [label, setLabel] = useState('Versions');
const windowSize = useWindowSize();
useEffect(() => {
async function getVersions() {
try {
let baseUrl = 'https://immich.app';
if (window.location.origin === 'http://localhost:3005') {
baseUrl = window.location.origin;
}
const response = await fetch(`${baseUrl}/archived-versions.json`);
const archiveVersions = await response.json();
const allVersions = [
{ label: 'Next', url: 'https://main.preview.immich.app' },
{ label: 'Latest', url: 'https://immich.app' },
...archiveVersions,
];
setVersions(allVersions);
const activeVersion = allVersions.find((version) => new URL(version.url).origin === window.location.origin);
if (activeVersion) {
setLabel(activeVersion.label);
}
} catch (error) {
console.error('Failed to fetch versions', error);
}
}
if (versions.length === 0) {
getVersions();
}
}, []);
return (
versions.length > 0 && (
<DropdownNavbarItem
className="navbar__item"
label={label}
mobile={windowSize === 'mobile'}
items={versions.map(({ label, url }) => ({
label,
to: url,
target: '_self',
}))}
/>
)
);
}
+4
View File
@@ -15,6 +15,10 @@ button {
font-family: 'Overpass', sans-serif; font-family: 'Overpass', sans-serif;
} }
img {
border-radius: 15px;
}
/* You can override the default Infima variables here. */ /* You can override the default Infima variables here. */
:root { :root {
--ifm-color-primary: #4250af; --ifm-color-primary: #4250af;
+15 -7
View File
@@ -63,12 +63,14 @@ import {
mdiVectorCombine, mdiVectorCombine,
mdiVideo, mdiVideo,
mdiWeb, mdiWeb,
mdiContentDuplicate,
} from '@mdi/js'; } from '@mdi/js';
import Layout from '@theme/Layout'; import Layout from '@theme/Layout';
import React from 'react'; import React from 'react';
import { Item, Timeline } from '../components/timeline'; import { Item, Timeline } from '../components/timeline';
const releases = { const releases = {
'v1.106.0': new Date(2024, 5, 11),
'v1.104.0': new Date(2024, 4, 13), 'v1.104.0': new Date(2024, 4, 13),
'v1.103.0': new Date(2024, 3, 29), 'v1.103.0': new Date(2024, 3, 29),
'v1.102.0': new Date(2024, 3, 15), 'v1.102.0': new Date(2024, 3, 15),
@@ -216,13 +218,19 @@ const roadmap: Item[] = [
]; ];
const milestones: Item[] = [ const milestones: Item[] = [
// withRelease({ withRelease({
// icon: mdiVectorCombine, icon: mdiContentDuplicate,
// title: 'Container consolidation', title: 'Similar image detection',
// description: description: 'Detect duplicate assets that arent exactly identical',
// 'The microservices container can be run as a worker within the server image, allowing us to remove it from the default stack.', release: 'v1.106.0',
// release: 'v1.106.0', }),
// }), withRelease({
icon: mdiVectorCombine,
title: 'Container consolidation',
description:
'The microservices container can be run as a worker within the server image, allowing us to remove it from the default stack.',
release: 'v1.106.0',
}),
withRelease({ withRelease({
icon: mdiPencil, icon: mdiPencil,
iconColor: 'saddlebrown', iconColor: 'saddlebrown',
@@ -0,0 +1,7 @@
import ComponentTypes from '@theme-original/NavbarItem/ComponentTypes';
import VersionSwitcher from '@site/src/components/version-switcher';
export default {
...ComponentTypes,
'custom-versionSwitcher': VersionSwitcher,
};
+58
View File
@@ -0,0 +1,58 @@
[
{
"label": "v1.106.3",
"url": "https://v1.106.3.archive.immich.app"
},
{
"label": "v1.106.2",
"url": "https://v1.106.2.archive.immich.app"
},
{
"label": "v1.106.1",
"url": "https://v1.106.1.archive.immich.app"
},
{
"label": "v1.105.1",
"url": "https://v1.105.1.archive.immich.app/"
},
{
"label": "v1.105.0",
"url": "https://v1.105.0.archive.immich.app/"
},
{
"label": "v1.104.0",
"url": "https://v1.104.0.archive.immich.app/"
},
{
"label": "v1.103.1",
"url": "https://v1.103.1.archive.immich.app/"
},
{
"label": "v1.103.0",
"url": "https://v1.103.0.archive.immich.app/"
},
{
"label": "v1.102.3",
"url": "https://v1.102.3.archive.immich.app/"
},
{
"label": "v1.102.2",
"url": "https://v1.102.2.archive.immich.app/"
},
{
"label": "v1.102.1",
"url": "https://v1.102.1.archive.immich.app/"
},
{
"label": "v1.102.0",
"url": "https://v1.102.0.archive.immich.app/"
},
{
"label": "v1.101.0",
"url": "https://v1.101.0.archive.immich.app/"
},
{
"label": "v1.100.0",
"url": "https://v1.100.0.archive.immich.app/"
}
]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 70 KiB

+1 -1
View File
@@ -1 +1 @@
20.13 20.14
+1 -1
View File
@@ -27,7 +27,7 @@ services:
- 2283:3001 - 2283:3001
redis: redis:
image: redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93 image: redis:6.2-alpine@sha256:d6c2911ac51b289db208767581a5d154544f2b2fe4914ea5056443f62dc6e900
database: database:
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
+63 -60
View File
@@ -1,12 +1,12 @@
{ {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.105.1", "version": "1.106.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.105.1", "version": "1.106.3",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"devDependencies": { "devDependencies": {
"@immich/cli": "file:../cli", "@immich/cli": "file:../cli",
@@ -39,7 +39,7 @@
}, },
"../cli": { "../cli": {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.0", "version": "2.2.3",
"dev": true, "dev": true,
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
@@ -81,7 +81,7 @@
}, },
"../open-api/typescript-sdk": { "../open-api/typescript-sdk": {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.105.1", "version": "1.106.3",
"dev": true, "dev": true,
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
@@ -1230,9 +1230,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.12.12", "version": "20.12.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.13.tgz",
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", "integrity": "sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1344,17 +1344,17 @@
} }
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.10.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.10.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz",
"integrity": "sha512-PzCr+a/KAef5ZawX7nbyNwBDtM1HdLIT53aSA2DDlxmxMngZ43O8SIePOeX8H5S+FHXeI6t97mTt/dDdzY4Fyw==", "integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.10.0", "@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/type-utils": "7.10.0", "@typescript-eslint/type-utils": "7.11.0",
"@typescript-eslint/utils": "7.10.0", "@typescript-eslint/utils": "7.11.0",
"@typescript-eslint/visitor-keys": "7.10.0", "@typescript-eslint/visitor-keys": "7.11.0",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^5.3.1", "ignore": "^5.3.1",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@@ -1378,16 +1378,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "7.10.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.10.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz",
"integrity": "sha512-2EjZMA0LUW5V5tGQiaa2Gys+nKdfrn2xiTIBLR4fxmPmVSvgPcKNW+AE/ln9k0A4zDUti0J/GZXMDupQoI+e1w==", "integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "7.10.0", "@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/types": "7.10.0", "@typescript-eslint/types": "7.11.0",
"@typescript-eslint/typescript-estree": "7.10.0", "@typescript-eslint/typescript-estree": "7.11.0",
"@typescript-eslint/visitor-keys": "7.10.0", "@typescript-eslint/visitor-keys": "7.11.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -1407,14 +1407,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "7.10.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.10.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz",
"integrity": "sha512-7L01/K8W/VGl7noe2mgH0K7BE29Sq6KAbVmxurj8GGaPDZXPr8EEQ2seOeAS+mEV9DnzxBQB6ax6qQQ5C6P4xg==", "integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.10.0", "@typescript-eslint/types": "7.11.0",
"@typescript-eslint/visitor-keys": "7.10.0" "@typescript-eslint/visitor-keys": "7.11.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@@ -1425,14 +1425,14 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "7.10.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.10.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz",
"integrity": "sha512-D7tS4WDkJWrVkuzgm90qYw9RdgBcrWmbbRkrLA4d7Pg3w0ttVGDsvYGV19SH8gPR5L7OtcN5J1hTtyenO9xE9g==", "integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "7.10.0", "@typescript-eslint/typescript-estree": "7.11.0",
"@typescript-eslint/utils": "7.10.0", "@typescript-eslint/utils": "7.11.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^1.3.0" "ts-api-utils": "^1.3.0"
}, },
@@ -1453,9 +1453,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "7.10.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.10.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz",
"integrity": "sha512-7fNj+Ya35aNyhuqrA1E/VayQX9Elwr8NKZ4WueClR3KwJ7Xx9jcCdOrLW04h51de/+gNbyFMs+IDxh5xIwfbNg==", "integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -1467,14 +1467,14 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "7.10.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.10.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz",
"integrity": "sha512-LXFnQJjL9XIcxeVfqmNj60YhatpRLt6UhdlFwAkjNc6jSUlK8zQOl1oktAP8PlWFzPQC1jny/8Bai3/HPuvN5g==", "integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.10.0", "@typescript-eslint/types": "7.11.0",
"@typescript-eslint/visitor-keys": "7.10.0", "@typescript-eslint/visitor-keys": "7.11.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -1522,16 +1522,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "7.10.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.10.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz",
"integrity": "sha512-olzif1Fuo8R8m/qKkzJqT7qwy16CzPRWBvERS0uvyc+DHd8AKbO4Jb7kpAvVzMmZm8TrHnI7hvjN4I05zow+tg==", "integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.10.0", "@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/types": "7.10.0", "@typescript-eslint/types": "7.11.0",
"@typescript-eslint/typescript-estree": "7.10.0" "@typescript-eslint/typescript-estree": "7.11.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@@ -1545,13 +1545,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "7.10.0", "version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.10.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz",
"integrity": "sha512-9ntIVgsi6gg6FIq9xjEO4VQJvwOqA3jaBFQJ/6TK5AvEup2+cECI6Fh7QiBxmfMHXU0V0J4RyPeOU1VDNzl9cg==", "integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.10.0", "@typescript-eslint/types": "7.11.0",
"eslint-visitor-keys": "^3.4.3" "eslint-visitor-keys": "^3.4.3"
}, },
"engines": { "engines": {
@@ -2700,10 +2700,11 @@
} }
}, },
"node_modules/exiftool-vendored": { "node_modules/exiftool-vendored": {
"version": "26.0.0", "version": "26.1.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-26.0.0.tgz", "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-26.1.0.tgz",
"integrity": "sha512-2TRxx21ovD95VvdSzHb/sTYYcwhiizQIhhVAbrgua9KoL902QRieREGvaUtfBZNjsptdjonuyku2kUBJCPqsgw==", "integrity": "sha512-Bhy2Ia86Agt3+PbJJhWeVMqJNXl74XJ0Oygef5F5uCL13fTxlmF8dECHiChyx8bBc3sxIw+2Q3ehWunJh3bs6w==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@photostructure/tz-lookup": "^10.0.0", "@photostructure/tz-lookup": "^10.0.0",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
@@ -2712,25 +2713,27 @@
"luxon": "^3.4.4" "luxon": "^3.4.4"
}, },
"optionalDependencies": { "optionalDependencies": {
"exiftool-vendored.exe": "12.84.0", "exiftool-vendored.exe": "12.85.0",
"exiftool-vendored.pl": "12.84.0" "exiftool-vendored.pl": "12.85.0"
} }
}, },
"node_modules/exiftool-vendored.exe": { "node_modules/exiftool-vendored.exe": {
"version": "12.84.0", "version": "12.85.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.84.0.tgz", "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.85.0.tgz",
"integrity": "sha512-9ocqJb0Pr9k0TownEMd75payF/XOQLF/swr/l0Ep49D+m609uIZsW09CtowhXmk1KrIFobS3+SkdXK04CSyUwQ==", "integrity": "sha512-rWsKVp9oXsS79S3bfCNXKeEo4av0xcd7slk/TfPpCa5pojg8ZVXSVfPZMAAlhOuK63YXrKN/e3jRNReeGP+2Gw==",
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
] ]
}, },
"node_modules/exiftool-vendored.pl": { "node_modules/exiftool-vendored.pl": {
"version": "12.84.0", "version": "12.85.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.84.0.tgz", "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.85.0.tgz",
"integrity": "sha512-TxvMRaVYtd24Vupn48zy24LOYItIIWEu4dgt/VlqLwxQItTpvJTV9YH04iZRvaNh9ZdPRgVKWMuuUDBBHv+lAg==", "integrity": "sha512-AelZQCCfl0a0g7PYx90TqbNGlSu2zDbRfCTjGw6bBBYnJF0NUfUWVhTpa8XGe2lHx1KYikH8AkJaey3esAxMAg==",
"dev": true, "dev": true,
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"!win32" "!win32"
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.105.1", "version": "1.106.3",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
@@ -47,6 +47,6 @@
"vitest": "^1.3.0" "vitest": "^1.3.0"
}, },
"volta": { "volta": {
"node": "20.13.1" "node": "20.14.0"
} }
} }
+25
View File
@@ -1148,4 +1148,29 @@ describe('/asset', () => {
expect(video.checksum).toStrictEqual(checksum); expect(video.checksum).toStrictEqual(checksum);
}); });
}); });
describe('POST /assets/exist', () => {
it('ignores invalid deviceAssetIds', async () => {
const response = await utils.checkExistingAssets(user1.accessToken, {
deviceId: 'test-assets-exist',
deviceAssetIds: ['invalid', 'INVALID'],
});
expect(response.existingIds).toHaveLength(0);
});
it('returns the IDs of existing assets', async () => {
await utils.createAsset(user1.accessToken, {
deviceId: 'test-assets-exist',
deviceAssetId: 'test-asset-0',
});
const response = await utils.checkExistingAssets(user1.accessToken, {
deviceId: 'test-assets-exist',
deviceAssetIds: ['test-asset-0'],
});
expect(response.existingIds).toEqual(['test-asset-0']);
});
});
}); });
+58 -6
View File
@@ -1,4 +1,11 @@
import { LibraryResponseDto, LoginResponseDto, ScanLibraryDto, getAllLibraries, scanLibrary } from '@immich/sdk'; import {
LibraryResponseDto,
LoginResponseDto,
ScanLibraryDto,
getAllLibraries,
removeOfflineFiles,
scanLibrary,
} from '@immich/sdk';
import { cpSync, existsSync } from 'node:fs'; import { cpSync, existsSync } from 'node:fs';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { userDto, uuidDto } from 'src/fixtures'; import { userDto, uuidDto } from 'src/fixtures';
@@ -384,6 +391,51 @@ describe('/libraries', () => {
); );
}); });
it('should not try to delete offline files', async () => {
utils.createImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline1`],
});
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(initialAssets).toEqual({
count: 1,
total: 1,
facets: [],
items: [expect.objectContaining({ originalFileName: 'assetA.png' })],
nextPage: null,
});
utils.removeImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: offlineAssets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
isOffline: true,
});
expect(offlineAssets).toEqual({
count: 1,
total: 1,
facets: [],
items: [expect.objectContaining({ originalFileName: 'assetA.png' })],
nextPage: null,
});
utils.createImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
await removeOfflineFiles({ id: library.id }, { headers: asBearerAuth(admin.accessToken) });
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForWebsocketEvent({ event: 'assetDelete', total: 1 });
expect(existsSync(`${testAssetDir}/temp/offline1/assetA.png`)).toBe(true);
});
it('should scan new files', async () => { it('should scan new files', async () => {
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId, ownerId: admin.userId,
@@ -507,10 +559,10 @@ describe('/libraries', () => {
it('should remove offline files', async () => { it('should remove offline files', async () => {
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId, ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`], importPaths: [`${testAssetDirInternal}/temp/offline2`],
}); });
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`); utils.createImageFile(`${testAssetDir}/temp/offline2/assetA.png`);
await scan(admin.accessToken, library.id); await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library'); await utils.waitForQueueFinish(admin.accessToken, 'library');
@@ -518,9 +570,9 @@ describe('/libraries', () => {
const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, { const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id, libraryId: library.id,
}); });
expect(initialAssets.count).toBe(3); expect(initialAssets.count).toBe(1);
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`); utils.removeImageFile(`${testAssetDir}/temp/offline2/assetA.png`);
await scan(admin.accessToken, library.id); await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library'); await utils.waitForQueueFinish(admin.accessToken, 'library');
@@ -541,7 +593,7 @@ describe('/libraries', () => {
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(2); expect(assets.count).toBe(0);
}); });
it('should not remove online files', async () => { it('should not remove online files', async () => {
+10 -2
View File
@@ -250,10 +250,18 @@ describe('/admin/users', () => {
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual({ avatar: { color: 'orange' }, memories: { enabled: false } }); expect(body).toEqual({
avatar: { color: 'orange' },
memories: { enabled: false },
emailNotifications: { enabled: true, albumInvite: true, albumUpdate: true },
});
const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) }); const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toEqual({ avatar: { color: 'orange' }, memories: { enabled: false } }); expect(after).toEqual({
avatar: { color: 'orange' },
memories: { enabled: false },
emailNotifications: { enabled: true, albumInvite: true, albumUpdate: true },
});
}); });
}); });
+10
View File
@@ -5,51 +5,61 @@ export const errorDto = {
error: 'Unauthorized', error: 'Unauthorized',
statusCode: 401, statusCode: 401,
message: 'Authentication required', message: 'Authentication required',
correlationId: expect.any(String),
}, },
forbidden: { forbidden: {
error: 'Forbidden', error: 'Forbidden',
statusCode: 403, statusCode: 403,
message: expect.any(String), message: expect.any(String),
correlationId: expect.any(String),
}, },
wrongPassword: { wrongPassword: {
error: 'Bad Request', error: 'Bad Request',
statusCode: 400, statusCode: 400,
message: 'Wrong password', message: 'Wrong password',
correlationId: expect.any(String),
}, },
invalidToken: { invalidToken: {
error: 'Unauthorized', error: 'Unauthorized',
statusCode: 401, statusCode: 401,
message: 'Invalid user token', message: 'Invalid user token',
correlationId: expect.any(String),
}, },
invalidShareKey: { invalidShareKey: {
error: 'Unauthorized', error: 'Unauthorized',
statusCode: 401, statusCode: 401,
message: 'Invalid share key', message: 'Invalid share key',
correlationId: expect.any(String),
}, },
invalidSharePassword: { invalidSharePassword: {
error: 'Unauthorized', error: 'Unauthorized',
statusCode: 401, statusCode: 401,
message: 'Invalid password', message: 'Invalid password',
correlationId: expect.any(String),
}, },
badRequest: (message: any = null) => ({ badRequest: (message: any = null) => ({
error: 'Bad Request', error: 'Bad Request',
statusCode: 400, statusCode: 400,
message: message ?? expect.anything(), message: message ?? expect.anything(),
correlationId: expect.any(String),
}), }),
noPermission: { noPermission: {
error: 'Bad Request', error: 'Bad Request',
statusCode: 400, statusCode: 400,
message: expect.stringContaining('Not found or no'), message: expect.stringContaining('Not found or no'),
correlationId: expect.any(String),
}, },
incorrectLogin: { incorrectLogin: {
error: 'Unauthorized', error: 'Unauthorized',
statusCode: 401, statusCode: 401,
message: 'Incorrect email or password', message: 'Incorrect email or password',
correlationId: expect.any(String),
}, },
alreadyHasAdmin: { alreadyHasAdmin: {
error: 'Bad Request', error: 'Bad Request',
statusCode: 400, statusCode: 400,
message: 'The server already has an admin', message: 'The server already has an admin',
correlationId: expect.any(String),
}, },
}; };
+39
View File
@@ -3,6 +3,7 @@ import {
AssetMediaCreateDto, AssetMediaCreateDto,
AssetMediaResponseDto, AssetMediaResponseDto,
AssetResponseDto, AssetResponseDto,
CheckExistingAssetsDto,
CreateAlbumDto, CreateAlbumDto,
CreateLibraryDto, CreateLibraryDto,
MetadataSearchDto, MetadataSearchDto,
@@ -10,6 +11,7 @@ import {
SharedLinkCreateDto, SharedLinkCreateDto,
UserAdminCreateDto, UserAdminCreateDto,
ValidateLibraryDto, ValidateLibraryDto,
checkExistingAssets,
createAlbum, createAlbum,
createApiKey, createApiKey,
createLibrary, createLibrary,
@@ -323,6 +325,40 @@ export const utils = {
return body as AssetMediaResponseDto; return body as AssetMediaResponseDto;
}, },
replaceAsset: async (
accessToken: string,
assetId: string,
dto?: Partial<Omit<AssetMediaCreateDto, 'assetData'>> & { assetData?: AssetData },
) => {
const _dto = {
deviceAssetId: 'test-1',
deviceId: 'test',
fileCreatedAt: new Date().toISOString(),
fileModifiedAt: new Date().toISOString(),
...dto,
};
const assetData = dto?.assetData?.bytes || makeRandomImage();
const filename = dto?.assetData?.filename || 'example.png';
if (dto?.assetData?.bytes) {
console.log(`Uploading ${filename}`);
}
const builder = request(app)
.put(`/assets/${assetId}/original`)
.attach('assetData', assetData, filename)
.set('Authorization', `Bearer ${accessToken}`);
for (const [key, value] of Object.entries(_dto)) {
void builder.field(key, String(value));
}
const { body } = await builder;
return body as AssetMediaResponseDto;
},
createImageFile: (path: string) => { createImageFile: (path: string) => {
if (!existsSync(dirname(path))) { if (!existsSync(dirname(path))) {
mkdirSync(dirname(path), { recursive: true }); mkdirSync(dirname(path), { recursive: true });
@@ -340,6 +376,9 @@ export const utils = {
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }), getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
checkExistingAssets: (accessToken: string, checkExistingAssetsDto: CheckExistingAssetsDto) =>
checkExistingAssets({ checkExistingAssetsDto }, { headers: asBearerAuth(accessToken) }),
metadataSearch: async (accessToken: string, dto: MetadataSearchDto) => { metadataSearch: async (accessToken: string, dto: MetadataSearchDto) => {
return searchMetadata({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) }); return searchMetadata({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) });
}, },
@@ -0,0 +1,57 @@
import { AssetMediaResponseDto, LoginResponseDto } from '@immich/sdk';
import { Page, expect, test } from '@playwright/test';
import { utils } from 'src/utils';
function imageLocator(page: Page) {
return page.getByAltText('Image taken on').locator('visible=true');
}
test.describe('Photo Viewer', () => {
let admin: LoginResponseDto;
let asset: AssetMediaResponseDto;
test.beforeAll(async () => {
utils.initSdk();
await utils.resetDatabase();
admin = await utils.adminSetup();
asset = await utils.createAsset(admin.accessToken);
});
test.beforeEach(async ({ context, page }) => {
// before each test, login as user
await utils.setAuthCookies(context, admin.accessToken);
await page.goto('/photos');
await page.waitForLoadState('networkidle');
});
test('initially shows a loading spinner', async ({ page }) => {
await page.route(`/api/assets/${asset.id}/thumbnail**`, async (route) => {
// slow down the request for thumbnail, so spiner has chance to show up
await new Promise((f) => setTimeout(f, 2000));
await route.continue();
});
await page.goto(`/photos/${asset.id}`);
await page.waitForLoadState('load');
// this is the spinner
await page.waitForSelector('svg[role=status]');
await expect(page.getByRole('status')).toBeVisible();
});
test('loads high resolution photo when zoomed', async ({ page }) => {
await page.goto(`/photos/${asset.id}`);
await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('thumbnail');
const box = await imageLocator(page).boundingBox();
expect(box).toBeTruthy;
const { x, y, width, height } = box!;
await page.mouse.move(x + width / 2, y + height / 2);
await page.mouse.wheel(0, -1);
await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('original');
});
test('reloads photo when checksum changes', async ({ page }) => {
await page.goto(`/photos/${asset.id}`);
await expect.poll(async () => await imageLocator(page).getAttribute('src')).toContain('thumbnail');
const initialSrc = await imageLocator(page).getAttribute('src');
await utils.replaceAsset(admin.accessToken, asset.id);
await expect.poll(async () => await imageLocator(page).getAttribute('src')).not.toBe(initialSrc);
});
});
+1 -11
View File
@@ -12,8 +12,6 @@ from rich.logging import RichHandler
from uvicorn import Server from uvicorn import Server
from uvicorn.workers import UvicornWorker from uvicorn.workers import UvicornWorker
from .schemas import ModelType
class PreloadModelData(BaseModel): class PreloadModelData(BaseModel):
clip: str | None clip: str | None
@@ -21,7 +19,7 @@ class PreloadModelData(BaseModel):
class Settings(BaseSettings): class Settings(BaseSettings):
cache_folder: str = "/cache" cache_folder: Path = Path("/cache")
model_ttl: int = 300 model_ttl: int = 300
model_ttl_poll_s: int = 10 model_ttl_poll_s: int = 10
host: str = "0.0.0.0" host: str = "0.0.0.0"
@@ -55,14 +53,6 @@ def clean_name(model_name: str) -> str:
return model_name.split("/")[-1].translate(_clean_name) return model_name.split("/")[-1].translate(_clean_name)
def get_cache_dir(model_name: str, model_type: ModelType) -> Path:
return Path(settings.cache_folder) / model_type.value / clean_name(model_name)
def get_hf_model_name(model_name: str) -> str:
return f"immich-app/{clean_name(model_name)}"
LOG_LEVELS: dict[str, int] = { LOG_LEVELS: dict[str, int] = {
"critical": logging.ERROR, "critical": logging.ERROR,
"error": logging.ERROR, "error": logging.ERROR,
+88 -25
View File
@@ -6,22 +6,34 @@ import threading
import time import time
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from functools import partial
from typing import Any, AsyncGenerator, Callable, Iterator from typing import Any, AsyncGenerator, Callable, Iterator
from zipfile import BadZipFile from zipfile import BadZipFile
import orjson import orjson
from fastapi import Depends, FastAPI, Form, HTTPException, UploadFile from fastapi import Depends, FastAPI, File, Form, HTTPException
from fastapi.responses import ORJSONResponse from fastapi.responses import ORJSONResponse
from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile
from PIL.Image import Image
from pydantic import ValidationError
from starlette.formparsers import MultiPartParser from starlette.formparsers import MultiPartParser
from app.models import get_model_deps
from app.models.base import InferenceModel from app.models.base import InferenceModel
from app.models.transforms import decode_pil
from .config import PreloadModelData, log, settings from .config import PreloadModelData, log, settings
from .models.cache import ModelCache from .models.cache import ModelCache
from .schemas import ( from .schemas import (
InferenceEntries,
InferenceEntry,
InferenceResponse,
MessageResponse, MessageResponse,
ModelIdentity,
ModelTask,
ModelType, ModelType,
PipelineRequest,
T,
TextResponse, TextResponse,
) )
@@ -63,12 +75,21 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
gc.collect() gc.collect()
async def preload_models(preload_models: PreloadModelData) -> None: async def preload_models(preload: PreloadModelData) -> None:
log.info(f"Preloading models: {preload_models}") log.info(f"Preloading models: {preload}")
if preload_models.clip is not None: if preload.clip is not None:
await load(await model_cache.get(preload_models.clip, ModelType.CLIP)) model = await model_cache.get(preload.clip, ModelType.TEXTUAL, ModelTask.SEARCH)
if preload_models.facial_recognition is not None: await load(model)
await load(await model_cache.get(preload_models.facial_recognition, ModelType.FACIAL_RECOGNITION))
model = await model_cache.get(preload.clip, ModelType.VISUAL, ModelTask.SEARCH)
await load(model)
if preload.facial_recognition is not None:
model = await model_cache.get(preload.facial_recognition, ModelType.DETECTION, ModelTask.FACIAL_RECOGNITION)
await load(model)
model = await model_cache.get(preload.facial_recognition, ModelType.RECOGNITION, ModelTask.FACIAL_RECOGNITION)
await load(model)
def update_state() -> Iterator[None]: def update_state() -> Iterator[None]:
@@ -81,6 +102,27 @@ def update_state() -> Iterator[None]:
active_requests -= 1 active_requests -= 1
def get_entries(entries: str = Form()) -> InferenceEntries:
try:
request: PipelineRequest = orjson.loads(entries)
without_deps: list[InferenceEntry] = []
with_deps: list[InferenceEntry] = []
for task, types in request.items():
for type, entry in types.items():
parsed: InferenceEntry = {
"name": entry["modelName"],
"task": task,
"type": type,
"options": entry.get("options", {}),
}
dep = get_model_deps(parsed["name"], type, task)
(with_deps if dep else without_deps).append(parsed)
return without_deps, with_deps
except (orjson.JSONDecodeError, ValidationError, KeyError, AttributeError) as e:
log.error(f"Invalid request format: {e}")
raise HTTPException(422, "Invalid request format.")
app = FastAPI(lifespan=lifespan) app = FastAPI(lifespan=lifespan)
@@ -96,42 +138,63 @@ def ping() -> str:
@app.post("/predict", dependencies=[Depends(update_state)]) @app.post("/predict", dependencies=[Depends(update_state)])
async def predict( async def predict(
model_name: str = Form(alias="modelName"), entries: InferenceEntries = Depends(get_entries),
model_type: ModelType = Form(alias="modelType"), image: bytes | None = File(default=None),
options: str = Form(default="{}"),
text: str | None = Form(default=None), text: str | None = Form(default=None),
image: UploadFile | None = None,
) -> Any: ) -> Any:
if image is not None: if image is not None:
inputs: str | bytes = await image.read() inputs: Image | str = await run(lambda: decode_pil(image))
elif text is not None: elif text is not None:
inputs = text inputs = text
else: else:
raise HTTPException(400, "Either image or text must be provided") raise HTTPException(400, "Either image or text must be provided")
try: response = await run_inference(inputs, entries)
kwargs = orjson.loads(options) return ORJSONResponse(response)
except orjson.JSONDecodeError:
raise HTTPException(400, f"Invalid options JSON: {options}")
model = await load(await model_cache.get(model_name, model_type, ttl=settings.model_ttl, **kwargs))
model.configure(**kwargs)
outputs = await run(model.predict, inputs)
return ORJSONResponse(outputs)
async def run(func: Callable[..., Any], inputs: Any) -> Any: async def run_inference(payload: Image | str, entries: InferenceEntries) -> InferenceResponse:
outputs: dict[ModelIdentity, Any] = {}
response: InferenceResponse = {}
async def _run_inference(entry: InferenceEntry) -> None:
model = await model_cache.get(entry["name"], entry["type"], entry["task"], ttl=settings.model_ttl)
inputs = [payload]
for dep in model.depends:
try:
inputs.append(outputs[dep])
except KeyError:
message = f"Task {entry['task']} of type {entry['type']} depends on output of {dep}"
raise HTTPException(400, message)
model = await load(model)
output = await run(model.predict, *inputs, **entry["options"])
outputs[model.identity] = output
response[entry["task"]] = output
without_deps, with_deps = entries
await asyncio.gather(*[_run_inference(entry) for entry in without_deps])
if with_deps:
await asyncio.gather(*[_run_inference(entry) for entry in with_deps])
if isinstance(payload, Image):
response["imageHeight"], response["imageWidth"] = payload.height, payload.width
return response
async def run(func: Callable[..., T], *args: Any, **kwargs: Any) -> T:
if thread_pool is None: if thread_pool is None:
return func(inputs) return func(*args, **kwargs)
return await asyncio.get_running_loop().run_in_executor(thread_pool, func, inputs) partial_func = partial(func, *args, **kwargs)
return await asyncio.get_running_loop().run_in_executor(thread_pool, partial_func)
async def load(model: InferenceModel) -> InferenceModel: async def load(model: InferenceModel) -> InferenceModel:
if model.loaded: if model.loaded:
return model return model
def _load(model: InferenceModel) -> None: def _load(model: InferenceModel) -> InferenceModel:
with lock: with lock:
model.load() model.load()
return model
try: try:
await run(_load, model) await run(_load, model)
+33 -17
View File
@@ -1,24 +1,40 @@
from typing import Any from typing import Any
from app.schemas import ModelType from app.models.base import InferenceModel
from app.models.clip.textual import MClipTextualEncoder, OpenClipTextualEncoder
from app.models.clip.visual import OpenClipVisualEncoder
from app.schemas import ModelSource, ModelTask, ModelType
from .base import InferenceModel from .constants import get_model_source
from .clip import MCLIPEncoder, OpenCLIPEncoder from .facial_recognition.detection import FaceDetector
from .constants import is_insightface, is_mclip, is_openclip from .facial_recognition.recognition import FaceRecognizer
from .facial_recognition import FaceRecognizer
def from_model_type(model_type: ModelType, model_name: str, **model_kwargs: Any) -> InferenceModel: def get_model_class(model_name: str, model_type: ModelType, model_task: ModelTask) -> type[InferenceModel]:
match model_type: source = get_model_source(model_name)
case ModelType.CLIP: match source, model_type, model_task:
if is_openclip(model_name): case ModelSource.OPENCLIP | ModelSource.MCLIP, ModelType.VISUAL, ModelTask.SEARCH:
return OpenCLIPEncoder(model_name, **model_kwargs) return OpenClipVisualEncoder
elif is_mclip(model_name):
return MCLIPEncoder(model_name, **model_kwargs) case ModelSource.OPENCLIP, ModelType.TEXTUAL, ModelTask.SEARCH:
case ModelType.FACIAL_RECOGNITION: return OpenClipTextualEncoder
if is_insightface(model_name):
return FaceRecognizer(model_name, **model_kwargs) case ModelSource.MCLIP, ModelType.TEXTUAL, ModelTask.SEARCH:
return MClipTextualEncoder
case ModelSource.INSIGHTFACE, ModelType.DETECTION, ModelTask.FACIAL_RECOGNITION:
return FaceDetector
case ModelSource.INSIGHTFACE, ModelType.RECOGNITION, ModelTask.FACIAL_RECOGNITION:
return FaceRecognizer
case _: case _:
raise ValueError(f"Unknown model type {model_type}") raise ValueError(f"Unknown model combination: {source}, {model_type}, {model_task}")
raise ValueError(f"Unknown {model_type} model {model_name}")
def from_model_type(model_name: str, model_type: ModelType, model_task: ModelTask, **kwargs: Any) -> InferenceModel:
return get_model_class(model_name, model_type, model_task)(model_name, **kwargs)
def get_model_deps(model_name: str, model_type: ModelType, model_task: ModelTask) -> list[tuple[ModelType, ModelTask]]:
return get_model_class(model_name, model_type, model_task).depends
+42 -28
View File
@@ -3,7 +3,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from pathlib import Path from pathlib import Path
from shutil import rmtree from shutil import rmtree
from typing import Any from typing import Any, ClassVar
import onnxruntime as ort import onnxruntime as ort
from huggingface_hub import snapshot_download from huggingface_hub import snapshot_download
@@ -11,13 +11,14 @@ from huggingface_hub import snapshot_download
import ann.ann import ann.ann
from app.models.constants import SUPPORTED_PROVIDERS from app.models.constants import SUPPORTED_PROVIDERS
from ..config import get_cache_dir, get_hf_model_name, log, settings from ..config import clean_name, log, settings
from ..schemas import ModelRuntime, ModelType from ..schemas import ModelFormat, ModelIdentity, ModelSession, ModelTask, ModelType
from .ann import AnnSession from .ann import AnnSession
class InferenceModel(ABC): class InferenceModel(ABC):
_model_type: ModelType depends: ClassVar[list[ModelIdentity]]
identity: ClassVar[ModelIdentity]
def __init__( def __init__(
self, self,
@@ -26,16 +27,16 @@ class InferenceModel(ABC):
providers: list[str] | None = None, providers: list[str] | None = None,
provider_options: list[dict[str, Any]] | None = None, provider_options: list[dict[str, Any]] | None = None,
sess_options: ort.SessionOptions | None = None, sess_options: ort.SessionOptions | None = None,
preferred_runtime: ModelRuntime | None = None, preferred_format: ModelFormat | None = None,
**model_kwargs: Any, **model_kwargs: Any,
) -> None: ) -> None:
self.loaded = False self.loaded = False
self.model_name = model_name self.model_name = clean_name(model_name)
self.cache_dir = Path(cache_dir) if cache_dir is not None else self.cache_dir_default self.cache_dir = Path(cache_dir) if cache_dir is not None else self.cache_dir_default
self.providers = providers if providers is not None else self.providers_default self.providers = providers if providers is not None else self.providers_default
self.provider_options = provider_options if provider_options is not None else self.provider_options_default self.provider_options = provider_options if provider_options is not None else self.provider_options_default
self.sess_options = sess_options if sess_options is not None else self.sess_options_default self.sess_options = sess_options if sess_options is not None else self.sess_options_default
self.preferred_runtime = preferred_runtime if preferred_runtime is not None else self.preferred_runtime_default self.preferred_format = preferred_format if preferred_format is not None else self.preferred_format_default
def download(self) -> None: def download(self) -> None:
if not self.cached: if not self.cached:
@@ -47,35 +48,36 @@ class InferenceModel(ABC):
def load(self) -> None: def load(self) -> None:
if self.loaded: if self.loaded:
return return
self.download() self.download()
log.info(f"Loading {self.model_type.replace('-', ' ')} model '{self.model_name}' to memory") log.info(f"Loading {self.model_type.replace('-', ' ')} model '{self.model_name}' to memory")
self._load() self.session = self._load()
self.loaded = True self.loaded = True
def predict(self, inputs: Any, **model_kwargs: Any) -> Any: def predict(self, *inputs: Any, **model_kwargs: Any) -> Any:
self.load() self.load()
if model_kwargs: if model_kwargs:
self.configure(**model_kwargs) self.configure(**model_kwargs)
return self._predict(inputs) return self._predict(*inputs, **model_kwargs)
@abstractmethod @abstractmethod
def _predict(self, inputs: Any) -> Any: ... def _predict(self, *inputs: Any, **model_kwargs: Any) -> Any: ...
def configure(self, **model_kwargs: Any) -> None: def configure(self, **kwargs: Any) -> None:
pass pass
def _download(self) -> None: def _download(self) -> None:
ignore_patterns = [] if self.preferred_runtime == ModelRuntime.ARMNN else ["*.armnn"] ignore_patterns = [] if self.preferred_format == ModelFormat.ARMNN else ["*.armnn"]
snapshot_download( snapshot_download(
get_hf_model_name(self.model_name), f"immich-app/{clean_name(self.model_name)}",
cache_dir=self.cache_dir, cache_dir=self.cache_dir,
local_dir=self.cache_dir, local_dir=self.cache_dir,
local_dir_use_symlinks=False, local_dir_use_symlinks=False,
ignore_patterns=ignore_patterns, ignore_patterns=ignore_patterns,
) )
@abstractmethod def _load(self) -> ModelSession:
def _load(self) -> None: ... return self._make_session(self.model_path)
def clear_cache(self) -> None: def clear_cache(self) -> None:
if not self.cache_dir.exists(): if not self.cache_dir.exists():
@@ -99,7 +101,7 @@ class InferenceModel(ABC):
self.cache_dir.unlink() self.cache_dir.unlink()
self.cache_dir.mkdir(parents=True, exist_ok=True) self.cache_dir.mkdir(parents=True, exist_ok=True)
def _make_session(self, model_path: Path) -> AnnSession | ort.InferenceSession: def _make_session(self, model_path: Path) -> ModelSession:
if not model_path.is_file(): if not model_path.is_file():
onnx_path = model_path.with_suffix(".onnx") onnx_path = model_path.with_suffix(".onnx")
if not onnx_path.is_file(): if not onnx_path.is_file():
@@ -124,9 +126,21 @@ class InferenceModel(ABC):
raise ValueError(f"Unsupported model file type: {model_path.suffix}") raise ValueError(f"Unsupported model file type: {model_path.suffix}")
return session return session
@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.preferred_format}"
@property
def model_task(self) -> ModelTask:
return self.identity[1]
@property @property
def model_type(self) -> ModelType: def model_type(self) -> ModelType:
return self._model_type return self.identity[0]
@property @property
def cache_dir(self) -> Path: def cache_dir(self) -> Path:
@@ -138,11 +152,11 @@ class InferenceModel(ABC):
@property @property
def cache_dir_default(self) -> Path: def cache_dir_default(self) -> Path:
return get_cache_dir(self.model_name, self.model_type) return settings.cache_folder / self.model_task.value / self.model_name
@property @property
def cached(self) -> bool: def cached(self) -> bool:
return self.cache_dir.is_dir() and any(self.cache_dir.iterdir()) return self.model_path.is_file()
@property @property
def providers(self) -> list[str]: def providers(self) -> list[str]:
@@ -226,14 +240,14 @@ class InferenceModel(ABC):
return sess_options return sess_options
@property @property
def preferred_runtime(self) -> ModelRuntime: def preferred_format(self) -> ModelFormat:
return self._preferred_runtime return self._preferred_format
@preferred_runtime.setter @preferred_format.setter
def preferred_runtime(self, preferred_runtime: ModelRuntime) -> None: def preferred_format(self, preferred_format: ModelFormat) -> None:
log.debug(f"Setting preferred runtime to {preferred_runtime}") log.debug(f"Setting preferred format to {preferred_format}")
self._preferred_runtime = preferred_runtime self._preferred_format = preferred_format
@property @property
def preferred_runtime_default(self) -> ModelRuntime: def preferred_format_default(self) -> ModelFormat:
return ModelRuntime.ARMNN if ann.ann.is_available and settings.ann else ModelRuntime.ONNX return ModelFormat.ARMNN if ann.ann.is_available and settings.ann else ModelFormat.ONNX
+9 -16
View File
@@ -5,9 +5,9 @@ from aiocache.lock import OptimisticLock
from aiocache.plugins import TimingPlugin from aiocache.plugins import TimingPlugin
from app.models import from_model_type from app.models import from_model_type
from app.models.base import InferenceModel
from ..schemas import ModelType, has_profiling from ..schemas import ModelTask, ModelType, has_profiling
from .base import InferenceModel
class ModelCache: class ModelCache:
@@ -31,28 +31,21 @@ class ModelCache:
if profiling: if profiling:
plugins.append(TimingPlugin()) plugins.append(TimingPlugin())
self.revalidate_enable = revalidate self.should_revalidate = revalidate
self.cache = SimpleMemoryCache(timeout=timeout, plugins=plugins, namespace=None) self.cache = SimpleMemoryCache(timeout=timeout, plugins=plugins, namespace=None)
async def get(self, model_name: str, model_type: ModelType, **model_kwargs: Any) -> InferenceModel: async def get(
""" self, model_name: str, model_type: ModelType, model_task: ModelTask, **model_kwargs: Any
Args: ) -> InferenceModel:
model_name: Name of model in the model hub used for the task. key = f"{model_name}{model_type}{model_task}"
model_type: Model type or task, which determines which model zoo is used.
Returns:
model: The requested model.
"""
key = f"{model_name}{model_type.value}{model_kwargs.get('mode', '')}"
async with OptimisticLock(self.cache, key) as lock: async with OptimisticLock(self.cache, key) as lock:
model: InferenceModel | None = await self.cache.get(key) model: InferenceModel | None = await self.cache.get(key)
if model is None: if model is None:
model = from_model_type(model_type, model_name, **model_kwargs) model = from_model_type(model_name, model_type, model_task, **model_kwargs)
await lock.cas(model, ttl=model_kwargs.get("ttl", None)) await lock.cas(model, ttl=model_kwargs.get("ttl", None))
elif self.revalidate_enable: elif self.should_revalidate:
await self.revalidate(key, model_kwargs.get("ttl", None)) await self.revalidate(key, model_kwargs.get("ttl", None))
return model return model
-189
View File
@@ -1,189 +0,0 @@
import json
from abc import abstractmethod
from functools import cached_property
from io import BytesIO
from pathlib import Path
from typing import Any, Literal
import numpy as np
from numpy.typing import NDArray
from PIL import Image
from tokenizers import Encoding, Tokenizer
from app.config import clean_name, log
from app.models.transforms import crop, get_pil_resampling, normalize, resize, to_numpy
from app.schemas import ModelType
from .base import InferenceModel
class BaseCLIPEncoder(InferenceModel):
_model_type = ModelType.CLIP
def __init__(
self,
model_name: str,
cache_dir: Path | str | None = None,
mode: Literal["text", "vision"] | None = None,
**model_kwargs: Any,
) -> None:
self.mode = mode
super().__init__(model_name, cache_dir, **model_kwargs)
def _load(self) -> None:
if self.mode == "text" or self.mode is None:
log.debug(f"Loading clip text model '{self.model_name}'")
self.text_model = self._make_session(self.textual_path)
log.debug(f"Loaded clip text model '{self.model_name}'")
if self.mode == "vision" or self.mode is None:
log.debug(f"Loading clip vision model '{self.model_name}'")
self.vision_model = self._make_session(self.visual_path)
log.debug(f"Loaded clip vision model '{self.model_name}'")
def _predict(self, image_or_text: Image.Image | str) -> NDArray[np.float32]:
if isinstance(image_or_text, bytes):
image_or_text = Image.open(BytesIO(image_or_text))
match image_or_text:
case Image.Image():
if self.mode == "text":
raise TypeError("Cannot encode image as text-only model")
outputs: NDArray[np.float32] = self.vision_model.run(None, self.transform(image_or_text))[0][0]
case str():
if self.mode == "vision":
raise TypeError("Cannot encode text as vision-only model")
outputs = self.text_model.run(None, self.tokenize(image_or_text))[0][0]
case _:
raise TypeError(f"Expected Image or str, but got: {type(image_or_text)}")
return outputs
@abstractmethod
def tokenize(self, text: str) -> dict[str, NDArray[np.int32]]:
pass
@abstractmethod
def transform(self, image: Image.Image) -> dict[str, NDArray[np.float32]]:
pass
@property
def textual_dir(self) -> Path:
return self.cache_dir / "textual"
@property
def visual_dir(self) -> Path:
return self.cache_dir / "visual"
@property
def model_cfg_path(self) -> Path:
return self.cache_dir / "config.json"
@property
def textual_path(self) -> Path:
return self.textual_dir / f"model.{self.preferred_runtime}"
@property
def visual_path(self) -> Path:
return self.visual_dir / f"model.{self.preferred_runtime}"
@property
def tokenizer_file_path(self) -> Path:
return self.textual_dir / "tokenizer.json"
@property
def tokenizer_cfg_path(self) -> Path:
return self.textual_dir / "tokenizer_config.json"
@property
def preprocess_cfg_path(self) -> Path:
return self.visual_dir / "preprocess_cfg.json"
@property
def cached(self) -> bool:
return self.textual_path.is_file() and self.visual_path.is_file()
@cached_property
def model_cfg(self) -> dict[str, Any]:
log.debug(f"Loading model config for CLIP model '{self.model_name}'")
model_cfg: dict[str, Any] = json.load(self.model_cfg_path.open())
log.debug(f"Loaded model config for CLIP model '{self.model_name}'")
return model_cfg
@cached_property
def tokenizer_file(self) -> dict[str, Any]:
log.debug(f"Loading tokenizer file for CLIP model '{self.model_name}'")
tokenizer_file: dict[str, Any] = json.load(self.tokenizer_file_path.open())
log.debug(f"Loaded tokenizer file for CLIP model '{self.model_name}'")
return tokenizer_file
@cached_property
def tokenizer_cfg(self) -> dict[str, Any]:
log.debug(f"Loading tokenizer config for CLIP model '{self.model_name}'")
tokenizer_cfg: dict[str, Any] = json.load(self.tokenizer_cfg_path.open())
log.debug(f"Loaded tokenizer config for CLIP model '{self.model_name}'")
return tokenizer_cfg
@cached_property
def preprocess_cfg(self) -> dict[str, Any]:
log.debug(f"Loading visual preprocessing config for CLIP model '{self.model_name}'")
preprocess_cfg: dict[str, Any] = json.load(self.preprocess_cfg_path.open())
log.debug(f"Loaded visual preprocessing config for CLIP model '{self.model_name}'")
return preprocess_cfg
class OpenCLIPEncoder(BaseCLIPEncoder):
def __init__(
self,
model_name: str,
cache_dir: Path | str | None = None,
mode: Literal["text", "vision"] | None = None,
**model_kwargs: Any,
) -> None:
super().__init__(clean_name(model_name), cache_dir, mode, **model_kwargs)
def _load(self) -> None:
super()._load()
self._load_tokenizer()
size: list[int] | int = self.preprocess_cfg["size"]
self.size = size[0] if isinstance(size, list) else size
self.resampling = get_pil_resampling(self.preprocess_cfg["interpolation"])
self.mean = np.array(self.preprocess_cfg["mean"], dtype=np.float32)
self.std = np.array(self.preprocess_cfg["std"], dtype=np.float32)
def _load_tokenizer(self) -> Tokenizer:
log.debug(f"Loading tokenizer for CLIP model '{self.model_name}'")
text_cfg: dict[str, Any] = self.model_cfg["text_cfg"]
context_length: int = text_cfg.get("context_length", 77)
pad_token: str = self.tokenizer_cfg["pad_token"]
self.tokenizer: Tokenizer = Tokenizer.from_file(self.tokenizer_file_path.as_posix())
pad_id: int = self.tokenizer.token_to_id(pad_token)
self.tokenizer.enable_padding(length=context_length, pad_token=pad_token, pad_id=pad_id)
self.tokenizer.enable_truncation(max_length=context_length)
log.debug(f"Loaded tokenizer for CLIP model '{self.model_name}'")
def tokenize(self, text: str) -> dict[str, NDArray[np.int32]]:
tokens: Encoding = self.tokenizer.encode(text)
return {"text": np.array([tokens.ids], dtype=np.int32)}
def transform(self, image: Image.Image) -> dict[str, NDArray[np.float32]]:
image = resize(image, self.size)
image = crop(image, self.size)
image_np = to_numpy(image)
image_np = normalize(image_np, self.mean, self.std)
return {"image": np.expand_dims(image_np.transpose(2, 0, 1), 0)}
class MCLIPEncoder(OpenCLIPEncoder):
def tokenize(self, text: str) -> dict[str, NDArray[np.int32]]:
tokens: Encoding = self.tokenizer.encode(text)
return {
"input_ids": np.array([tokens.ids], dtype=np.int32),
"attention_mask": np.array([tokens.attention_mask], dtype=np.int32),
}
@@ -0,0 +1,98 @@
import json
from abc import abstractmethod
from functools import cached_property
from pathlib import Path
from typing import Any
import numpy as np
from numpy.typing import NDArray
from tokenizers import Encoding, Tokenizer
from app.config import log
from app.models.base import InferenceModel
from app.schemas import ModelSession, ModelTask, ModelType
class BaseCLIPTextualEncoder(InferenceModel):
depends = []
identity = (ModelType.TEXTUAL, ModelTask.SEARCH)
def _predict(self, inputs: str, **kwargs: Any) -> NDArray[np.float32]:
res: NDArray[np.float32] = self.session.run(None, self.tokenize(inputs))[0][0]
return res
def _load(self) -> ModelSession:
log.debug(f"Loading tokenizer for CLIP model '{self.model_name}'")
self.tokenizer = self._load_tokenizer()
log.debug(f"Loaded tokenizer for CLIP model '{self.model_name}'")
return super()._load()
@abstractmethod
def _load_tokenizer(self) -> Tokenizer:
pass
@abstractmethod
def tokenize(self, text: str) -> dict[str, NDArray[np.int32]]:
pass
@property
def model_cfg_path(self) -> Path:
return self.cache_dir / "config.json"
@property
def tokenizer_file_path(self) -> Path:
return self.model_dir / "tokenizer.json"
@property
def tokenizer_cfg_path(self) -> Path:
return self.model_dir / "tokenizer_config.json"
@cached_property
def model_cfg(self) -> dict[str, Any]:
log.debug(f"Loading model config for CLIP model '{self.model_name}'")
model_cfg: dict[str, Any] = json.load(self.model_cfg_path.open())
log.debug(f"Loaded model config for CLIP model '{self.model_name}'")
return model_cfg
@cached_property
def tokenizer_file(self) -> dict[str, Any]:
log.debug(f"Loading tokenizer file for CLIP model '{self.model_name}'")
tokenizer_file: dict[str, Any] = json.load(self.tokenizer_file_path.open())
log.debug(f"Loaded tokenizer file for CLIP model '{self.model_name}'")
return tokenizer_file
@cached_property
def tokenizer_cfg(self) -> dict[str, Any]:
log.debug(f"Loading tokenizer config for CLIP model '{self.model_name}'")
tokenizer_cfg: dict[str, Any] = json.load(self.tokenizer_cfg_path.open())
log.debug(f"Loaded tokenizer config for CLIP model '{self.model_name}'")
return tokenizer_cfg
class OpenClipTextualEncoder(BaseCLIPTextualEncoder):
def _load_tokenizer(self) -> Tokenizer:
text_cfg: dict[str, Any] = self.model_cfg["text_cfg"]
context_length: int = text_cfg.get("context_length", 77)
pad_token: str = self.tokenizer_cfg["pad_token"]
tokenizer: Tokenizer = Tokenizer.from_file(self.tokenizer_file_path.as_posix())
pad_id: int = tokenizer.token_to_id(pad_token)
tokenizer.enable_padding(length=context_length, pad_token=pad_token, pad_id=pad_id)
tokenizer.enable_truncation(max_length=context_length)
return tokenizer
def tokenize(self, text: str) -> dict[str, NDArray[np.int32]]:
tokens: Encoding = self.tokenizer.encode(text)
return {"text": np.array([tokens.ids], dtype=np.int32)}
class MClipTextualEncoder(OpenClipTextualEncoder):
def tokenize(self, text: str) -> dict[str, NDArray[np.int32]]:
tokens: Encoding = self.tokenizer.encode(text)
return {
"input_ids": np.array([tokens.ids], dtype=np.int32),
"attention_mask": np.array([tokens.attention_mask], dtype=np.int32),
}
@@ -0,0 +1,69 @@
import json
from abc import abstractmethod
from functools import cached_property
from pathlib import Path
from typing import Any
import numpy as np
from numpy.typing import NDArray
from PIL import Image
from app.config import log
from app.models.base import InferenceModel
from app.models.transforms import crop_pil, decode_pil, get_pil_resampling, normalize, resize_pil, to_numpy
from app.schemas import ModelSession, ModelTask, ModelType
class BaseCLIPVisualEncoder(InferenceModel):
depends = []
identity = (ModelType.VISUAL, ModelTask.SEARCH)
def _predict(self, inputs: Image.Image | bytes, **kwargs: Any) -> NDArray[np.float32]:
image = decode_pil(inputs)
res: NDArray[np.float32] = self.session.run(None, self.transform(image))[0][0]
return res
@abstractmethod
def transform(self, image: Image.Image) -> dict[str, NDArray[np.float32]]:
pass
@property
def model_cfg_path(self) -> Path:
return self.cache_dir / "config.json"
@property
def preprocess_cfg_path(self) -> Path:
return self.model_dir / "preprocess_cfg.json"
@cached_property
def model_cfg(self) -> dict[str, Any]:
log.debug(f"Loading model config for CLIP model '{self.model_name}'")
model_cfg: dict[str, Any] = json.load(self.model_cfg_path.open())
log.debug(f"Loaded model config for CLIP model '{self.model_name}'")
return model_cfg
@cached_property
def preprocess_cfg(self) -> dict[str, Any]:
log.debug(f"Loading visual preprocessing config for CLIP model '{self.model_name}'")
preprocess_cfg: dict[str, Any] = json.load(self.preprocess_cfg_path.open())
log.debug(f"Loaded visual preprocessing config for CLIP model '{self.model_name}'")
return preprocess_cfg
class OpenClipVisualEncoder(BaseCLIPVisualEncoder):
def _load(self) -> ModelSession:
size: list[int] | int = self.preprocess_cfg["size"]
self.size = size[0] if isinstance(size, list) else size
self.resampling = get_pil_resampling(self.preprocess_cfg["interpolation"])
self.mean = np.array(self.preprocess_cfg["mean"], dtype=np.float32)
self.std = np.array(self.preprocess_cfg["std"], dtype=np.float32)
return super()._load()
def transform(self, image: Image.Image) -> dict[str, NDArray[np.float32]]:
image = resize_pil(image, self.size)
image = crop_pil(image, self.size)
image_np = to_numpy(image)
image_np = normalize(image_np, self.mean, self.std)
return {"image": np.expand_dims(image_np.transpose(2, 0, 1), 0)}
+10 -6
View File
@@ -1,4 +1,5 @@
from app.config import clean_name from app.config import clean_name
from app.schemas import ModelSource
_OPENCLIP_MODELS = { _OPENCLIP_MODELS = {
"RN50__openai", "RN50__openai",
@@ -54,13 +55,16 @@ _INSIGHTFACE_MODELS = {
SUPPORTED_PROVIDERS = ["CUDAExecutionProvider", "OpenVINOExecutionProvider", "CPUExecutionProvider"] SUPPORTED_PROVIDERS = ["CUDAExecutionProvider", "OpenVINOExecutionProvider", "CPUExecutionProvider"]
def is_openclip(model_name: str) -> bool: def get_model_source(model_name: str) -> ModelSource | None:
return clean_name(model_name) in _OPENCLIP_MODELS cleaned_name = clean_name(model_name)
if cleaned_name in _INSIGHTFACE_MODELS:
return ModelSource.INSIGHTFACE
def is_mclip(model_name: str) -> bool: if cleaned_name in _MCLIP_MODELS:
return clean_name(model_name) in _MCLIP_MODELS return ModelSource.MCLIP
if cleaned_name in _OPENCLIP_MODELS:
return ModelSource.OPENCLIP
def is_insightface(model_name: str) -> bool: return None
return clean_name(model_name) in _INSIGHTFACE_MODELS
@@ -1,90 +0,0 @@
from pathlib import Path
from typing import Any
import cv2
import numpy as np
from insightface.model_zoo import ArcFaceONNX, RetinaFace
from insightface.utils.face_align import norm_crop
from numpy.typing import NDArray
from app.config import clean_name
from app.schemas import Face, ModelType, is_ndarray
from .base import InferenceModel
class FaceRecognizer(InferenceModel):
_model_type = ModelType.FACIAL_RECOGNITION
def __init__(
self,
model_name: str,
min_score: float = 0.7,
cache_dir: Path | str | None = None,
**model_kwargs: Any,
) -> None:
self.min_score = model_kwargs.pop("minScore", min_score)
super().__init__(clean_name(model_name), cache_dir, **model_kwargs)
def _load(self) -> None:
self.det_model = RetinaFace(session=self._make_session(self.det_file))
self.rec_model = ArcFaceONNX(
self.rec_file.with_suffix(".onnx").as_posix(),
session=self._make_session(self.rec_file),
)
self.det_model.prepare(
ctx_id=0,
det_thresh=self.min_score,
input_size=(640, 640),
)
self.rec_model.prepare(ctx_id=0)
def _predict(self, image: NDArray[np.uint8] | bytes) -> list[Face]:
if isinstance(image, bytes):
decoded_image = cv2.imdecode(np.frombuffer(image, np.uint8), cv2.IMREAD_COLOR)
else:
decoded_image = image
assert is_ndarray(decoded_image, np.uint8)
bboxes, kpss = self.det_model.detect(decoded_image)
if bboxes.size == 0:
return []
assert is_ndarray(kpss, np.float32)
scores = bboxes[:, 4].tolist()
bboxes = bboxes[:, :4].round().tolist()
results = []
height, width, _ = decoded_image.shape
for (x1, y1, x2, y2), score, kps in zip(bboxes, scores, kpss):
cropped_img = norm_crop(decoded_image, kps)
embedding: NDArray[np.float32] = self.rec_model.get_feat(cropped_img)[0]
face: Face = {
"imageWidth": width,
"imageHeight": height,
"boundingBox": {
"x1": x1,
"y1": y1,
"x2": x2,
"y2": y2,
},
"score": score,
"embedding": embedding,
}
results.append(face)
return results
@property
def cached(self) -> bool:
return self.det_file.is_file() and self.rec_file.is_file()
@property
def det_file(self) -> Path:
return self.cache_dir / "detection" / f"model.{self.preferred_runtime}"
@property
def rec_file(self) -> Path:
return self.cache_dir / "recognition" / f"model.{self.preferred_runtime}"
def configure(self, **model_kwargs: Any) -> None:
self.det_model.det_thresh = model_kwargs.pop("minScore", self.det_model.det_thresh)
@@ -0,0 +1,48 @@
from pathlib import Path
from typing import Any
import numpy as np
from insightface.model_zoo import RetinaFace
from numpy.typing import NDArray
from app.models.base import InferenceModel
from app.models.transforms import decode_cv2
from app.schemas import FaceDetectionOutput, ModelSession, ModelTask, ModelType
class FaceDetector(InferenceModel):
depends = []
identity = (ModelType.DETECTION, ModelTask.FACIAL_RECOGNITION)
def __init__(
self,
model_name: str,
min_score: float = 0.7,
cache_dir: Path | str | None = None,
**model_kwargs: Any,
) -> None:
self.min_score = model_kwargs.pop("minScore", min_score)
super().__init__(model_name, cache_dir, **model_kwargs)
def _load(self) -> ModelSession:
session = self._make_session(self.model_path)
self.model = RetinaFace(session=session)
self.model.prepare(ctx_id=0, det_thresh=self.min_score, input_size=(640, 640))
return session
def _predict(self, inputs: NDArray[np.uint8] | bytes, **kwargs: Any) -> FaceDetectionOutput:
inputs = decode_cv2(inputs)
bboxes, landmarks = self._detect(inputs)
return {
"boxes": bboxes[:, :4].round(),
"scores": bboxes[:, 4],
"landmarks": landmarks,
}
def _detect(self, inputs: NDArray[np.uint8] | bytes) -> tuple[NDArray[np.float32], NDArray[np.float32]]:
return self.model.detect(inputs) # type: ignore
def configure(self, **kwargs: Any) -> None:
self.model.det_thresh = kwargs.pop("minScore", self.model.det_thresh)
@@ -0,0 +1,77 @@
from pathlib import Path
from typing import Any
import numpy as np
import onnx
import onnxruntime as ort
from insightface.model_zoo import ArcFaceONNX
from insightface.utils.face_align import norm_crop
from numpy.typing import NDArray
from onnx.tools.update_model_dims import update_inputs_outputs_dims
from PIL import Image
from app.config import clean_name, log
from app.models.base import InferenceModel
from app.models.transforms import decode_cv2
from app.schemas import FaceDetectionOutput, FacialRecognitionOutput, ModelSession, ModelTask, ModelType
class FaceRecognizer(InferenceModel):
depends = [(ModelType.DETECTION, ModelTask.FACIAL_RECOGNITION)]
identity = (ModelType.RECOGNITION, ModelTask.FACIAL_RECOGNITION)
def __init__(
self,
model_name: str,
min_score: float = 0.7,
cache_dir: Path | str | None = None,
**model_kwargs: Any,
) -> None:
self.min_score = model_kwargs.pop("minScore", min_score)
super().__init__(clean_name(model_name), cache_dir, **model_kwargs)
def _load(self) -> ModelSession:
session = self._make_session(self.model_path)
if not self._has_batch_dim(session):
self._add_batch_dim(self.model_path)
session = self._make_session(self.model_path)
self.model = ArcFaceONNX(
self.model_path.with_suffix(".onnx").as_posix(),
session=session,
)
return session
def _predict(
self, inputs: NDArray[np.uint8] | bytes | Image.Image, faces: FaceDetectionOutput, **kwargs: Any
) -> FacialRecognitionOutput:
if faces["boxes"].shape[0] == 0:
return []
inputs = decode_cv2(inputs)
embeddings: NDArray[np.float32] = self.model.get_feat(self._crop(inputs, faces))
return self.postprocess(faces, embeddings)
def postprocess(self, faces: FaceDetectionOutput, embeddings: NDArray[np.float32]) -> FacialRecognitionOutput:
return [
{
"boundingBox": {"x1": x1, "y1": y1, "x2": x2, "y2": y2},
"embedding": embedding,
"score": score,
}
for (x1, y1, x2, y2), embedding, score in zip(faces["boxes"], embeddings, faces["scores"])
]
def _crop(self, image: NDArray[np.uint8], faces: FaceDetectionOutput) -> list[NDArray[np.uint8]]:
return [norm_crop(image, landmark) for landmark in faces["landmarks"]]
def _has_batch_dim(self, session: ort.InferenceSession) -> bool:
return not isinstance(session, ort.InferenceSession) or session.get_inputs()[0].shape[0] == "batch"
def _add_batch_dim(self, model_path: Path) -> None:
log.debug(f"Adding batch dimension to model {model_path}")
proto = onnx.load(model_path)
static_input_dims = [shape.dim_value for shape in proto.graph.input[0].type.tensor_type.shape.dim[1:]]
static_output_dims = [shape.dim_value for shape in proto.graph.output[0].type.tensor_type.shape.dim[1:]]
input_dims = {proto.graph.input[0].name: ["batch"] + static_input_dims}
output_dims = {proto.graph.output[0].name: ["batch"] + static_output_dims}
updated_proto = update_inputs_outputs_dims(proto, input_dims, output_dims)
onnx.save(updated_proto, model_path)
+30 -4
View File
@@ -1,3 +1,7 @@
from io import BytesIO
from typing import IO
import cv2
import numpy as np import numpy as np
from numpy.typing import NDArray from numpy.typing import NDArray
from PIL import Image from PIL import Image
@@ -5,7 +9,7 @@ from PIL import Image
_PIL_RESAMPLING_METHODS = {resampling.name.lower(): resampling for resampling in Image.Resampling} _PIL_RESAMPLING_METHODS = {resampling.name.lower(): resampling for resampling in Image.Resampling}
def resize(img: Image.Image, size: int) -> Image.Image: def resize_pil(img: Image.Image, size: int) -> Image.Image:
if img.width < img.height: if img.width < img.height:
return img.resize((size, int((img.height / img.width) * size)), resample=Image.Resampling.BICUBIC) return img.resize((size, int((img.height / img.width) * size)), resample=Image.Resampling.BICUBIC)
else: else:
@@ -13,7 +17,7 @@ def resize(img: Image.Image, size: int) -> Image.Image:
# https://stackoverflow.com/a/60883103 # https://stackoverflow.com/a/60883103
def crop(img: Image.Image, size: int) -> Image.Image: def crop_pil(img: Image.Image, size: int) -> Image.Image:
left = int((img.size[0] / 2) - (size / 2)) left = int((img.size[0] / 2) - (size / 2))
upper = int((img.size[1] / 2) - (size / 2)) upper = int((img.size[1] / 2) - (size / 2))
right = left + size right = left + size
@@ -23,14 +27,36 @@ def crop(img: Image.Image, size: int) -> Image.Image:
def to_numpy(img: Image.Image) -> NDArray[np.float32]: def to_numpy(img: Image.Image) -> NDArray[np.float32]:
return np.asarray(img.convert("RGB")).astype(np.float32) / 255.0 return np.asarray(img if img.mode == "RGB" else img.convert("RGB"), dtype=np.float32) / 255.0
def normalize( def normalize(
img: NDArray[np.float32], mean: float | NDArray[np.float32], std: float | NDArray[np.float32] img: NDArray[np.float32], mean: float | NDArray[np.float32], std: float | NDArray[np.float32]
) -> NDArray[np.float32]: ) -> NDArray[np.float32]:
return (img - mean) / std return np.divide(img - mean, std, dtype=np.float32)
def get_pil_resampling(resample: str) -> Image.Resampling: def get_pil_resampling(resample: str) -> Image.Resampling:
return _PIL_RESAMPLING_METHODS[resample.lower()] return _PIL_RESAMPLING_METHODS[resample.lower()]
def pil_to_cv2(image: Image.Image) -> NDArray[np.uint8]:
return cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) # type: ignore
def decode_pil(image_bytes: bytes | IO[bytes] | Image.Image) -> Image.Image:
if isinstance(image_bytes, Image.Image):
return image_bytes
image = Image.open(BytesIO(image_bytes) if isinstance(image_bytes, bytes) else image_bytes)
image.load() # type: ignore
if not image.mode == "RGB":
image = image.convert("RGB")
return image
def decode_cv2(image_bytes: NDArray[np.uint8] | bytes | Image.Image) -> NDArray[np.uint8]:
if isinstance(image_bytes, bytes):
image_bytes = decode_pil(image_bytes) # pillow is much faster than cv2
if isinstance(image_bytes, Image.Image):
return pil_to_cv2(image_bytes)
return image_bytes
+64 -8
View File
@@ -1,5 +1,5 @@
from enum import Enum from enum import Enum
from typing import Any, Protocol, TypedDict, TypeGuard from typing import Any, Literal, Protocol, TypedDict, TypeGuard, TypeVar
import numpy as np import numpy as np
import numpy.typing as npt import numpy.typing as npt
@@ -28,31 +28,87 @@ class BoundingBox(TypedDict):
y2: int y2: int
class ModelType(StrEnum): class ModelTask(StrEnum):
CLIP = "clip"
FACIAL_RECOGNITION = "facial-recognition" FACIAL_RECOGNITION = "facial-recognition"
SEARCH = "clip"
class ModelRuntime(StrEnum): class ModelType(StrEnum):
ONNX = "onnx" DETECTION = "detection"
RECOGNITION = "recognition"
TEXTUAL = "textual"
VISUAL = "visual"
class ModelFormat(StrEnum):
ARMNN = "armnn" ARMNN = "armnn"
ONNX = "onnx"
class ModelSource(StrEnum):
INSIGHTFACE = "insightface"
MCLIP = "mclip"
OPENCLIP = "openclip"
ModelIdentity = tuple[ModelType, ModelTask]
class ModelSession(Protocol):
def run(
self,
output_names: list[str] | None,
input_feed: dict[str, npt.NDArray[np.float32]] | dict[str, npt.NDArray[np.int32]],
run_options: Any = None,
) -> list[npt.NDArray[np.float32]]: ...
class HasProfiling(Protocol): class HasProfiling(Protocol):
profiling: dict[str, float] profiling: dict[str, float]
class Face(TypedDict): class FaceDetectionOutput(TypedDict):
boxes: npt.NDArray[np.float32]
scores: npt.NDArray[np.float32]
landmarks: npt.NDArray[np.float32]
class DetectedFace(TypedDict):
boundingBox: BoundingBox boundingBox: BoundingBox
embedding: npt.NDArray[np.float32] embedding: npt.NDArray[np.float32]
imageWidth: int
imageHeight: int
score: float score: float
FacialRecognitionOutput = list[DetectedFace]
class PipelineEntry(TypedDict):
modelName: str
options: dict[str, Any]
PipelineRequest = dict[ModelTask, dict[ModelType, PipelineEntry]]
class InferenceEntry(TypedDict):
name: str
task: ModelTask
type: ModelType
options: dict[str, Any]
InferenceEntries = tuple[list[InferenceEntry], list[InferenceEntry]]
InferenceResponse = dict[ModelTask | Literal["imageHeight"] | Literal["imageWidth"], Any]
def has_profiling(obj: Any) -> TypeGuard[HasProfiling]: def has_profiling(obj: Any) -> TypeGuard[HasProfiling]:
return hasattr(obj, "profiling") and isinstance(obj.profiling, dict) return hasattr(obj, "profiling") and isinstance(obj.profiling, dict)
def is_ndarray(obj: Any, dtype: "type[np._DTypeScalar_co]") -> "TypeGuard[npt.NDArray[np._DTypeScalar_co]]": def is_ndarray(obj: Any, dtype: "type[np._DTypeScalar_co]") -> "TypeGuard[npt.NDArray[np._DTypeScalar_co]]":
return isinstance(obj, np.ndarray) and obj.dtype == dtype return isinstance(obj, np.ndarray) and obj.dtype == dtype
T = TypeVar("T")
+209 -121
View File
@@ -17,13 +17,15 @@ from pytest import MonkeyPatch
from pytest_mock import MockerFixture from pytest_mock import MockerFixture
from app.main import load, preload_models from app.main import load, preload_models
from app.models.clip.textual import MClipTextualEncoder, OpenClipTextualEncoder
from app.models.clip.visual import OpenClipVisualEncoder
from app.models.facial_recognition.detection import FaceDetector
from app.models.facial_recognition.recognition import FaceRecognizer
from .config import Settings, log, settings from .config import Settings, log, settings
from .models.base import InferenceModel from .models.base import InferenceModel
from .models.cache import ModelCache from .models.cache import ModelCache
from .models.clip import MCLIPEncoder, OpenCLIPEncoder from .schemas import ModelFormat, ModelTask, ModelType
from .models.facial_recognition import FaceRecognizer
from .schemas import ModelRuntime, ModelType
class TestBase: class TestBase:
@@ -35,13 +37,13 @@ class TestBase:
@pytest.mark.providers(CPU_EP) @pytest.mark.providers(CPU_EP)
def test_sets_cpu_provider(self, providers: list[str]) -> None: def test_sets_cpu_provider(self, providers: list[str]) -> None:
encoder = OpenCLIPEncoder("ViT-B-32__openai") encoder = OpenClipTextualEncoder("ViT-B-32__openai")
assert encoder.providers == self.CPU_EP assert encoder.providers == self.CPU_EP
@pytest.mark.providers(CUDA_EP) @pytest.mark.providers(CUDA_EP)
def test_sets_cuda_provider_if_available(self, providers: list[str]) -> None: def test_sets_cuda_provider_if_available(self, providers: list[str]) -> None:
encoder = OpenCLIPEncoder("ViT-B-32__openai") encoder = OpenClipTextualEncoder("ViT-B-32__openai")
assert encoder.providers == self.CUDA_EP assert encoder.providers == self.CUDA_EP
@@ -50,7 +52,7 @@ class TestBase:
mocked = mocker.patch("app.models.base.ort.capi._pybind_state") mocked = mocker.patch("app.models.base.ort.capi._pybind_state")
mocked.get_available_openvino_device_ids.return_value = ["GPU.0", "CPU"] mocked.get_available_openvino_device_ids.return_value = ["GPU.0", "CPU"]
encoder = OpenCLIPEncoder("ViT-B-32__openai") encoder = OpenClipTextualEncoder("ViT-B-32__openai")
assert encoder.providers == self.OV_EP assert encoder.providers == self.OV_EP
@@ -59,25 +61,25 @@ class TestBase:
mocked = mocker.patch("app.models.base.ort.capi._pybind_state") mocked = mocker.patch("app.models.base.ort.capi._pybind_state")
mocked.get_available_openvino_device_ids.return_value = ["CPU"] mocked.get_available_openvino_device_ids.return_value = ["CPU"]
encoder = OpenCLIPEncoder("ViT-B-32__openai") encoder = OpenClipTextualEncoder("ViT-B-32__openai")
assert encoder.providers == self.CPU_EP assert encoder.providers == self.CPU_EP
@pytest.mark.providers(CUDA_EP_OUT_OF_ORDER) @pytest.mark.providers(CUDA_EP_OUT_OF_ORDER)
def test_sets_providers_in_correct_order(self, providers: list[str]) -> None: def test_sets_providers_in_correct_order(self, providers: list[str]) -> None:
encoder = OpenCLIPEncoder("ViT-B-32__openai") encoder = OpenClipTextualEncoder("ViT-B-32__openai")
assert encoder.providers == self.CUDA_EP assert encoder.providers == self.CUDA_EP
@pytest.mark.providers(TRT_EP) @pytest.mark.providers(TRT_EP)
def test_ignores_unsupported_providers(self, providers: list[str]) -> None: def test_ignores_unsupported_providers(self, providers: list[str]) -> None:
encoder = OpenCLIPEncoder("ViT-B-32__openai") encoder = OpenClipTextualEncoder("ViT-B-32__openai")
assert encoder.providers == self.CUDA_EP assert encoder.providers == self.CUDA_EP
def test_sets_provider_kwarg(self) -> None: def test_sets_provider_kwarg(self) -> None:
providers = ["CUDAExecutionProvider"] providers = ["CUDAExecutionProvider"]
encoder = OpenCLIPEncoder("ViT-B-32__openai", providers=providers) encoder = OpenClipTextualEncoder("ViT-B-32__openai", providers=providers)
assert encoder.providers == providers assert encoder.providers == providers
@@ -85,7 +87,9 @@ class TestBase:
mocked = mocker.patch("app.models.base.ort.capi._pybind_state") mocked = mocker.patch("app.models.base.ort.capi._pybind_state")
mocked.get_available_openvino_device_ids.return_value = ["GPU.0", "CPU"] mocked.get_available_openvino_device_ids.return_value = ["GPU.0", "CPU"]
encoder = OpenCLIPEncoder("ViT-B-32__openai", providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"]) encoder = OpenClipTextualEncoder(
"ViT-B-32__openai", providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"]
)
assert encoder.provider_options == [ assert encoder.provider_options == [
{"device_type": "GPU_FP32", "cache_dir": (encoder.cache_dir / "openvino").as_posix()}, {"device_type": "GPU_FP32", "cache_dir": (encoder.cache_dir / "openvino").as_posix()},
@@ -93,7 +97,7 @@ class TestBase:
] ]
def test_sets_provider_options_kwarg(self) -> None: def test_sets_provider_options_kwarg(self) -> None:
encoder = OpenCLIPEncoder( encoder = OpenClipTextualEncoder(
"ViT-B-32__openai", "ViT-B-32__openai",
providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"], providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"],
provider_options=[], provider_options=[],
@@ -102,7 +106,7 @@ class TestBase:
assert encoder.provider_options == [] assert encoder.provider_options == []
def test_sets_default_sess_options(self) -> None: def test_sets_default_sess_options(self) -> None:
encoder = OpenCLIPEncoder("ViT-B-32__openai") encoder = OpenClipTextualEncoder("ViT-B-32__openai")
assert encoder.sess_options.execution_mode == ort.ExecutionMode.ORT_SEQUENTIAL assert encoder.sess_options.execution_mode == ort.ExecutionMode.ORT_SEQUENTIAL
assert encoder.sess_options.inter_op_num_threads == 1 assert encoder.sess_options.inter_op_num_threads == 1
@@ -110,7 +114,9 @@ class TestBase:
assert encoder.sess_options.enable_cpu_mem_arena is False assert encoder.sess_options.enable_cpu_mem_arena is False
def test_sets_default_sess_options_does_not_set_threads_if_non_cpu_and_default_threads(self) -> None: def test_sets_default_sess_options_does_not_set_threads_if_non_cpu_and_default_threads(self) -> None:
encoder = OpenCLIPEncoder("ViT-B-32__openai", providers=["CUDAExecutionProvider", "CPUExecutionProvider"]) encoder = OpenClipTextualEncoder(
"ViT-B-32__openai", providers=["CUDAExecutionProvider", "CPUExecutionProvider"]
)
assert encoder.sess_options.inter_op_num_threads == 0 assert encoder.sess_options.inter_op_num_threads == 0
assert encoder.sess_options.intra_op_num_threads == 0 assert encoder.sess_options.intra_op_num_threads == 0
@@ -120,14 +126,16 @@ class TestBase:
mock_settings.model_inter_op_threads = 2 mock_settings.model_inter_op_threads = 2
mock_settings.model_intra_op_threads = 4 mock_settings.model_intra_op_threads = 4
encoder = OpenCLIPEncoder("ViT-B-32__openai", providers=["CUDAExecutionProvider", "CPUExecutionProvider"]) encoder = OpenClipTextualEncoder(
"ViT-B-32__openai", providers=["CUDAExecutionProvider", "CPUExecutionProvider"]
)
assert encoder.sess_options.inter_op_num_threads == 2 assert encoder.sess_options.inter_op_num_threads == 2
assert encoder.sess_options.intra_op_num_threads == 4 assert encoder.sess_options.intra_op_num_threads == 4
def test_sets_sess_options_kwarg(self) -> None: def test_sets_sess_options_kwarg(self) -> None:
sess_options = ort.SessionOptions() sess_options = ort.SessionOptions()
encoder = OpenCLIPEncoder( encoder = OpenClipTextualEncoder(
"ViT-B-32__openai", "ViT-B-32__openai",
providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"], providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"],
provider_options=[], provider_options=[],
@@ -137,43 +145,43 @@ class TestBase:
assert sess_options is encoder.sess_options assert sess_options is encoder.sess_options
def test_sets_default_cache_dir(self) -> None: def test_sets_default_cache_dir(self) -> None:
encoder = OpenCLIPEncoder("ViT-B-32__openai") encoder = OpenClipTextualEncoder("ViT-B-32__openai")
assert encoder.cache_dir == Path(settings.cache_folder) / "clip" / "ViT-B-32__openai" assert encoder.cache_dir == Path(settings.cache_folder) / "clip" / "ViT-B-32__openai"
def test_sets_cache_dir_kwarg(self) -> None: def test_sets_cache_dir_kwarg(self) -> None:
cache_dir = Path("/test_cache") cache_dir = Path("/test_cache")
encoder = OpenCLIPEncoder("ViT-B-32__openai", cache_dir=cache_dir) encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir=cache_dir)
assert encoder.cache_dir == cache_dir assert encoder.cache_dir == cache_dir
def test_sets_default_preferred_runtime(self, mocker: MockerFixture) -> None: def test_sets_default_preferred_format(self, mocker: MockerFixture) -> None:
mocker.patch.object(settings, "ann", True) mocker.patch.object(settings, "ann", True)
mocker.patch("ann.ann.is_available", False) mocker.patch("ann.ann.is_available", False)
encoder = OpenCLIPEncoder("ViT-B-32__openai") encoder = OpenClipTextualEncoder("ViT-B-32__openai")
assert encoder.preferred_runtime == ModelRuntime.ONNX assert encoder.preferred_format == ModelFormat.ONNX
def test_sets_default_preferred_runtime_to_armnn_if_available(self, mocker: MockerFixture) -> None: def test_sets_default_preferred_format_to_armnn_if_available(self, mocker: MockerFixture) -> None:
mocker.patch.object(settings, "ann", True) mocker.patch.object(settings, "ann", True)
mocker.patch("ann.ann.is_available", True) mocker.patch("ann.ann.is_available", True)
encoder = OpenCLIPEncoder("ViT-B-32__openai") encoder = OpenClipTextualEncoder("ViT-B-32__openai")
assert encoder.preferred_runtime == ModelRuntime.ARMNN assert encoder.preferred_format == ModelFormat.ARMNN
def test_sets_preferred_runtime_kwarg(self, mocker: MockerFixture) -> None: def test_sets_preferred_format_kwarg(self, mocker: MockerFixture) -> None:
mocker.patch.object(settings, "ann", False) mocker.patch.object(settings, "ann", False)
mocker.patch("ann.ann.is_available", False) mocker.patch("ann.ann.is_available", False)
encoder = OpenCLIPEncoder("ViT-B-32__openai", preferred_runtime=ModelRuntime.ARMNN) encoder = OpenClipTextualEncoder("ViT-B-32__openai", preferred_format=ModelFormat.ARMNN)
assert encoder.preferred_runtime == ModelRuntime.ARMNN assert encoder.preferred_format == ModelFormat.ARMNN
def test_casts_cache_dir_string_to_path(self) -> None: def test_casts_cache_dir_string_to_path(self) -> None:
cache_dir = "/test_cache" cache_dir = "/test_cache"
encoder = OpenCLIPEncoder("ViT-B-32__openai", cache_dir=cache_dir) encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir=cache_dir)
assert encoder.cache_dir == Path(cache_dir) assert encoder.cache_dir == Path(cache_dir)
@@ -186,7 +194,7 @@ class TestBase:
mocker.patch("app.models.base.Path", return_value=mock_cache_dir) mocker.patch("app.models.base.Path", return_value=mock_cache_dir)
info = mocker.spy(log, "info") info = mocker.spy(log, "info")
encoder = OpenCLIPEncoder("ViT-B-32__openai", cache_dir=mock_cache_dir) encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir=mock_cache_dir)
encoder.clear_cache() encoder.clear_cache()
mock_rmtree.assert_called_once_with(encoder.cache_dir) mock_rmtree.assert_called_once_with(encoder.cache_dir)
@@ -201,7 +209,7 @@ class TestBase:
mocker.patch("app.models.base.Path", return_value=mock_cache_dir) mocker.patch("app.models.base.Path", return_value=mock_cache_dir)
warning = mocker.spy(log, "warning") warning = mocker.spy(log, "warning")
encoder = OpenCLIPEncoder("ViT-B-32__openai", cache_dir=mock_cache_dir) encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir=mock_cache_dir)
encoder.clear_cache() encoder.clear_cache()
mock_rmtree.assert_not_called() mock_rmtree.assert_not_called()
@@ -215,7 +223,7 @@ class TestBase:
mock_cache_dir.is_dir.return_value = True mock_cache_dir.is_dir.return_value = True
mocker.patch("app.models.base.Path", return_value=mock_cache_dir) mocker.patch("app.models.base.Path", return_value=mock_cache_dir)
encoder = OpenCLIPEncoder("ViT-B-32__openai", cache_dir=mock_cache_dir) encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir=mock_cache_dir)
with pytest.raises(RuntimeError): with pytest.raises(RuntimeError):
encoder.clear_cache() encoder.clear_cache()
@@ -230,7 +238,7 @@ class TestBase:
mocker.patch("app.models.base.Path", return_value=mock_cache_dir) mocker.patch("app.models.base.Path", return_value=mock_cache_dir)
warning = mocker.spy(log, "warning") warning = mocker.spy(log, "warning")
encoder = OpenCLIPEncoder("ViT-B-32__openai", cache_dir=mock_cache_dir) encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir=mock_cache_dir)
encoder.clear_cache() encoder.clear_cache()
mock_rmtree.assert_not_called() mock_rmtree.assert_not_called()
@@ -245,7 +253,7 @@ class TestBase:
mock_model_path.with_suffix.return_value = mock_model_path mock_model_path.with_suffix.return_value = mock_model_path
mock_ann = mocker.patch("app.models.base.AnnSession") mock_ann = mocker.patch("app.models.base.AnnSession")
encoder = OpenCLIPEncoder("ViT-B-32__openai") encoder = OpenClipTextualEncoder("ViT-B-32__openai")
encoder._make_session(mock_model_path) encoder._make_session(mock_model_path)
mock_ann.assert_called_once() mock_ann.assert_called_once()
@@ -263,7 +271,7 @@ class TestBase:
mock_ann = mocker.patch("app.models.base.AnnSession") mock_ann = mocker.patch("app.models.base.AnnSession")
mock_ort = mocker.patch("app.models.base.ort.InferenceSession") mock_ort = mocker.patch("app.models.base.ort.InferenceSession")
encoder = OpenCLIPEncoder("ViT-B-32__openai") encoder = OpenClipTextualEncoder("ViT-B-32__openai")
encoder._make_session(mock_armnn_path) encoder._make_session(mock_armnn_path)
mock_ort.assert_called_once() mock_ort.assert_called_once()
@@ -277,7 +285,7 @@ class TestBase:
mock_ann = mocker.patch("app.models.base.AnnSession") mock_ann = mocker.patch("app.models.base.AnnSession")
mock_ort = mocker.patch("app.models.base.ort.InferenceSession") mock_ort = mocker.patch("app.models.base.ort.InferenceSession")
encoder = OpenCLIPEncoder("ViT-B-32__openai") encoder = OpenClipTextualEncoder("ViT-B-32__openai")
with pytest.raises(ValueError): with pytest.raises(ValueError):
encoder._make_session(mock_model_path) encoder._make_session(mock_model_path)
@@ -287,7 +295,7 @@ class TestBase:
def test_download(self, mocker: MockerFixture) -> None: def test_download(self, mocker: MockerFixture) -> None:
mock_snapshot_download = mocker.patch("app.models.base.snapshot_download") mock_snapshot_download = mocker.patch("app.models.base.snapshot_download")
encoder = OpenCLIPEncoder("ViT-B-32__openai", cache_dir="/path/to/cache") encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir="/path/to/cache")
encoder.download() encoder.download()
mock_snapshot_download.assert_called_once_with( mock_snapshot_download.assert_called_once_with(
@@ -298,10 +306,10 @@ class TestBase:
ignore_patterns=["*.armnn"], ignore_patterns=["*.armnn"],
) )
def test_download_downloads_armnn_if_preferred_runtime(self, mocker: MockerFixture) -> None: def test_download_downloads_armnn_if_preferred_format(self, mocker: MockerFixture) -> None:
mock_snapshot_download = mocker.patch("app.models.base.snapshot_download") mock_snapshot_download = mocker.patch("app.models.base.snapshot_download")
encoder = OpenCLIPEncoder("ViT-B-32__openai", preferred_runtime=ModelRuntime.ARMNN) encoder = OpenClipTextualEncoder("ViT-B-32__openai", preferred_format=ModelFormat.ARMNN)
encoder.download() encoder.download()
mock_snapshot_download.assert_called_once_with( mock_snapshot_download.assert_called_once_with(
@@ -323,21 +331,17 @@ class TestCLIP:
mocker: MockerFixture, mocker: MockerFixture,
clip_model_cfg: dict[str, Any], clip_model_cfg: dict[str, Any],
clip_preprocess_cfg: Callable[[Path], dict[str, Any]], clip_preprocess_cfg: Callable[[Path], dict[str, Any]],
clip_tokenizer_cfg: Callable[[Path], dict[str, Any]],
) -> None: ) -> None:
mocker.patch.object(OpenCLIPEncoder, "download") mocker.patch.object(OpenClipVisualEncoder, "download")
mocker.patch.object(OpenCLIPEncoder, "model_cfg", clip_model_cfg) mocker.patch.object(OpenClipVisualEncoder, "model_cfg", clip_model_cfg)
mocker.patch.object(OpenCLIPEncoder, "preprocess_cfg", clip_preprocess_cfg) mocker.patch.object(OpenClipVisualEncoder, "preprocess_cfg", clip_preprocess_cfg)
mocker.patch.object(OpenCLIPEncoder, "tokenizer_cfg", clip_tokenizer_cfg)
mocked = mocker.patch.object(InferenceModel, "_make_session", autospec=True).return_value mocked = mocker.patch.object(InferenceModel, "_make_session", autospec=True).return_value
mocked.run.return_value = [[self.embedding]] mocked.run.return_value = [[self.embedding]]
mocker.patch("app.models.clip.Tokenizer.from_file", autospec=True)
clip_encoder = OpenCLIPEncoder("ViT-B-32__openai", cache_dir="test_cache", mode="vision") clip_encoder = OpenClipVisualEncoder("ViT-B-32__openai", cache_dir="test_cache")
embedding = clip_encoder.predict(pil_image) embedding = clip_encoder.predict(pil_image)
assert clip_encoder.mode == "vision"
assert isinstance(embedding, np.ndarray) assert isinstance(embedding, np.ndarray)
assert embedding.shape[0] == clip_model_cfg["embed_dim"] assert embedding.shape[0] == clip_model_cfg["embed_dim"]
assert embedding.dtype == np.float32 assert embedding.dtype == np.float32
@@ -347,22 +351,19 @@ class TestCLIP:
self, self,
mocker: MockerFixture, mocker: MockerFixture,
clip_model_cfg: dict[str, Any], clip_model_cfg: dict[str, Any],
clip_preprocess_cfg: Callable[[Path], dict[str, Any]],
clip_tokenizer_cfg: Callable[[Path], dict[str, Any]], clip_tokenizer_cfg: Callable[[Path], dict[str, Any]],
) -> None: ) -> None:
mocker.patch.object(OpenCLIPEncoder, "download") mocker.patch.object(OpenClipTextualEncoder, "download")
mocker.patch.object(OpenCLIPEncoder, "model_cfg", clip_model_cfg) mocker.patch.object(OpenClipTextualEncoder, "model_cfg", clip_model_cfg)
mocker.patch.object(OpenCLIPEncoder, "preprocess_cfg", clip_preprocess_cfg) mocker.patch.object(OpenClipTextualEncoder, "tokenizer_cfg", clip_tokenizer_cfg)
mocker.patch.object(OpenCLIPEncoder, "tokenizer_cfg", clip_tokenizer_cfg)
mocked = mocker.patch.object(InferenceModel, "_make_session", autospec=True).return_value mocked = mocker.patch.object(InferenceModel, "_make_session", autospec=True).return_value
mocked.run.return_value = [[self.embedding]] mocked.run.return_value = [[self.embedding]]
mocker.patch("app.models.clip.Tokenizer.from_file", autospec=True) mocker.patch("app.models.clip.textual.Tokenizer.from_file", autospec=True)
clip_encoder = OpenCLIPEncoder("ViT-B-32__openai", cache_dir="test_cache", mode="text") clip_encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir="test_cache")
embedding = clip_encoder.predict("test search query") embedding = clip_encoder.predict("test search query")
assert clip_encoder.mode == "text"
assert isinstance(embedding, np.ndarray) assert isinstance(embedding, np.ndarray)
assert embedding.shape[0] == clip_model_cfg["embed_dim"] assert embedding.shape[0] == clip_model_cfg["embed_dim"]
assert embedding.dtype == np.float32 assert embedding.dtype == np.float32
@@ -372,19 +373,18 @@ class TestCLIP:
self, self,
mocker: MockerFixture, mocker: MockerFixture,
clip_model_cfg: dict[str, Any], clip_model_cfg: dict[str, Any],
clip_preprocess_cfg: Callable[[Path], dict[str, Any]],
clip_tokenizer_cfg: Callable[[Path], dict[str, Any]], clip_tokenizer_cfg: Callable[[Path], dict[str, Any]],
) -> None: ) -> None:
mocker.patch.object(OpenCLIPEncoder, "download") mocker.patch.object(OpenClipTextualEncoder, "download")
mocker.patch.object(OpenCLIPEncoder, "model_cfg", clip_model_cfg) mocker.patch.object(OpenClipTextualEncoder, "model_cfg", clip_model_cfg)
mocker.patch.object(OpenCLIPEncoder, "preprocess_cfg", clip_preprocess_cfg) mocker.patch.object(OpenClipTextualEncoder, "tokenizer_cfg", clip_tokenizer_cfg)
mocker.patch.object(OpenCLIPEncoder, "tokenizer_cfg", clip_tokenizer_cfg) mocker.patch.object(InferenceModel, "_make_session", autospec=True).return_value
mock_tokenizer = mocker.patch("app.models.clip.Tokenizer.from_file", autospec=True).return_value mock_tokenizer = mocker.patch("app.models.clip.textual.Tokenizer.from_file", autospec=True).return_value
mock_ids = [randint(0, 50000) for _ in range(77)] mock_ids = [randint(0, 50000) for _ in range(77)]
mock_tokenizer.encode.return_value = SimpleNamespace(ids=mock_ids) mock_tokenizer.encode.return_value = SimpleNamespace(ids=mock_ids)
clip_encoder = OpenCLIPEncoder("ViT-B-32__openai", cache_dir="test_cache", mode="text") clip_encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir="test_cache")
clip_encoder._load_tokenizer() clip_encoder._load()
tokens = clip_encoder.tokenize("test search query") tokens = clip_encoder.tokenize("test search query")
assert "text" in tokens assert "text" in tokens
@@ -397,20 +397,19 @@ class TestCLIP:
self, self,
mocker: MockerFixture, mocker: MockerFixture,
clip_model_cfg: dict[str, Any], clip_model_cfg: dict[str, Any],
clip_preprocess_cfg: Callable[[Path], dict[str, Any]],
clip_tokenizer_cfg: Callable[[Path], dict[str, Any]], clip_tokenizer_cfg: Callable[[Path], dict[str, Any]],
) -> None: ) -> None:
mocker.patch.object(OpenCLIPEncoder, "download") mocker.patch.object(MClipTextualEncoder, "download")
mocker.patch.object(OpenCLIPEncoder, "model_cfg", clip_model_cfg) mocker.patch.object(MClipTextualEncoder, "model_cfg", clip_model_cfg)
mocker.patch.object(OpenCLIPEncoder, "preprocess_cfg", clip_preprocess_cfg) mocker.patch.object(MClipTextualEncoder, "tokenizer_cfg", clip_tokenizer_cfg)
mocker.patch.object(OpenCLIPEncoder, "tokenizer_cfg", clip_tokenizer_cfg) mocker.patch.object(InferenceModel, "_make_session", autospec=True).return_value
mock_tokenizer = mocker.patch("app.models.clip.Tokenizer.from_file", autospec=True).return_value mock_tokenizer = mocker.patch("app.models.clip.textual.Tokenizer.from_file", autospec=True).return_value
mock_ids = [randint(0, 50000) for _ in range(77)] mock_ids = [randint(0, 50000) for _ in range(77)]
mock_attention_mask = [randint(0, 1) for _ in range(77)] mock_attention_mask = [randint(0, 1) for _ in range(77)]
mock_tokenizer.encode.return_value = SimpleNamespace(ids=mock_ids, attention_mask=mock_attention_mask) mock_tokenizer.encode.return_value = SimpleNamespace(ids=mock_ids, attention_mask=mock_attention_mask)
clip_encoder = MCLIPEncoder("ViT-B-32__openai", cache_dir="test_cache", mode="text") clip_encoder = MClipTextualEncoder("ViT-B-32__openai", cache_dir="test_cache")
clip_encoder._load_tokenizer() clip_encoder._load()
tokens = clip_encoder.tokenize("test search query") tokens = clip_encoder.tokenize("test search query")
assert "input_ids" in tokens assert "input_ids" in tokens
@@ -430,59 +429,90 @@ class TestFaceRecognition:
assert face_recognizer.min_score == 0.5 assert face_recognizer.min_score == 0.5
def test_basic(self, cv_image: cv2.Mat, mocker: MockerFixture) -> None: def test_detection(self, cv_image: cv2.Mat, mocker: MockerFixture) -> None:
mocker.patch.object(FaceRecognizer, "load") mocker.patch.object(FaceDetector, "load")
face_recognizer = FaceRecognizer("buffalo_s", min_score=0.0, cache_dir="test_cache") face_detector = FaceDetector("buffalo_s", min_score=0.0, cache_dir="test_cache")
det_model = mock.Mock() det_model = mock.Mock()
num_faces = 2 num_faces = 2
bbox = np.random.rand(num_faces, 4).astype(np.float32) bbox = np.random.rand(num_faces, 4).astype(np.float32)
score = np.array([[0.67]] * num_faces).astype(np.float32) scores = np.array([[0.67]] * num_faces).astype(np.float32)
kpss = np.random.rand(num_faces, 5, 2).astype(np.float32) kpss = np.random.rand(num_faces, 5, 2).astype(np.float32)
det_model.detect.return_value = (np.concatenate([bbox, score], axis=-1), kpss) det_model.detect.return_value = (np.concatenate([bbox, scores], axis=-1), kpss)
face_recognizer.det_model = det_model face_detector.model = det_model
faces = face_detector.predict(cv_image)
assert isinstance(faces, dict)
assert isinstance(faces.get("boxes", None), np.ndarray)
assert isinstance(faces.get("landmarks", None), np.ndarray)
assert isinstance(faces.get("scores", None), np.ndarray)
assert np.equal(faces["boxes"], bbox.round()).all()
assert np.equal(faces["landmarks"], kpss).all()
assert np.equal(faces["scores"], scores).all()
det_model.detect.assert_called_once()
def test_recognition(self, cv_image: cv2.Mat, mocker: MockerFixture) -> None:
mocker.patch.object(FaceRecognizer, "load")
face_recognizer = FaceRecognizer("buffalo_s", min_score=0.0, cache_dir="test_cache")
num_faces = 2
bbox = np.random.rand(num_faces, 4).astype(np.float32)
scores = np.array([0.67] * num_faces).astype(np.float32)
kpss = np.random.rand(num_faces, 5, 2).astype(np.float32)
faces = {"boxes": bbox, "landmarks": kpss, "scores": scores}
rec_model = mock.Mock() rec_model = mock.Mock()
embedding = np.random.rand(num_faces, 512).astype(np.float32) embedding = np.random.rand(num_faces, 512).astype(np.float32)
rec_model.get_feat.return_value = embedding rec_model.get_feat.return_value = embedding
face_recognizer.rec_model = rec_model face_recognizer.model = rec_model
faces = face_recognizer.predict(cv_image) faces = face_recognizer.predict(cv_image, faces)
assert isinstance(faces, list)
assert len(faces) == num_faces assert len(faces) == num_faces
for face in faces: for face in faces:
assert face["imageHeight"] == 800 assert isinstance(face.get("boundingBox"), dict)
assert face["imageWidth"] == 600 assert set(face["boundingBox"]) == {"x1", "y1", "x2", "y2"}
assert isinstance(face["embedding"], np.ndarray) assert all(isinstance(val, np.float32) for val in face["boundingBox"].values())
assert isinstance(face.get("embedding"), np.ndarray)
assert face["embedding"].shape[0] == 512 assert face["embedding"].shape[0] == 512
assert face["embedding"].dtype == np.float32 assert isinstance(face.get("score", None), np.float32)
det_model.detect.assert_called_once() rec_model.get_feat.assert_called_once()
assert rec_model.get_feat.call_count == num_faces call_args = rec_model.get_feat.call_args_list[0].args
assert len(call_args) == 1
assert isinstance(call_args[0], list)
assert isinstance(call_args[0][0], np.ndarray)
assert call_args[0][0].shape == (112, 112, 3)
@pytest.mark.asyncio @pytest.mark.asyncio
class TestCache: class TestCache:
async def test_caches(self, mock_get_model: mock.Mock) -> None: async def test_caches(self, mock_get_model: mock.Mock) -> None:
model_cache = ModelCache() model_cache = ModelCache()
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION) await model_cache.get("test_model_name", ModelType.RECOGNITION, ModelTask.FACIAL_RECOGNITION)
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION) await model_cache.get("test_model_name", ModelType.RECOGNITION, ModelTask.FACIAL_RECOGNITION)
assert len(model_cache.cache._cache) == 1 assert len(model_cache.cache._cache) == 1
mock_get_model.assert_called_once() mock_get_model.assert_called_once()
async def test_kwargs_used(self, mock_get_model: mock.Mock) -> None: async def test_kwargs_used(self, mock_get_model: mock.Mock) -> None:
model_cache = ModelCache() model_cache = ModelCache()
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION, cache_dir="test_cache") await model_cache.get(
mock_get_model.assert_called_once_with(ModelType.FACIAL_RECOGNITION, "test_model_name", cache_dir="test_cache") "test_model_name", ModelType.RECOGNITION, ModelTask.FACIAL_RECOGNITION, cache_dir="test_cache"
)
mock_get_model.assert_called_once_with(
"test_model_name", ModelType.RECOGNITION, ModelTask.FACIAL_RECOGNITION, cache_dir="test_cache"
)
async def test_different_clip(self, mock_get_model: mock.Mock) -> None: async def test_different_clip(self, mock_get_model: mock.Mock) -> None:
model_cache = ModelCache() model_cache = ModelCache()
await model_cache.get("test_image_model_name", ModelType.CLIP) await model_cache.get("test_model_name", ModelType.VISUAL, ModelTask.SEARCH)
await model_cache.get("test_text_model_name", ModelType.CLIP) await model_cache.get("test_model_name", ModelType.TEXTUAL, ModelTask.SEARCH)
mock_get_model.assert_has_calls( mock_get_model.assert_has_calls(
[ [
mock.call(ModelType.CLIP, "test_image_model_name"), mock.call("test_model_name", ModelType.VISUAL, ModelTask.SEARCH),
mock.call(ModelType.CLIP, "test_text_model_name"), mock.call("test_model_name", ModelType.TEXTUAL, ModelTask.SEARCH),
] ]
) )
assert len(model_cache.cache._cache) == 2 assert len(model_cache.cache._cache) == 2
@@ -490,19 +520,19 @@ class TestCache:
@mock.patch("app.models.cache.OptimisticLock", autospec=True) @mock.patch("app.models.cache.OptimisticLock", autospec=True)
async def test_model_ttl(self, mock_lock_cls: mock.Mock, mock_get_model: mock.Mock) -> None: async def test_model_ttl(self, mock_lock_cls: mock.Mock, mock_get_model: mock.Mock) -> None:
model_cache = ModelCache() model_cache = ModelCache()
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION, ttl=100) await model_cache.get("test_model_name", ModelType.RECOGNITION, ModelTask.FACIAL_RECOGNITION, ttl=100)
mock_lock_cls.return_value.__aenter__.return_value.cas.assert_called_with(mock.ANY, ttl=100) mock_lock_cls.return_value.__aenter__.return_value.cas.assert_called_with(mock.ANY, ttl=100)
@mock.patch("app.models.cache.SimpleMemoryCache.expire") @mock.patch("app.models.cache.SimpleMemoryCache.expire")
async def test_revalidate_get(self, mock_cache_expire: mock.Mock, mock_get_model: mock.Mock) -> None: async def test_revalidate_get(self, mock_cache_expire: mock.Mock, mock_get_model: mock.Mock) -> None:
model_cache = ModelCache(revalidate=True) model_cache = ModelCache(revalidate=True)
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION, ttl=100) await model_cache.get("test_model_name", ModelType.RECOGNITION, ModelTask.FACIAL_RECOGNITION, ttl=100)
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION, ttl=100) await model_cache.get("test_model_name", ModelType.RECOGNITION, ModelTask.FACIAL_RECOGNITION, ttl=100)
mock_cache_expire.assert_called_once_with(mock.ANY, 100) mock_cache_expire.assert_called_once_with(mock.ANY, 100)
async def test_profiling(self, mock_get_model: mock.Mock) -> None: async def test_profiling(self, mock_get_model: mock.Mock) -> None:
model_cache = ModelCache(profiling=True) model_cache = ModelCache(profiling=True)
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION, ttl=100) await model_cache.get("test_model_name", ModelType.RECOGNITION, ModelTask.FACIAL_RECOGNITION, ttl=100)
profiling = await model_cache.get_profiling() profiling = await model_cache.get_profiling()
assert isinstance(profiling, dict) assert isinstance(profiling, dict)
assert profiling == model_cache.cache.profiling assert profiling == model_cache.cache.profiling
@@ -510,9 +540,9 @@ class TestCache:
async def test_loads_mclip(self) -> None: async def test_loads_mclip(self) -> None:
model_cache = ModelCache() model_cache = ModelCache()
model = await model_cache.get("XLM-Roberta-Large-Vit-B-32", ModelType.CLIP, mode="text") model = await model_cache.get("XLM-Roberta-Large-Vit-B-32", ModelType.TEXTUAL, ModelTask.SEARCH)
assert isinstance(model, MCLIPEncoder) assert isinstance(model, MClipTextualEncoder)
assert model.model_name == "XLM-Roberta-Large-Vit-B-32" assert model.model_name == "XLM-Roberta-Large-Vit-B-32"
async def test_raises_exception_if_invalid_model_type(self) -> None: async def test_raises_exception_if_invalid_model_type(self) -> None:
@@ -520,15 +550,55 @@ class TestCache:
model_cache = ModelCache() model_cache = ModelCache()
with pytest.raises(ValueError): with pytest.raises(ValueError):
await model_cache.get("XLM-Roberta-Large-Vit-B-32", invalid, mode="text") await model_cache.get("XLM-Roberta-Large-Vit-B-32", ModelType.TEXTUAL, invalid)
async def test_raises_exception_if_unknown_model_name(self) -> None: async def test_raises_exception_if_unknown_model_name(self) -> None:
model_cache = ModelCache() model_cache = ModelCache()
with pytest.raises(ValueError): with pytest.raises(ValueError):
await model_cache.get("test_model_name", ModelType.CLIP, mode="text") await model_cache.get("test_model_name", ModelType.TEXTUAL, ModelTask.SEARCH)
async def test_preloads_models(self, monkeypatch: MonkeyPatch, mock_get_model: mock.Mock) -> None: async def test_preloads_clip_models(self, monkeypatch: MonkeyPatch, mock_get_model: mock.Mock) -> None:
os.environ["MACHINE_LEARNING_PRELOAD__CLIP"] = "ViT-B-32__openai"
settings = Settings()
assert settings.preload is not None
assert settings.preload.clip == "ViT-B-32__openai"
model_cache = ModelCache()
monkeypatch.setattr("app.main.model_cache", model_cache)
await preload_models(settings.preload)
mock_get_model.assert_has_calls(
[
mock.call("ViT-B-32__openai", ModelType.TEXTUAL, ModelTask.SEARCH),
mock.call("ViT-B-32__openai", ModelType.VISUAL, ModelTask.SEARCH),
],
any_order=True,
)
async def test_preloads_facial_recognition_models(
self, monkeypatch: MonkeyPatch, mock_get_model: mock.Mock
) -> None:
os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION"] = "buffalo_s"
settings = Settings()
assert settings.preload is not None
assert settings.preload.facial_recognition == "buffalo_s"
model_cache = ModelCache()
monkeypatch.setattr("app.main.model_cache", model_cache)
await preload_models(settings.preload)
mock_get_model.assert_has_calls(
[
mock.call("buffalo_s", ModelType.DETECTION, ModelTask.FACIAL_RECOGNITION),
mock.call("buffalo_s", ModelType.RECOGNITION, ModelTask.FACIAL_RECOGNITION),
],
any_order=True,
)
async def test_preloads_all_models(self, monkeypatch: MonkeyPatch, mock_get_model: mock.Mock) -> None:
os.environ["MACHINE_LEARNING_PRELOAD__CLIP"] = "ViT-B-32__openai" os.environ["MACHINE_LEARNING_PRELOAD__CLIP"] = "ViT-B-32__openai"
os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION"] = "buffalo_s" os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION"] = "buffalo_s"
@@ -541,11 +611,15 @@ class TestCache:
monkeypatch.setattr("app.main.model_cache", model_cache) monkeypatch.setattr("app.main.model_cache", model_cache)
await preload_models(settings.preload) await preload_models(settings.preload)
assert len(model_cache.cache._cache) == 2 mock_get_model.assert_has_calls(
assert mock_get_model.call_count == 2 [
await model_cache.get("ViT-B-32__openai", ModelType.CLIP, ttl=100) mock.call("ViT-B-32__openai", ModelType.TEXTUAL, ModelTask.SEARCH),
await model_cache.get("buffalo_s", ModelType.FACIAL_RECOGNITION, ttl=100) mock.call("ViT-B-32__openai", ModelType.VISUAL, ModelTask.SEARCH),
assert mock_get_model.call_count == 2 mock.call("buffalo_s", ModelType.DETECTION, ModelTask.FACIAL_RECOGNITION),
mock.call("buffalo_s", ModelType.RECOGNITION, ModelTask.FACIAL_RECOGNITION),
],
any_order=True,
)
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -572,7 +646,8 @@ class TestLoad:
async def test_load_clears_cache_and_retries_if_os_error(self) -> None: async def test_load_clears_cache_and_retries_if_os_error(self) -> None:
mock_model = mock.Mock(spec=InferenceModel) mock_model = mock.Mock(spec=InferenceModel)
mock_model.model_name = "test_model_name" mock_model.model_name = "test_model_name"
mock_model.model_type = ModelType.CLIP mock_model.model_type = ModelType.VISUAL
mock_model.model_task = ModelTask.SEARCH
mock_model.load.side_effect = [OSError, None] mock_model.load.side_effect = [OSError, None]
mock_model.loaded = False mock_model.loaded = False
@@ -597,13 +672,15 @@ class TestEndpoints:
response = deployed_app.post( response = deployed_app.post(
"http://localhost:3003/predict", "http://localhost:3003/predict",
data={"modelName": "ViT-B-32__openai", "modelType": "clip", "options": json.dumps({"mode": "vision"})}, data={"entries": json.dumps({"clip": {"visual": {"modelName": "ViT-B-32__openai"}}})},
files={"image": byte_image.getvalue()}, files={"image": byte_image.getvalue()},
) )
actual = response.json() actual = response.json()
assert response.status_code == 200 assert response.status_code == 200
assert np.allclose(expected, actual) assert isinstance(actual, dict)
assert isinstance(actual.get("clip", None), list)
assert np.allclose(expected, actual["clip"])
def test_clip_text_endpoint(self, responses: dict[str, Any], deployed_app: TestClient) -> None: def test_clip_text_endpoint(self, responses: dict[str, Any], deployed_app: TestClient) -> None:
expected = responses["clip"]["text"] expected = responses["clip"]["text"]
@@ -611,38 +688,49 @@ class TestEndpoints:
response = deployed_app.post( response = deployed_app.post(
"http://localhost:3003/predict", "http://localhost:3003/predict",
data={ data={
"modelName": "ViT-B-32__openai", "entries": json.dumps(
"modelType": "clip", {
"clip": {"textual": {"modelName": "ViT-B-32__openai"}},
},
),
"text": "test search query", "text": "test search query",
"options": json.dumps({"mode": "text"}),
}, },
) )
actual = response.json() actual = response.json()
assert response.status_code == 200 assert response.status_code == 200
assert np.allclose(expected, actual) assert isinstance(actual, dict)
assert isinstance(actual.get("clip", None), list)
assert np.allclose(expected, actual["clip"])
def test_face_endpoint(self, pil_image: Image.Image, responses: dict[str, Any], deployed_app: TestClient) -> None: def test_face_endpoint(self, pil_image: Image.Image, responses: dict[str, Any], deployed_app: TestClient) -> None:
byte_image = BytesIO() byte_image = BytesIO()
pil_image.save(byte_image, format="jpeg") pil_image.save(byte_image, format="jpeg")
expected = responses["facial-recognition"]
response = deployed_app.post( response = deployed_app.post(
"http://localhost:3003/predict", "http://localhost:3003/predict",
data={ data={
"modelName": "buffalo_l", "entries": json.dumps(
"modelType": "facial-recognition", {
"options": json.dumps({"minScore": 0.034}), "facial-recognition": {
"detection": {"modelName": "buffalo_l", "options": {"minScore": 0.034}},
"recognition": {"modelName": "buffalo_l"},
}
}
)
}, },
files={"image": byte_image.getvalue()}, files={"image": byte_image.getvalue()},
) )
actual = response.json() actual = response.json()
assert response.status_code == 200 assert response.status_code == 200
assert len(expected) == len(actual) assert isinstance(actual, dict)
for expected_face, actual_face in zip(expected, actual): assert actual.get("imageHeight", None) == responses["imageHeight"]
assert expected_face["imageHeight"] == actual_face["imageHeight"] assert actual.get("imageWidth", None) == responses["imageWidth"]
assert expected_face["imageWidth"] == actual_face["imageWidth"] assert "facial-recognition" in actual and isinstance(actual["facial-recognition"], list)
assert len(actual["facial-recognition"]) == len(responses["facial-recognition"])
for expected_face, actual_face in zip(responses["facial-recognition"], actual["facial-recognition"]):
assert expected_face["boundingBox"] == actual_face["boundingBox"] assert expected_face["boundingBox"] == actual_face["boundingBox"]
assert np.allclose(expected_face["embedding"], actual_face["embedding"]) assert np.allclose(expected_face["embedding"], actual_face["embedding"])
assert np.allclose(expected_face["score"], actual_face["score"]) assert np.allclose(expected_face["score"], actual_face["score"])
+1 -1
View File
@@ -1,4 +1,4 @@
FROM mambaorg/micromamba:bookworm-slim@sha256:d889b18dcb737911bb2628a649bcbd0dc42556148b0ece298021343bc66cf84a as builder FROM mambaorg/micromamba:bookworm-slim@sha256:4688551ffd61358d5bebfd88e0aac12d5b4aed7a153c170dbc435da453476a13 as builder
ENV TRANSFORMERS_CACHE=/cache \ ENV TRANSFORMERS_CACHE=/cache \
PYTHONDONTWRITEBYTECODE=1 \ PYTHONDONTWRITEBYTECODE=1 \
+16 -19
View File
@@ -37,7 +37,6 @@ def on_test_start(environment: Environment, **kwargs: Any) -> None:
global byte_image global byte_image
assert environment.parsed_options is not None assert environment.parsed_options is not None
image = Image.new("RGB", (environment.parsed_options.image_size, environment.parsed_options.image_size)) image = Image.new("RGB", (environment.parsed_options.image_size, environment.parsed_options.image_size))
byte_image = BytesIO()
image.save(byte_image, format="jpeg") image.save(byte_image, format="jpeg")
@@ -45,34 +44,25 @@ class InferenceLoadTest(HttpUser):
abstract: bool = True abstract: bool = True
host = "http://127.0.0.1:3003" host = "http://127.0.0.1:3003"
data: bytes data: bytes
headers: dict[str, str] = {"Content-Type": "image/jpg"}
# re-use the image across all instances in a process # re-use the image across all instances in a process
def on_start(self) -> None: def on_start(self) -> None:
global byte_image
self.data = byte_image.getvalue() self.data = byte_image.getvalue()
class CLIPTextFormDataLoadTest(InferenceLoadTest): class CLIPTextFormDataLoadTest(InferenceLoadTest):
@task @task
def encode_text(self) -> None: def encode_text(self) -> None:
data = [ request = {"clip": {"textual": {"modelName": self.environment.parsed_options.clip_model}}}
("modelName", self.environment.parsed_options.clip_model), data = [("entries", json.dumps(request)), ("text", "test search query")]
("modelType", "clip"),
("options", json.dumps({"mode": "text"})),
("text", "test search query"),
]
self.client.post("/predict", data=data) self.client.post("/predict", data=data)
class CLIPVisionFormDataLoadTest(InferenceLoadTest): class CLIPVisionFormDataLoadTest(InferenceLoadTest):
@task @task
def encode_image(self) -> None: def encode_image(self) -> None:
data = [ request = {"clip": {"visual": {"modelName": self.environment.parsed_options.clip_model, "options": {}}}}
("modelName", self.environment.parsed_options.clip_model), data = [("entries", json.dumps(request))]
("modelType", "clip"),
("options", json.dumps({"mode": "vision"})),
]
files = {"image": self.data} files = {"image": self.data}
self.client.post("/predict", data=data, files=files) self.client.post("/predict", data=data, files=files)
@@ -80,11 +70,18 @@ class CLIPVisionFormDataLoadTest(InferenceLoadTest):
class RecognitionFormDataLoadTest(InferenceLoadTest): class RecognitionFormDataLoadTest(InferenceLoadTest):
@task @task
def recognize(self) -> None: def recognize(self) -> None:
data = [ request = {
("modelName", self.environment.parsed_options.face_model), "facial-recognition": {
("modelType", "facial-recognition"), "recognition": {
("options", json.dumps({"minScore": self.environment.parsed_options.face_min_score})), "modelName": self.environment.parsed_options.face_model,
] "options": {"minScore": self.environment.parsed_options.face_min_score},
},
"detection": {
"modelName": self.environment.parsed_options.face_model,
},
}
}
data = [("entries", json.dumps(request))]
files = {"image": self.data} files = {"image": self.data}
self.client.post("/predict", data=data, files=files) self.client.post("/predict", data=data, files=files)
+46 -46
View File
@@ -1236,13 +1236,13 @@ socks = ["socksio (==1.*)"]
[[package]] [[package]]
name = "huggingface-hub" name = "huggingface-hub"
version = "0.23.2" version = "0.23.3"
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
optional = false optional = false
python-versions = ">=3.8.0" python-versions = ">=3.8.0"
files = [ files = [
{file = "huggingface_hub-0.23.2-py3-none-any.whl", hash = "sha256:48727a16e704d409c4bb5913613308499664f22a99743435dc3a13b23c485827"}, {file = "huggingface_hub-0.23.3-py3-none-any.whl", hash = "sha256:22222c41223f1b7c209ae5511d2d82907325a0e3cdbce5f66949d43c598ff3bc"},
{file = "huggingface_hub-0.23.2.tar.gz", hash = "sha256:f6829b62d5fdecb452a76fdbec620cba4c1573655a8d710c1df71735fd9edbd2"}, {file = "huggingface_hub-0.23.3.tar.gz", hash = "sha256:1a1118a0b3dea3bab6c325d71be16f5ffe441d32f3ac7c348d6875911b694b5b"},
] ]
[package.dependencies] [package.dependencies]
@@ -2000,21 +2000,21 @@ sympy = "*"
[[package]] [[package]]
name = "onnxruntime-gpu" name = "onnxruntime-gpu"
version = "1.17.1" version = "1.18.0"
description = "ONNX Runtime is a runtime accelerator for Machine Learning models" description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "onnxruntime_gpu-1.17.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e34ecb2b527ee1265135ae74cd99ea198ff344b8221929a920596a1e461e2bbb"}, {file = "onnxruntime_gpu-1.18.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:039be9a6b7f71c6739e97eec79f4bf240793a7c0c4108a09e0e1a27b4c33dbca"},
{file = "onnxruntime_gpu-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:37786c0f225be90da0a66ca413fe125a925a0900263301cc4dbcad4ff0404673"}, {file = "onnxruntime_gpu-1.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:afd4bc090b9412ab695cb34c05f4f92f88dbb6bd52d9b38658ad0115c50ff653"},
{file = "onnxruntime_gpu-1.17.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:3bde190a683ec84ecf61bd390f3c275d388efe72404633df374c52c557ce6d4d"}, {file = "onnxruntime_gpu-1.18.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:2280f94c6be2717f010a73c30a94c2721af853c6b7110e83afa52d03de6614a8"},
{file = "onnxruntime_gpu-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:5206c84caa770efcc2ca819f71ec007a244ed748ca04e7ff76b86df1a096d2c8"}, {file = "onnxruntime_gpu-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:9e3b4e9a0171e53a71001805b9b0e1a98cbad5a413d795c0e132b0f058b386d6"},
{file = "onnxruntime_gpu-1.17.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0396ec73de565a64509d96dff154f531f8da8023c191f771ceba47a3f4efc266"}, {file = "onnxruntime_gpu-1.18.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:546fbf2dcb7a2830ca69bde0c38665a88a9454e923ebb76bedf85eaed33a6f4a"},
{file = "onnxruntime_gpu-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:8531d4a833c8e978c5ff1de7b3bcc4126bbe58ea71fae54ddce58fe8777cb136"}, {file = "onnxruntime_gpu-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:24146aa670c45734d9b8583cd78bd790363bc8695a3808d129ec913186064e4c"},
{file = "onnxruntime_gpu-1.17.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7b831f9eafd626f3d44955420a4b1b84f9ffcb987712a0ab6a37d1ee9f2f7a45"}, {file = "onnxruntime_gpu-1.18.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:75884c51c2aa47c349de3b5485df7f9573e1b89c607dd55984d3fe40615ef002"},
{file = "onnxruntime_gpu-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:a389334d3797519d4b12077db32b8764f1ce54374d0f89235edc04efe8bc192c"}, {file = "onnxruntime_gpu-1.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:f3b00a7443252dbfbd18ff72bcc2f44066fad9128eaa29bff8b315a834241701"},
{file = "onnxruntime_gpu-1.17.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:27aeaa36385e459b3867577ed7f68c1756de79aa68f57141d4ae2a31c84f6a33"}, {file = "onnxruntime_gpu-1.18.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:14f53bd74ad21c61fee55eca988758e5eec4c39450040c8986ec3a960cb127a8"},
{file = "onnxruntime_gpu-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:8b46094ea348aff6c6494402ac4260e2d2aba0522ae13e1ae29d98a29384ed70"}, {file = "onnxruntime_gpu-1.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:aea5c02b3a0ee6682214a61a2a0467773401b075afdcb41dc2ef595f41c2d185"},
] ]
[package.dependencies] [package.dependencies]
@@ -2054,18 +2054,18 @@ sympy = "*"
[[package]] [[package]]
name = "opencv-python-headless" name = "opencv-python-headless"
version = "4.9.0.80" version = "4.10.0.82"
description = "Wrapper package for OpenCV python bindings." description = "Wrapper package for OpenCV python bindings."
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
files = [ files = [
{file = "opencv-python-headless-4.9.0.80.tar.gz", hash = "sha256:71a4cd8cf7c37122901d8e81295db7fb188730e33a0e40039a4e59c1030b0958"}, {file = "opencv-python-headless-4.10.0.82.tar.gz", hash = "sha256:de9e742c1b9540816fbd115b0b03841d41ed0c65566b0d7a5371f98b131b7e6d"},
{file = "opencv_python_headless-4.9.0.80-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:2ea8a2edc4db87841991b2fbab55fc07b97ecb602e0f47d5d485bd75cee17c1a"}, {file = "opencv_python_headless-4.10.0.82-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:a09ed50ba21cc5bf5d436cb0e784ad09c692d6b1d1454252772f6c8f2c7b4088"},
{file = "opencv_python_headless-4.9.0.80-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0ee54e27be493e8f7850847edae3128e18b540dac1d7b2e4001b8944e11e1c6"}, {file = "opencv_python_headless-4.10.0.82-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:977a5fd21e1fe0d3d2134887db4441f8725abeae95150126302f31fcd9f548fa"},
{file = "opencv_python_headless-4.9.0.80-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57ce2865e8fec431c6f97a81e9faaf23fa5be61011d0a75ccf47a3c0d65fa73d"}, {file = "opencv_python_headless-4.10.0.82-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db4ec6755838b0be12510bfc9ffb014779c612418f11f4f7e6f505c36124a3aa"},
{file = "opencv_python_headless-4.9.0.80-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:976656362d68d9f40a5c66f83901430538002465f7db59142784f3893918f3df"}, {file = "opencv_python_headless-4.10.0.82-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a37fa5276967ecf6eb297295b16b28b7a2eb3b568ca0ee469fb1a5954de298"},
{file = "opencv_python_headless-4.9.0.80-cp37-abi3-win32.whl", hash = "sha256:11e3849d83e6651d4e7699aadda9ec7ed7c38957cbbcb99db074f2a2d2de9670"}, {file = "opencv_python_headless-4.10.0.82-cp37-abi3-win32.whl", hash = "sha256:94736e9b322d13db4768fd35588ad5e8995e78e207263076bfbee18aac835ad5"},
{file = "opencv_python_headless-4.9.0.80-cp37-abi3-win_amd64.whl", hash = "sha256:a8056c2cb37cd65dfcdf4153ca16f7362afcf3a50d600d6bb69c660fc61ee29c"}, {file = "opencv_python_headless-4.10.0.82-cp37-abi3-win_amd64.whl", hash = "sha256:c1822fa23d1641c0249ed5eb906f4c385f7959ff1bd601a776d56b0c18914af4"},
] ]
[package.dependencies] [package.dependencies]
@@ -2438,13 +2438,13 @@ files = [
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "8.2.1" version = "8.2.2"
description = "pytest: simple powerful testing with Python" description = "pytest: simple powerful testing with Python"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"},
{file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"},
] ]
[package.dependencies] [package.dependencies]
@@ -2799,28 +2799,28 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.4.5" version = "0.4.8"
description = "An extremely fast Python linter and code formatter, written in Rust." description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "ruff-0.4.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8f58e615dec58b1a6b291769b559e12fdffb53cc4187160a2fc83250eaf54e96"}, {file = "ruff-0.4.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7663a6d78f6adb0eab270fa9cf1ff2d28618ca3a652b60f2a234d92b9ec89066"},
{file = "ruff-0.4.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:84dd157474e16e3a82745d2afa1016c17d27cb5d52b12e3d45d418bcc6d49264"}, {file = "ruff-0.4.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eeceb78da8afb6de0ddada93112869852d04f1cd0f6b80fe464fd4e35c330913"},
{file = "ruff-0.4.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25f483ad9d50b00e7fd577f6d0305aa18494c6af139bce7319c68a17180087f4"}, {file = "ruff-0.4.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aad360893e92486662ef3be0a339c5ca3c1b109e0134fcd37d534d4be9fb8de3"},
{file = "ruff-0.4.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:63fde3bf6f3ad4e990357af1d30e8ba2730860a954ea9282c95fc0846f5f64af"}, {file = "ruff-0.4.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:284c2e3f3396fb05f5f803c9fffb53ebbe09a3ebe7dda2929ed8d73ded736deb"},
{file = "ruff-0.4.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e3ba4620dee27f76bbcad97067766026c918ba0f2d035c2fc25cbdd04d9c97"}, {file = "ruff-0.4.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7354f921e3fbe04d2a62d46707e569f9315e1a613307f7311a935743c51a764"},
{file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:441dab55c568e38d02bbda68a926a3d0b54f5510095c9de7f95e47a39e0168aa"}, {file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:72584676164e15a68a15778fd1b17c28a519e7a0622161eb2debdcdabdc71883"},
{file = "ruff-0.4.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1169e47e9c4136c997f08f9857ae889d614c5035d87d38fda9b44b4338909cdf"}, {file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9678d5c9b43315f323af2233a04d747409d1e3aa6789620083a82d1066a35199"},
{file = "ruff-0.4.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:755ac9ac2598a941512fc36a9070a13c88d72ff874a9781493eb237ab02d75df"}, {file = "ruff-0.4.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704977a658131651a22b5ebeb28b717ef42ac6ee3b11e91dc87b633b5d83142b"},
{file = "ruff-0.4.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4b02a65985be2b34b170025a8b92449088ce61e33e69956ce4d316c0fe7cce0"}, {file = "ruff-0.4.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05f8d6f0c3cce5026cecd83b7a143dcad503045857bc49662f736437380ad45"},
{file = "ruff-0.4.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:75a426506a183d9201e7e5664de3f6b414ad3850d7625764106f7b6d0486f0a1"}, {file = "ruff-0.4.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ea874950daca5697309d976c9afba830d3bf0ed66887481d6bca1673fc5b66a"},
{file = "ruff-0.4.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6e1b139b45e2911419044237d90b60e472f57285950e1492c757dfc88259bb06"}, {file = "ruff-0.4.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fc95aac2943ddf360376be9aa3107c8cf9640083940a8c5bd824be692d2216dc"},
{file = "ruff-0.4.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a6f29a8221d2e3d85ff0c7b4371c0e37b39c87732c969b4d90f3dad2e721c5b1"}, {file = "ruff-0.4.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:384154a1c3f4bf537bac69f33720957ee49ac8d484bfc91720cc94172026ceed"},
{file = "ruff-0.4.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d6ef817124d72b54cc923f3444828ba24fa45c3164bc9e8f1813db2f3d3a8a11"}, {file = "ruff-0.4.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e9d5ce97cacc99878aa0d084c626a15cd21e6b3d53fd6f9112b7fc485918e1fa"},
{file = "ruff-0.4.5-py3-none-win32.whl", hash = "sha256:aed8166c18b1a169a5d3ec28a49b43340949e400665555b51ee06f22813ef062"}, {file = "ruff-0.4.8-py3-none-win32.whl", hash = "sha256:6d795d7639212c2dfd01991259460101c22aabf420d9b943f153ab9d9706e6a9"},
{file = "ruff-0.4.5-py3-none-win_amd64.whl", hash = "sha256:b0b03c619d2b4350b4a27e34fd2ac64d0dabe1afbf43de57d0f9d8a05ecffa45"}, {file = "ruff-0.4.8-py3-none-win_amd64.whl", hash = "sha256:e14a3a095d07560a9d6769a72f781d73259655919d9b396c650fc98a8157555d"},
{file = "ruff-0.4.5-py3-none-win_arm64.whl", hash = "sha256:9d15de3425f53161b3f5a5658d4522e4eee5ea002bf2ac7aa380743dd9ad5fba"}, {file = "ruff-0.4.8-py3-none-win_arm64.whl", hash = "sha256:14019a06dbe29b608f6b7cbcec300e3170a8d86efaddb7b23405cb7f7dcaf780"},
{file = "ruff-0.4.5.tar.gz", hash = "sha256:286eabd47e7d4d521d199cab84deca135557e6d1e0f0d01c29e757c3cb151b54"}, {file = "ruff-0.4.8.tar.gz", hash = "sha256:16d717b1d57b2e2fd68bd0bf80fb43931b79d05a7131aa477d66fc40fbd86268"},
] ]
[[package]] [[package]]
@@ -3234,13 +3234,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.29.0" version = "0.30.1"
description = "The lightning-fast ASGI server." description = "The lightning-fast ASGI server."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"}, {file = "uvicorn-0.30.1-py3-none-any.whl", hash = "sha256:cd17daa7f3b9d7a24de3617820e634d0933b69eed8e33a516071174427238c81"},
{file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"}, {file = "uvicorn-0.30.1.tar.gz", hash = "sha256:d46cd8e0fd80240baffbcd9ec1012a712938754afcf81bce56c024c1656aece8"},
] ]
[package.dependencies] [package.dependencies]
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "machine-learning" name = "machine-learning"
version = "1.105.1" version = "1.106.3"
description = "" description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"] authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md" readme = "README.md"
+3 -3
View File
@@ -213,8 +213,6 @@
}, },
"facial-recognition": [ "facial-recognition": [
{ {
"imageWidth": 600,
"imageHeight": 800,
"boundingBox": { "boundingBox": {
"x1": 690.0, "x1": 690.0,
"y1": -89.0, "y1": -89.0,
@@ -325,5 +323,7 @@
-0.077056274, 0.002099529 -0.077056274, 0.002099529
] ]
} }
] ],
"imageWidth": 600,
"imageHeight": 800
} }
+17
View File
@@ -0,0 +1,17 @@
#! /usr/bin/env node
const { readFileSync, writeFileSync } = require('node:fs');
const nextVersion = process.argv[2];
if (!nextVersion) {
console.log('Usage: archive-version.js <version>');
process.exit(1);
}
const filename = './docs/static/archived-versions.json';
const oldVersions = JSON.parse(readFileSync(filename));
const newVersions = [
{ label: `v${nextVersion}`, url: `https://v${nextVersion}.archive.immich.app` },
...oldVersions,
];
writeFileSync(filename, JSON.stringify(newVersions, null, 2) + '\n');
+7 -3
View File
@@ -66,10 +66,12 @@ if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
npm --prefix server run build npm --prefix server run build
make open-api make open-api
npm --prefix open-api/typescript-sdk version "$SERVER_PUMP" npm --prefix open-api/typescript-sdk version "$SERVER_PUMP"
npm --prefix web version "$SERVER_PUMP" # TODO use $SERVER_PUMP once we pass 2.2.x
npm --prefix e2e version "$SERVER_PUMP" npm --prefix cli version patch
npm --prefix web i --package-lock-only
npm --prefix cli i --package-lock-only npm --prefix cli i --package-lock-only
npm --prefix web version "$SERVER_PUMP"
npm --prefix web i --package-lock-only
npm --prefix e2e version "$SERVER_PUMP"
npm --prefix e2e i --package-lock-only npm --prefix e2e i --package-lock-only
poetry --directory machine-learning version "$SERVER_PUMP" poetry --directory machine-learning version "$SERVER_PUMP"
fi fi
@@ -83,4 +85,6 @@ sed -i "s/version_number: \"$CURRENT_SERVER\"$/version_number: \"$NEXT_SERVER\"/
sed -i "s/\"android\.injected\.version\.code\" => $CURRENT_MOBILE,/\"android\.injected\.version\.code\" => $NEXT_MOBILE,/" mobile/android/fastlane/Fastfile sed -i "s/\"android\.injected\.version\.code\" => $CURRENT_MOBILE,/\"android\.injected\.version\.code\" => $NEXT_MOBILE,/" mobile/android/fastlane/Fastfile
sed -i "s/^version: $CURRENT_SERVER+$CURRENT_MOBILE$/version: $NEXT_SERVER+$NEXT_MOBILE/" mobile/pubspec.yaml sed -i "s/^version: $CURRENT_SERVER+$CURRENT_MOBILE$/version: $NEXT_SERVER+$NEXT_MOBILE/" mobile/pubspec.yaml
./misc/release/archive-version.js "$NEXT_SERVER"
echo "IMMICH_VERSION=v$NEXT_SERVER" >>"$GITHUB_ENV" echo "IMMICH_VERSION=v$NEXT_SERVER" >>"$GITHUB_ENV"
+2 -2
View File
@@ -35,8 +35,8 @@ platform :android do
task: 'bundle', task: 'bundle',
build_type: 'Release', build_type: 'Release',
properties: { properties: {
"android.injected.version.code" => 140, "android.injected.version.code" => 143,
"android.injected.version.name" => "1.105.1", "android.injected.version.name" => "1.106.3",
} }
) )
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
+3 -3
View File
@@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000374"> <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000381">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="84.292464"> <testcase classname="fastlane.lanes" name="1: bundleRelease" time="52.832426">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="33.336934"> <testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="27.616558">
</testcase> </testcase>
+91 -87
View File
@@ -1,12 +1,12 @@
{ {
"action_common_back": "Back", "action_common_back": "خلف",
"action_common_cancel": "يلغي", "action_common_cancel": "يلغي",
"action_common_clear": "Clear", "action_common_clear": "مسح",
"action_common_confirm": "Confirm", "action_common_confirm": "تأكيد",
"action_common_update": "تحديث", "action_common_update": "تحديث",
"add_to_album_bottom_sheet_added": "تمت الاضافة{album}", "add_to_album_bottom_sheet_added": "تمت الاضافة{album}",
"add_to_album_bottom_sheet_already_exists": "موجودة مسبقا {album}", "add_to_album_bottom_sheet_already_exists": "موجودة مسبقا {album}",
"advanced_settings_log_level_title": "تسجيل مستوى: {}", "advanced_settings_log_level_title": "Log level: {}",
"advanced_settings_prefer_remote_subtitle": "تكون بعض الأجهزة بطيئة للغاية في تحميل الصور المصغرة من الأصول الموجودة على الجهاز. قم بتنشيط هذا الإعداد لتحميل الصور البعيدة بدلاً من ذلك.", "advanced_settings_prefer_remote_subtitle": "تكون بعض الأجهزة بطيئة للغاية في تحميل الصور المصغرة من الأصول الموجودة على الجهاز. قم بتنشيط هذا الإعداد لتحميل الصور البعيدة بدلاً من ذلك.",
"advanced_settings_prefer_remote_title": "تفضل الصور البعيدة", "advanced_settings_prefer_remote_title": "تفضل الصور البعيدة",
"advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
@@ -18,11 +18,11 @@
"album_info_card_backup_album_excluded": "مستبعد", "album_info_card_backup_album_excluded": "مستبعد",
"album_info_card_backup_album_included": "متضمنة", "album_info_card_backup_album_included": "متضمنة",
"album_thumbnail_card_item": "عنصر واحد", "album_thumbnail_card_item": "عنصر واحد",
"album_thumbnail_card_items": "{} عناصر", "album_thumbnail_card_items": "{} items",
"album_thumbnail_card_shared": " · . مشترك", "album_thumbnail_card_shared": " · . مشترك",
"album_thumbnail_owned": "مملوكة", "album_thumbnail_owned": "مملوكة",
"album_thumbnail_shared_by": "مشترك مع", "album_thumbnail_shared_by": "Shared by {}",
"album_viewer_appbar_delete_confirm": "Are you sure you want to delete this album from your account?", "album_viewer_appbar_delete_confirm": "هل أنت متأكد أنك تريد حذف هذا الألبوم من حسابك؟",
"album_viewer_appbar_share_delete": "حذف الألبوم", "album_viewer_appbar_share_delete": "حذف الألبوم",
"album_viewer_appbar_share_err_delete": "فشل في حذف الألبوم", "album_viewer_appbar_share_err_delete": "فشل في حذف الألبوم",
"album_viewer_appbar_share_err_leave": "فشل في ترك الألبوم", "album_viewer_appbar_share_err_leave": "فشل في ترك الألبوم",
@@ -38,20 +38,20 @@
"app_bar_signout_dialog_ok": "نعم", "app_bar_signout_dialog_ok": "نعم",
"app_bar_signout_dialog_title": "خروج", "app_bar_signout_dialog_title": "خروج",
"archive_page_no_archived_assets": "لم يتم العثور على الأصول المؤرشفة", "archive_page_no_archived_assets": "لم يتم العثور على الأصول المؤرشفة",
"archive_page_title": "أرشيف ({})", "archive_page_title": "Archive ({})",
"asset_action_delete_err_read_only": "لا يمكن حذف الأصول ذات للقراءة فقط، وسوف يتم التخطي", "asset_action_delete_err_read_only": "لا يمكن حذف الأصول ذات للقراءة فقط، وسوف يتم التخطي",
"asset_action_share_err_offline": "لا يمكن جلب الأصول غير المتصلة بالإنترنت، وسوف يتم التخطي", "asset_action_share_err_offline": "لا يمكن جلب الأصول غير المتصلة بالإنترنت، وسوف يتم التخطي",
"asset_list_group_by_sub_title": "Group by", "asset_list_group_by_sub_title": "تنظيم بواسطة",
"asset_list_layout_settings_dynamic_layout_title": "تخطيط ديناميكي", "asset_list_layout_settings_dynamic_layout_title": "تخطيط ديناميكي",
"asset_list_layout_settings_group_automatically": "تلقائي", "asset_list_layout_settings_group_automatically": "تلقائي",
"asset_list_layout_settings_group_by": "مجموعة الأصول حسب", "asset_list_layout_settings_group_by": "مجموعة الأصول حسب",
"asset_list_layout_settings_group_by_month": "شهر", "asset_list_layout_settings_group_by_month": "شهر",
"asset_list_layout_settings_group_by_month_day": "شهر + يوم", "asset_list_layout_settings_group_by_month_day": "شهر + يوم",
"asset_list_layout_sub_title": "Layout", "asset_list_layout_sub_title": "تصميم",
"asset_list_settings_subtitle": "إعدادات تخطيط شبكة الصور", "asset_list_settings_subtitle": "إعدادات تخطيط شبكة الصور",
"asset_list_settings_title": "شبكة الصور", "asset_list_settings_title": "شبكة الصور",
"asset_viewer_settings_title": "Asset Viewer", "asset_viewer_settings_title": "عارض الأصول",
"backup_album_selection_page_albums_device": "الألبومات الموجودة على الجهاز ({})", "backup_album_selection_page_albums_device": "Albums on device ({})",
"backup_album_selection_page_albums_tap": "انقر للتضمين، وانقر نقرًا مزدوجًا للاستثناء", "backup_album_selection_page_albums_tap": "انقر للتضمين، وانقر نقرًا مزدوجًا للاستثناء",
"backup_album_selection_page_assets_scatter": "يمكن أن تنتشر الأصول عبر ألبومات متعددة. وبالتالي، يمكن تضمين الألبومات أو استبعادها أثناء عملية النسخ الاحتياطي.", "backup_album_selection_page_assets_scatter": "يمكن أن تنتشر الأصول عبر ألبومات متعددة. وبالتالي، يمكن تضمين الألبومات أو استبعادها أثناء عملية النسخ الاحتياطي.",
"backup_album_selection_page_select_albums": "حدد الألبومات", "backup_album_selection_page_select_albums": "حدد الألبومات",
@@ -60,22 +60,22 @@
"backup_all": "الجميع", "backup_all": "الجميع",
"backup_background_service_backup_failed_message": "فشل في النسخ الاحتياطي للأصول. جارٍ إعادة المحاولة...", "backup_background_service_backup_failed_message": "فشل في النسخ الاحتياطي للأصول. جارٍ إعادة المحاولة...",
"backup_background_service_connection_failed_message": "فشل في الاتصال بالخادم. جارٍ إعادة المحاولة...", "backup_background_service_connection_failed_message": "فشل في الاتصال بالخادم. جارٍ إعادة المحاولة...",
"backup_background_service_current_upload_notification": "تحميل {}", "backup_background_service_current_upload_notification": "Uploading {}",
"backup_background_service_default_notification": "التحقق من الأصول الجديدة ...", "backup_background_service_default_notification": "التحقق من الأصول الجديدة ...",
"backup_background_service_error_title": "خطأ في النسخ الاحتياطي", "backup_background_service_error_title": "خطأ في النسخ الاحتياطي",
"backup_background_service_in_progress_notification": "النسخ الاحتياطي للأصول الخاصة بك...", "backup_background_service_in_progress_notification": "النسخ الاحتياطي للأصول الخاصة بك...",
"backup_background_service_upload_failure_notification": "فشل التحميل {}", "backup_background_service_upload_failure_notification": "Failed to upload {}",
"backup_controller_page_albums": "ألبومات احتياطية", "backup_controller_page_albums": "ألبومات احتياطية",
"backup_controller_page_background_app_refresh_disabled_content": "قم بتمكين تحديث تطبيق الخلفية في الإعدادات > عام > تحديث تطبيق الخلفية لاستخدام النسخ الاحتياطي في الخلفية.", "backup_controller_page_background_app_refresh_disabled_content": "قم بتمكين تحديث تطبيق الخلفية في الإعدادات > عام > تحديث تطبيق الخلفية لاستخدام النسخ الاحتياطي في الخلفية.",
"backup_controller_page_background_app_refresh_disabled_title": "تم تعطيل تحديث التطبيق في الخلفية", "backup_controller_page_background_app_refresh_disabled_title": "تم تعطيل تحديث التطبيق في الخلفية",
"backup_controller_page_background_app_refresh_enable_button_text": "اذهب للاعدادات", "backup_controller_page_background_app_refresh_enable_button_text": "اذهب للاعدادات",
"backup_controller_page_background_battery_info_link": "أرني كيف", "backup_controller_page_background_battery_info_link": "أرني كيف",
"backup_controller_page_background_battery_info_message": "للحصول على أفضل تجربة نسخ احتياطي في الخلفية، يرجى تعطيل أي تحسينات للبطارية تقيد نشاط الخلفية لـ Immich.\n\nنظرًا لأن هذا خاص بالجهاز، يرجى البحث عن المعلومات المطلوبة للشركة المصنعة لجهازك.", "backup_controller_page_background_battery_info_message": "للحصول على أفضل تجربة نسخ احتياطي في الخلفية، يرجى تعطيل أي تحسينات للبطارية تقيد نشاط الخلفية لـ تطبيق.\n\nنظرًا لأن هذا خاص بالجهاز، يرجى البحث عن المعلومات المطلوبة للشركة المصنعة لجهازك.",
"backup_controller_page_background_battery_info_ok": "نعم", "backup_controller_page_background_battery_info_ok": "نعم",
"backup_controller_page_background_battery_info_title": "تحسين البطارية", "backup_controller_page_background_battery_info_title": "تحسين البطارية",
"backup_controller_page_background_charging": "فقط أثناء الشحن", "backup_controller_page_background_charging": "فقط أثناء الشحن",
"backup_controller_page_background_configure_error": "فشل في تكوين خدمة الخلفية", "backup_controller_page_background_configure_error": "فشل في تكوين خدمة الخلفية",
"backup_controller_page_background_delay": "تأخير النسخ الاحتياطي للأصول الجديدة: {}", "backup_controller_page_background_delay": "Delay new assets backup: {}",
"backup_controller_page_background_description": "قم بتشغيل خدمة الخلفية لإجراء نسخ احتياطي لأي أصول جديدة تلقائيًا دون الحاجة إلى فتح التطبيق", "backup_controller_page_background_description": "قم بتشغيل خدمة الخلفية لإجراء نسخ احتياطي لأي أصول جديدة تلقائيًا دون الحاجة إلى فتح التطبيق",
"backup_controller_page_background_is_off": "تم إيقاف النسخ الاحتياطي التلقائي للخلفية", "backup_controller_page_background_is_off": "تم إيقاف النسخ الاحتياطي التلقائي للخلفية",
"backup_controller_page_background_is_on": "النسخ الاحتياطي التلقائي للخلفية قيد التشغيل", "backup_controller_page_background_is_on": "النسخ الاحتياطي التلقائي للخلفية قيد التشغيل",
@@ -86,12 +86,12 @@
"backup_controller_page_backup_selected": "المحدد: ", "backup_controller_page_backup_selected": "المحدد: ",
"backup_controller_page_backup_sub": "النسخ الاحتياطي للصور ومقاطع الفيديو", "backup_controller_page_backup_sub": "النسخ الاحتياطي للصور ومقاطع الفيديو",
"backup_controller_page_cancel": "يلغي", "backup_controller_page_cancel": "يلغي",
"backup_controller_page_created": "تم إنشاؤها على: {}", "backup_controller_page_created": "Created on: {}",
"backup_controller_page_desc_backup": "قم بتشغيل النسخ الاحتياطي الأمامي لتحميل الأصول الجديدة تلقائيًا إلى الخادم عند فتح التطبيق.", "backup_controller_page_desc_backup": "قم بتشغيل النسخ الاحتياطي الأمامي لتحميل الأصول الجديدة تلقائيًا إلى الخادم عند فتح التطبيق.",
"backup_controller_page_excluded": "مستبعد: ", "backup_controller_page_excluded": "مستبعد: ",
"backup_controller_page_failed": "فشل ({})", "backup_controller_page_failed": "Failed ({})",
"backup_controller_page_filename": "اسم الملف: {} [{}]", "backup_controller_page_filename": "File name: {} [{}]",
"backup_controller_page_id": "رقم البطاقة: {}", "backup_controller_page_id": "ID: {}",
"backup_controller_page_info": "معلومات النسخ الاحتياطي", "backup_controller_page_info": "معلومات النسخ الاحتياطي",
"backup_controller_page_none_selected": "لم يتم التحديد", "backup_controller_page_none_selected": "لم يتم التحديد",
"backup_controller_page_remainder": "بقية", "backup_controller_page_remainder": "بقية",
@@ -101,7 +101,7 @@
"backup_controller_page_start_backup": "بدء النسخ الاحتياطي", "backup_controller_page_start_backup": "بدء النسخ الاحتياطي",
"backup_controller_page_status_off": "النسخة الاحتياطية التلقائية غير فعالة", "backup_controller_page_status_off": "النسخة الاحتياطية التلقائية غير فعالة",
"backup_controller_page_status_on": "النسخة الاحتياطية التلقائية فعالة", "backup_controller_page_status_on": "النسخة الاحتياطية التلقائية فعالة",
"backup_controller_page_storage_format": "{} من {} المستخدم", "backup_controller_page_storage_format": "{} of {} used",
"backup_controller_page_to_backup": "الألبومات الاحتياطية", "backup_controller_page_to_backup": "الألبومات الاحتياطية",
"backup_controller_page_total": "المجموع", "backup_controller_page_total": "المجموع",
"backup_controller_page_total_sub": "جميع الصور ومقاطع الفيديو الفريدة من ألبومات مختارة", "backup_controller_page_total_sub": "جميع الصور ومقاطع الفيديو الفريدة من ألبومات مختارة",
@@ -115,22 +115,22 @@
"backup_manual_in_progress": "قيد التحميل حاول مره اخرى", "backup_manual_in_progress": "قيد التحميل حاول مره اخرى",
"backup_manual_success": "نجاح", "backup_manual_success": "نجاح",
"backup_manual_title": "حالة التحميل", "backup_manual_title": "حالة التحميل",
"backup_options_page_title": "Backup options", "backup_options_page_title": "خيارات النسخ الاحتياطي",
"cache_settings_album_thumbnails": "صور صفحة المكتبة ({} الأصول)", "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
"cache_settings_clear_cache_button": "مسح ذاكرة التخزين المؤقت", "cache_settings_clear_cache_button": "مسح ذاكرة التخزين المؤقت",
"cache_settings_clear_cache_button_title": "يقوم بمسح ذاكرة التخزين المؤقت للتطبيق.سيؤثر هذا بشكل كبير على أداء التطبيق حتى إعادة بناء ذاكرة التخزين المؤقت.", "cache_settings_clear_cache_button_title": "يقوم بمسح ذاكرة التخزين المؤقت للتطبيق.سيؤثر هذا بشكل كبير على أداء التطبيق حتى إعادة بناء ذاكرة التخزين المؤقت.",
"cache_settings_duplicated_assets_clear_button": "واضح", "cache_settings_duplicated_assets_clear_button": "واضح",
"cache_settings_duplicated_assets_subtitle": "الصور ومقاطع الفيديو اللتي تم تجاهلها المدرجة في التطبيق", "cache_settings_duplicated_assets_subtitle": "الصور ومقاطع الفيديو اللتي تم تجاهلها المدرجة في التطبيق",
"cache_settings_duplicated_assets_title": "الأصول المكررة ({})", "cache_settings_duplicated_assets_title": "Duplicated Assets ({})",
"cache_settings_image_cache_size": "حجم ذاكرة التخزين المؤقت للصور ({} الأصول)", "cache_settings_image_cache_size": "Image cache size ({} assets)",
"cache_settings_statistics_album": "مكتبه الصور المصغره", "cache_settings_statistics_album": "مكتبه الصور المصغره",
"cache_settings_statistics_assets": " ({})أصول ", "cache_settings_statistics_assets": "{} assets ({})",
"cache_settings_statistics_full": "صور كاملة", "cache_settings_statistics_full": "صور كاملة",
"cache_settings_statistics_shared": "صورة ألبوم مشتركة", "cache_settings_statistics_shared": "صورة ألبوم مشتركة",
"cache_settings_statistics_thumbnail": "الصورة المصغرة", "cache_settings_statistics_thumbnail": "الصورة المصغرة",
"cache_settings_statistics_title": "استخدام ذاكرة التخزين المؤقت", "cache_settings_statistics_title": "استخدام ذاكرة التخزين المؤقت",
"cache_settings_subtitle": "تحكم في سلوك التخزين المؤقت لتطبيق الجوال Imich", "cache_settings_subtitle": "تحكم في سلوك التخزين المؤقت لتطبيق الجوال.",
"cache_settings_thumbnail_size": "حجم ذاكرة التخزين المؤقت Thumbnail ({} الأصول)", "cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)",
"cache_settings_tile_subtitle": "التحكم في سلوك التخزين المحلي", "cache_settings_tile_subtitle": "التحكم في سلوك التخزين المحلي",
"cache_settings_tile_title": "التخزين المحلي", "cache_settings_tile_title": "التخزين المحلي",
"cache_settings_title": "إعدادات التخزين المؤقت", "cache_settings_title": "إعدادات التخزين المؤقت",
@@ -145,12 +145,12 @@
"common_server_error": "يرجى التحقق من اتصال الشبكة الخاص بك ، والتأكد من أن الجهاز قابل للوصول وإصدارات التطبيق/الجهاز متوافقة.", "common_server_error": "يرجى التحقق من اتصال الشبكة الخاص بك ، والتأكد من أن الجهاز قابل للوصول وإصدارات التطبيق/الجهاز متوافقة.",
"common_shared": "مشترك", "common_shared": "مشترك",
"control_bottom_app_bar_add_to_album": "أضف إلى الألبوم", "control_bottom_app_bar_add_to_album": "أضف إلى الألبوم",
"control_bottom_app_bar_album_info": "{} أغراض", "control_bottom_app_bar_album_info": "{} items",
"control_bottom_app_bar_album_info_shared": "{} العناصر المشتركة", "control_bottom_app_bar_album_info_shared": "{} items · Shared",
"control_bottom_app_bar_archive": "أرشيف", "control_bottom_app_bar_archive": "أرشيف",
"control_bottom_app_bar_create_new_album": "إنشاء ألبوم جديد", "control_bottom_app_bar_create_new_album": "إنشاء ألبوم جديد",
"control_bottom_app_bar_delete": "يمسح", "control_bottom_app_bar_delete": "يمسح",
"control_bottom_app_bar_delete_from_immich": " Immich حذف منال تطبيق", "control_bottom_app_bar_delete_from_immich": " حذف منال تطبيق",
"control_bottom_app_bar_delete_from_local": "حذف من الجهاز", "control_bottom_app_bar_delete_from_local": "حذف من الجهاز",
"control_bottom_app_bar_edit_location": "تحديد الوجهة", "control_bottom_app_bar_edit_location": "تحديد الوجهة",
"control_bottom_app_bar_edit_time": "تحرير التاريخ والوقت", "control_bottom_app_bar_edit_time": "تحرير التاريخ والوقت",
@@ -172,10 +172,10 @@
"daily_title_text_date": "E ، MMM DD", "daily_title_text_date": "E ، MMM DD",
"daily_title_text_date_year": "E ، MMM DD ، yyyy", "daily_title_text_date_year": "E ، MMM DD ، yyyy",
"date_format": "E ، Lll D ، Y • H: MM A", "date_format": "E ، Lll D ، Y • H: MM A",
"delete_dialog_alert": " Immich هذه العناصر سيتم حذفها بشكل دائم من جهازك ومن تطبيق", "delete_dialog_alert": " هذه العناصر سيتم حذفها بشكل دائم من جهازك ومن تطبيق",
"delete_dialog_alert_local": " Immich العناصر التي تم حذفها من جهازك ولكنها موجوده في تطبيق", "delete_dialog_alert_local": " العناصر التي تم حذفها من جهازك ولكنها موجوده في تطبيق",
"delete_dialog_alert_local_non_backed_up": "Immich بعض العناصر التي سيتم حذفها بشكل دائم ولا يوجد لها نسخه احتياطيه في تطبيق ", "delete_dialog_alert_local_non_backed_up": "بعض العناصر التي سيتم حذفها بشكل دائم ولا يوجد لها نسخه احتياطيه في تطبيق ",
"delete_dialog_alert_remote": "Immich العناصر التي سيتم حذفها بشكل دائم من تطبيق", "delete_dialog_alert_remote": "العناصر التي سيتم حذفها بشكل دائم من تطبيق",
"delete_dialog_cancel": "يلغي", "delete_dialog_cancel": "يلغي",
"delete_dialog_ok": "يمسح", "delete_dialog_ok": "يمسح",
"delete_dialog_ok_force": "احذف على أي حال", "delete_dialog_ok_force": "احذف على أي حال",
@@ -194,15 +194,15 @@
"exif_bottom_sheet_location": "موقع", "exif_bottom_sheet_location": "موقع",
"exif_bottom_sheet_location_add": "إضافة موقع", "exif_bottom_sheet_location_add": "إضافة موقع",
"exif_bottom_sheet_people": "الناس", "exif_bottom_sheet_people": "الناس",
"exif_bottom_sheet_person_add_person": "Add name", "exif_bottom_sheet_person_add_person": "اضف اسما",
"experimental_settings_new_asset_list_subtitle": "أعمال جارية", "experimental_settings_new_asset_list_subtitle": "أعمال جارية",
"experimental_settings_new_asset_list_title": "تمكين شبكة الصور التجريبية", "experimental_settings_new_asset_list_title": "تمكين شبكة الصور التجريبية",
"experimental_settings_subtitle": "استخدام على مسؤوليتك الخاصة!", "experimental_settings_subtitle": "استخدام على مسؤوليتك الخاصة!",
"experimental_settings_title": "تجريبي", "experimental_settings_title": "تجريبي",
"favorites_page_no_favorites": "لم يتم العثور على الأصول المفضلة", "favorites_page_no_favorites": "لم يتم العثور على الأصول المفضلة",
"favorites_page_title": "المفضلة", "favorites_page_title": "المفضلة",
"haptic_feedback_switch": "Enable haptic feedback", "haptic_feedback_switch": "تمكين ردود الفعل اللمسية",
"haptic_feedback_title": "Haptic Feedback", "haptic_feedback_title": "ردود فعل لمسية",
"home_page_add_to_album_conflicts": "تمت إضافة {تمت إضافة} الأصول إلى الألبوم {الألبوم}.{فشل} الأصول موجودة بالفعل في الألبوم.", "home_page_add_to_album_conflicts": "تمت إضافة {تمت إضافة} الأصول إلى الألبوم {الألبوم}.{فشل} الأصول موجودة بالفعل في الألبوم.",
"home_page_add_to_album_err_local": "لا يمكن إضافة الأصول المحلية إلى الألبومات حتى الآن ، سوف يتخطى", "home_page_add_to_album_err_local": "لا يمكن إضافة الأصول المحلية إلى الألبومات حتى الآن ، سوف يتخطى",
"home_page_add_to_album_success": "تمت إضافة {تمت إضافة} الأصول إلى الألبوم {الألبوم}.", "home_page_add_to_album_success": "تمت إضافة {تمت إضافة} الأصول إلى الألبوم {الألبوم}.",
@@ -218,7 +218,7 @@
"home_page_share_err_local": "لا يمكن مشاركة الأصول المحلية عبر الرابط ، سوف يتخطى", "home_page_share_err_local": "لا يمكن مشاركة الأصول المحلية عبر الرابط ، سوف يتخطى",
"home_page_upload_err_limit": "لا يمكن إلا تحميل 30 أحد الأصول في وقت واحد ، سوف يتخطى", "home_page_upload_err_limit": "لا يمكن إلا تحميل 30 أحد الأصول في وقت واحد ، سوف يتخطى",
"image_viewer_page_state_provider_download_error": "خطا في التحميل", "image_viewer_page_state_provider_download_error": "خطا في التحميل",
"image_viewer_page_state_provider_download_started": "Download Started", "image_viewer_page_state_provider_download_started": "بدأ التنزيل",
"image_viewer_page_state_provider_download_success": "تم التنزيل بنجاح", "image_viewer_page_state_provider_download_success": "تم التنزيل بنجاح",
"image_viewer_page_state_provider_share_error": "خطأ في المشاركة", "image_viewer_page_state_provider_share_error": "خطأ في المشاركة",
"library_page_albums": "ألبومات", "library_page_albums": "ألبومات",
@@ -265,8 +265,8 @@
"login_form_server_error": "لا يمكن الاتصال بالخادم.", "login_form_server_error": "لا يمكن الاتصال بالخادم.",
"login_password_changed_error": "كان هناك خطأ في تحديث كلمة المرور الخاصة بك", "login_password_changed_error": "كان هناك خطأ في تحديث كلمة المرور الخاصة بك",
"login_password_changed_success": "تم تحديث كلمة السر بنجاح", "login_password_changed_success": "تم تحديث كلمة السر بنجاح",
"map_assets_in_bound": "{} صورة", "map_assets_in_bound": "{} photo",
"map_assets_in_bounds": "{} الصور", "map_assets_in_bounds": "{} photos",
"map_cannot_get_user_location": "لا يمكن الحصول على موقع المستخدم", "map_cannot_get_user_location": "لا يمكن الحصول على موقع المستخدم",
"map_location_dialog_cancel": "يلغي", "map_location_dialog_cancel": "يلغي",
"map_location_dialog_yes": "نعم", "map_location_dialog_yes": "نعم",
@@ -279,27 +279,27 @@
"map_settings_dark_mode": "الوضع المظلم", "map_settings_dark_mode": "الوضع المظلم",
"map_settings_date_range_option_all": "الجميع", "map_settings_date_range_option_all": "الجميع",
"map_settings_date_range_option_day": "24 ساعة الماضية", "map_settings_date_range_option_day": "24 ساعة الماضية",
"map_settings_date_range_option_days": "الأيام الماضية {}", "map_settings_date_range_option_days": "Past {} days",
"map_settings_date_range_option_year": "السنة الفائتة", "map_settings_date_range_option_year": "السنة الفائتة",
"map_settings_date_range_option_years": "السنوات الماضية {}", "map_settings_date_range_option_years": "Past {} years",
"map_settings_dialog_cancel": "يلغي", "map_settings_dialog_cancel": "يلغي",
"map_settings_dialog_save": "يحفظ", "map_settings_dialog_save": "يحفظ",
"map_settings_dialog_title": "إعدادات الخريطة", "map_settings_dialog_title": "إعدادات الخريطة",
"map_settings_include_show_archived": "تشمل الأرشفة", "map_settings_include_show_archived": "تشمل الأرشفة",
"map_settings_include_show_partners": "Include Partners", "map_settings_include_show_partners": "تضمين الشركاء",
"map_settings_only_relative_range": "نطاق الموعد", "map_settings_only_relative_range": "نطاق الموعد",
"map_settings_only_show_favorites": "اظهار المفضلة فقط", "map_settings_only_show_favorites": "اظهار المفضلة فقط",
"map_settings_theme_settings": "مظهر الخريطة", "map_settings_theme_settings": "مظهر الخريطة",
"map_zoom_to_see_photos": "قم بتصغيرها لرؤية الصور", "map_zoom_to_see_photos": "قم بتصغيرها لرؤية الصور",
"memories_all_caught_up": "All caught up", "memories_all_caught_up": "كل شيء محدث",
"memories_check_back_tomorrow": "Check back tomorrow for more memories", "memories_check_back_tomorrow": "التحقق مرة أخرى غدا لمزيد من الذكريات",
"memories_start_over": "Start Over", "memories_start_over": "ابدأ من جديد",
"memories_swipe_to_close": "Swipe up to close", "memories_swipe_to_close": "اسحب لأعلى للإغلاق",
"monthly_title_text_date_format": "ط ط ط", "monthly_title_text_date_format": "ط ط ط",
"motion_photos_page_title": "الصور المتحركة", "motion_photos_page_title": "الصور المتحركة",
"multiselect_grid_edit_date_time_err_read_only": "لا يمكن تعديل تاريخ الأصول (المواد) للقراءة فقط، سوف يتخطى", "multiselect_grid_edit_date_time_err_read_only": "لا يمكن تعديل تاريخ الأصول (المواد) للقراءة فقط، سوف يتخطى",
"multiselect_grid_edit_gps_err_read_only": "لا يمكن تعديل موقع الأصول (المواد) للقراءة فقط، سوف يتخطى", "multiselect_grid_edit_gps_err_read_only": "لا يمكن تعديل موقع الأصول (المواد) للقراءة فقط، سوف يتخطى",
"no_assets_to_show": "No assets to show", "no_assets_to_show": "لا توجد أصول لعرضها",
"notification_permission_dialog_cancel": "يلغي", "notification_permission_dialog_cancel": "يلغي",
"notification_permission_dialog_content": "لتمكين الإخطارات ، انتقل إلى الإعدادات و اختار السماح.", "notification_permission_dialog_content": "لتمكين الإخطارات ، انتقل إلى الإعدادات و اختار السماح.",
"notification_permission_dialog_settings": "إعدادات", "notification_permission_dialog_settings": "إعدادات",
@@ -307,14 +307,14 @@
"notification_permission_list_tile_enable_button": "تمكين الإخطارات", "notification_permission_list_tile_enable_button": "تمكين الإخطارات",
"notification_permission_list_tile_title": "إذن الإخطار", "notification_permission_list_tile_title": "إذن الإخطار",
"partner_list_user_photos": "{user}'s photos", "partner_list_user_photos": "{user}'s photos",
"partner_list_view_all": "View all", "partner_list_view_all": "عرض الكل",
"partner_page_add_partner": "أضف شريكًا", "partner_page_add_partner": "أضف شريكًا",
"partner_page_empty_message": "لم يتم مشاركة صورك بعد مع أي شريك.", "partner_page_empty_message": "لم يتم مشاركة صورك بعد مع أي شريك.",
"partner_page_no_more_users": "لا مزيد من المستخدمين لإضافة", "partner_page_no_more_users": "لا مزيد من المستخدمين لإضافة",
"partner_page_partner_add_failed": "فشل في إضافة شريك", "partner_page_partner_add_failed": "فشل في إضافة شريك",
"partner_page_select_partner": "حدد شريكًا", "partner_page_select_partner": "حدد شريكًا",
"partner_page_shared_to_title": "مشترك ل", "partner_page_shared_to_title": "مشترك ل",
"partner_page_stop_sharing_content": "{} لن يكون قادرًا على الوصول إلى صورك.", "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
"partner_page_stop_sharing_title": "توقف عن مشاركة صورك؟", "partner_page_stop_sharing_title": "توقف عن مشاركة صورك؟",
"partner_page_title": "شريك", "partner_page_title": "شريك",
"permission_onboarding_back": "خلف", "permission_onboarding_back": "خلف",
@@ -327,7 +327,7 @@
"permission_onboarding_permission_granted": "تم تأمين التصريح! وضعك تمام.", "permission_onboarding_permission_granted": "تم تأمين التصريح! وضعك تمام.",
"permission_onboarding_permission_limited": "إذن محدود. للسماح بالنسخ الاحتياطي للتطبيق وإدارة مجموعة المعرض بالكامل، امنح أذونات الصور والفيديو في الإعدادات.", "permission_onboarding_permission_limited": "إذن محدود. للسماح بالنسخ الاحتياطي للتطبيق وإدارة مجموعة المعرض بالكامل، امنح أذونات الصور والفيديو في الإعدادات.",
"permission_onboarding_request": "يتطلب التطبيق إذنًا لعرض الصور ومقاطع الفيديو الخاصة بك", "permission_onboarding_request": "يتطلب التطبيق إذنًا لعرض الصور ومقاطع الفيديو الخاصة بك",
"preferences_settings_title": "Preferences", "preferences_settings_title": "التفضيلات",
"profile_drawer_app_logs": "السجلات", "profile_drawer_app_logs": "السجلات",
"profile_drawer_client_out_of_date_major": "تطبيق الهاتف المحمول قديم.يرجى التحديث إلى أحدث إصدار رئيسي.", "profile_drawer_client_out_of_date_major": "تطبيق الهاتف المحمول قديم.يرجى التحديث إلى أحدث إصدار رئيسي.",
"profile_drawer_client_out_of_date_minor": "تطبيق الهاتف المحمول قديم.يرجى التحديث إلى أحدث إصدار صغير.", "profile_drawer_client_out_of_date_minor": "تطبيق الهاتف المحمول قديم.يرجى التحديث إلى أحدث إصدار صغير.",
@@ -342,18 +342,18 @@
"recently_added_page_title": "أضيف مؤخرا", "recently_added_page_title": "أضيف مؤخرا",
"scaffold_body_error_occurred": "حدث خطأ", "scaffold_body_error_occurred": "حدث خطأ",
"search_bar_hint": "ابحث عن صورك", "search_bar_hint": "ابحث عن صورك",
"search_filter_apply": "Apply filter", "search_filter_apply": "اختار الفلتر ",
"search_filter_camera_make": "Make", "search_filter_camera_make": "صنع",
"search_filter_camera_model": "Model", "search_filter_camera_model": "نموذج",
"search_filter_display_option_archive": "Archive", "search_filter_display_option_archive": "أرشيف",
"search_filter_display_option_favorite": "Favorite", "search_filter_display_option_favorite": "مفضل",
"search_filter_display_option_not_in_album": "Not in album", "search_filter_display_option_not_in_album": "ليس في الألبوم",
"search_filter_location_city": "City", "search_filter_location_city": "مدينة",
"search_filter_location_country": "Country", "search_filter_location_country": "دولة",
"search_filter_location_state": "State", "search_filter_location_state": "ولاية",
"search_filter_media_type_all": "All", "search_filter_media_type_all": "الجميع",
"search_filter_media_type_image": "Image", "search_filter_media_type_image": "صورة",
"search_filter_media_type_video": "Video", "search_filter_media_type_video": "شريط فيديو",
"search_page_categories": "فئات", "search_page_categories": "فئات",
"search_page_favorites": "المفضلة", "search_page_favorites": "المفضلة",
"search_page_motion_photos": "الصور المتحركه", "search_page_motion_photos": "الصور المتحركه",
@@ -391,14 +391,15 @@
"setting_image_viewer_original_title": "تحميل الصورة الأصلية", "setting_image_viewer_original_title": "تحميل الصورة الأصلية",
"setting_image_viewer_preview_subtitle": "تمكين تحميل صورة متوسطة الدقة.تعطيل إما لتحميل مباشرة أو استخدام الصورة المصغرة مباشرة.", "setting_image_viewer_preview_subtitle": "تمكين تحميل صورة متوسطة الدقة.تعطيل إما لتحميل مباشرة أو استخدام الصورة المصغرة مباشرة.",
"setting_image_viewer_preview_title": "تحميل صورة معاينة", "setting_image_viewer_preview_title": "تحميل صورة معاينة",
"setting_languages_apply": "Apply", "setting_image_viewer_title": "الصور",
"setting_languages_title": "Languages", "setting_languages_apply": "تغيير الإعدادات",
"setting_notifications_notify_failures_grace_period": "أخطر فشل النسخ الاحتياطي في الخلفية: {}", "setting_languages_title": "اللغات",
"setting_notifications_notify_hours": "{} ساعات", "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
"setting_notifications_notify_hours": "{} hours",
"setting_notifications_notify_immediately": "في الحال", "setting_notifications_notify_immediately": "في الحال",
"setting_notifications_notify_minutes": "{} دقائق", "setting_notifications_notify_minutes": "{} minutes",
"setting_notifications_notify_never": "أبداً", "setting_notifications_notify_never": "أبداً",
"setting_notifications_notify_seconds": "{} ثانية", "setting_notifications_notify_seconds": "{} seconds",
"setting_notifications_single_progress_subtitle": "معلومات التقدم التفصيلية تحميل لكل أصل", "setting_notifications_single_progress_subtitle": "معلومات التقدم التفصيلية تحميل لكل أصل",
"setting_notifications_single_progress_title": "إظهار تقدم التفاصيل الاحتياطية الخلفية", "setting_notifications_single_progress_title": "إظهار تقدم التفاصيل الاحتياطية الخلفية",
"setting_notifications_subtitle": "اضبط تفضيلات الإخطار", "setting_notifications_subtitle": "اضبط تفضيلات الإخطار",
@@ -406,7 +407,10 @@
"setting_notifications_total_progress_subtitle": "التقدم التحميل العام (تم القيام به/إجمالي الأصول)", "setting_notifications_total_progress_subtitle": "التقدم التحميل العام (تم القيام به/إجمالي الأصول)",
"setting_notifications_total_progress_title": "إظهار النسخ الاحتياطي الخلفية التقدم المحرز", "setting_notifications_total_progress_title": "إظهار النسخ الاحتياطي الخلفية التقدم المحرز",
"setting_pages_app_bar_settings": "إعدادات", "setting_pages_app_bar_settings": "إعدادات",
"settings_require_restart": "يرجى إعادة تشغيل Imich لتطبيق هذا الإعداد", "settings_require_restart": "يرجى إعادة تشغيل لتطبيق هذا الإعداد",
"setting_video_viewer_looping_subtitle": "تمكين تكرار مقطع فيديو تلقائيًا في عارض التفاصيل.",
"setting_video_viewer_looping_title": "تكرار مقطع فيديو تلقائيًا",
"setting_video_viewer_title": "أشرطة فيديو",
"share_add": "يضيف", "share_add": "يضيف",
"share_add_photos": "إضافة الصور", "share_add_photos": "إضافة الصور",
"share_add_title": "إضافة عنوان", "share_add_title": "إضافة عنوان",
@@ -426,7 +430,7 @@
"share_dialog_preparing": "تحضير...", "share_dialog_preparing": "تحضير...",
"shared_link_app_bar_title": "روابط مشتركة", "shared_link_app_bar_title": "روابط مشتركة",
"shared_link_clipboard_copied_massage": "نسخ إلى الحافظة", "shared_link_clipboard_copied_massage": "نسخ إلى الحافظة",
"shared_link_clipboard_text": "وصلة: {}كلمة المرور: {}", "shared_link_clipboard_text": "Link: {}\nPassword: {}",
"shared_link_create_app_bar_title": "إنشاء رابط للمشاركة", "shared_link_create_app_bar_title": "إنشاء رابط للمشاركة",
"shared_link_create_error": "خطأ أثناء إنشاء رابط مشترك", "shared_link_create_error": "خطأ أثناء إنشاء رابط مشترك",
"shared_link_create_info": "دع أي شخص لديه الرابط يرى الصور المحددة)", "shared_link_create_info": "دع أي شخص لديه الرابط يرى الصور المحددة)",
@@ -439,11 +443,11 @@
"shared_link_edit_description_hint": "أدخل وصف المشاركة", "shared_link_edit_description_hint": "أدخل وصف المشاركة",
"shared_link_edit_expire_after": "تنتهي بعد", "shared_link_edit_expire_after": "تنتهي بعد",
"shared_link_edit_expire_after_option_day": "يوم 1", "shared_link_edit_expire_after_option_day": "يوم 1",
"shared_link_edit_expire_after_option_days": "{} أيام", "shared_link_edit_expire_after_option_days": "{} days",
"shared_link_edit_expire_after_option_hour": "1 ساعة", "shared_link_edit_expire_after_option_hour": "1 ساعة",
"shared_link_edit_expire_after_option_hours": "{} ساعات", "shared_link_edit_expire_after_option_hours": "{} hours",
"shared_link_edit_expire_after_option_minute": "1 دقيقة", "shared_link_edit_expire_after_option_minute": "1 دقيقة",
"shared_link_edit_expire_after_option_minutes": "{} دقائق", "shared_link_edit_expire_after_option_minutes": "{} minutes",
"shared_link_edit_expire_after_option_months": "{} months", "shared_link_edit_expire_after_option_months": "{} months",
"shared_link_edit_expire_after_option_never": "أبداً", "shared_link_edit_expire_after_option_never": "أبداً",
"shared_link_edit_expire_after_option_year": "{} year", "shared_link_edit_expire_after_option_year": "{} year",
@@ -454,21 +458,21 @@
"shared_link_empty": "ليس لديك أي روابط مشتركة", "shared_link_empty": "ليس لديك أي روابط مشتركة",
"shared_link_error_server_url_fetch": "لا يمكن جلب عنوان الخادم", "shared_link_error_server_url_fetch": "لا يمكن جلب عنوان الخادم",
"shared_link_expired": "منتهي الصلاحية", "shared_link_expired": "منتهي الصلاحية",
"shared_link_expires_day": "تنتهي في اليوم {}", "shared_link_expires_day": "Expires in {} day",
"shared_link_expires_days": "تنتهي في {} أيام", "shared_link_expires_days": "Expires in {} days",
"shared_link_expires_hour": "تنتهي في الساعة {}", "shared_link_expires_hour": "Expires in {} hour",
"shared_link_expires_hours": "تنتهي في {} ساعات", "shared_link_expires_hours": "Expires in {} hours",
"shared_link_expires_minute": "تنتهي في {} دقيقة", "shared_link_expires_minute": "Expires in {} minute",
"shared_link_expires_minutes": "تنتهي في الدقائق {}", "shared_link_expires_minutes": "Expires in {} minutes",
"shared_link_expires_never": "تنتهي ∞", "shared_link_expires_never": "تنتهي ∞",
"shared_link_expires_second": "تنتهي في {} الثاني", "shared_link_expires_second": "Expires in {} second",
"shared_link_expires_seconds": "تنتهي في ثواني {}", "shared_link_expires_seconds": "Expires in {} seconds",
"shared_link_individual_shared": "Individual shared", "shared_link_individual_shared": "Individual shared",
"shared_link_info_chip_download": "تحميل", "shared_link_info_chip_download": "تحميل",
"shared_link_info_chip_metadata": "EXIF", "shared_link_info_chip_metadata": "EXIF",
"shared_link_info_chip_upload": "رفع", "shared_link_info_chip_upload": "رفع",
"shared_link_manage_links": "إدارة الروابط المشتركة", "shared_link_manage_links": "إدارة الروابط المشتركة",
"shared_link_public_album": "Public album", "shared_link_public_album": "الألبوم العام",
"share_done": "منتهي", "share_done": "منتهي",
"share_invite": "دعوة إلى الألبوم", "share_invite": "دعوة إلى الألبوم",
"sharing_page_album": "ألبومات مشتركة", "sharing_page_album": "ألبومات مشتركة",
@@ -482,7 +486,7 @@
"tab_controller_nav_search": "يبحث", "tab_controller_nav_search": "يبحث",
"tab_controller_nav_sharing": "مشاركة", "tab_controller_nav_sharing": "مشاركة",
"theme_setting_asset_list_storage_indicator_title": "عرض مؤشر التخزين على بلاط الأصول", "theme_setting_asset_list_storage_indicator_title": "عرض مؤشر التخزين على بلاط الأصول",
"theme_setting_asset_list_tiles_per_row_title": "عدد الأصول لكل صف ({})", "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})",
"theme_setting_dark_mode_switch": "الوضع المظلم", "theme_setting_dark_mode_switch": "الوضع المظلم",
"theme_setting_image_viewer_quality_subtitle": "اضبط جودة عارض الصورة التفصيلية", "theme_setting_image_viewer_quality_subtitle": "اضبط جودة عارض الصورة التفصيلية",
"theme_setting_image_viewer_quality_title": "جودة عارض الصورة", "theme_setting_image_viewer_quality_title": "جودة عارض الصورة",
@@ -497,13 +501,13 @@
"trash_page_empty_trash_btn": "افرغ سله المهملات ", "trash_page_empty_trash_btn": "افرغ سله المهملات ",
"trash_page_empty_trash_dialog_content": "هل تريد تفريغ أصولك المهملة؟ ستتم إزالة هذه العناصر نهائيًا من التطبيق", "trash_page_empty_trash_dialog_content": "هل تريد تفريغ أصولك المهملة؟ ستتم إزالة هذه العناصر نهائيًا من التطبيق",
"trash_page_empty_trash_dialog_ok": "نعم", "trash_page_empty_trash_dialog_ok": "نعم",
"trash_page_info": "سيتم حذف العناصر المحذوفة بشكل دائم بعد {} أيام", "trash_page_info": "Trashed items will be permanently deleted after {} days",
"trash_page_no_assets": "لا توجد اصول في سله المهملات", "trash_page_no_assets": "لا توجد اصول في سله المهملات",
"trash_page_restore": "الترجيع من سله المهملات", "trash_page_restore": "الترجيع من سله المهملات",
"trash_page_restore_all": "استعادة الكل", "trash_page_restore_all": "استعادة الكل",
"trash_page_select_assets_btn": "اختر الأصول ", "trash_page_select_assets_btn": "اختر الأصول ",
"trash_page_select_btn": "يختار", "trash_page_select_btn": "يختار",
"trash_page_title": "نفايات ({})", "trash_page_title": "Trash ({})",
"upload_dialog_cancel": "يلغي", "upload_dialog_cancel": "يلغي",
"upload_dialog_info": "هل تريد النسخ الاحتياطي للأصول (الأصول) المحددة إلى الخادم؟", "upload_dialog_info": "هل تريد النسخ الاحتياطي للأصول (الأصول) المحددة إلى الخادم؟",
"upload_dialog_ok": "رفع", "upload_dialog_ok": "رفع",
+4
View File
@@ -391,6 +391,7 @@
"setting_image_viewer_original_title": "Načíst původní obrázek", "setting_image_viewer_original_title": "Načíst původní obrázek",
"setting_image_viewer_preview_subtitle": "Umožňuje načíst obrázek se středním rozlišením. Zakažte, pokud chcete přímo načíst originál nebo použít pouze miniaturu.", "setting_image_viewer_preview_subtitle": "Umožňuje načíst obrázek se středním rozlišením. Zakažte, pokud chcete přímo načíst originál nebo použít pouze miniaturu.",
"setting_image_viewer_preview_title": "Načíst náhled obrázku", "setting_image_viewer_preview_title": "Načíst náhled obrázku",
"setting_image_viewer_title": "Obrázky",
"setting_languages_apply": "Použít", "setting_languages_apply": "Použít",
"setting_languages_title": "Jazyk", "setting_languages_title": "Jazyk",
"setting_notifications_notify_failures_grace_period": "Oznámení o selhání zálohování na pozadí: {}", "setting_notifications_notify_failures_grace_period": "Oznámení o selhání zálohování na pozadí: {}",
@@ -407,6 +408,9 @@
"setting_notifications_total_progress_title": "Zobrazit celkový průběh zálohování na pozadí", "setting_notifications_total_progress_title": "Zobrazit celkový průběh zálohování na pozadí",
"setting_pages_app_bar_settings": "Nastavení", "setting_pages_app_bar_settings": "Nastavení",
"settings_require_restart": "Pro použití tohoto nastavení restartujte Immich", "settings_require_restart": "Pro použití tohoto nastavení restartujte Immich",
"setting_video_viewer_looping_subtitle": "Povolení automatické smyčky videa v prohlížeči detailů.",
"setting_video_viewer_looping_title": "Smyčka",
"setting_video_viewer_title": "Videa",
"share_add": "Přidat", "share_add": "Přidat",
"share_add_photos": "Přidat fotografie", "share_add_photos": "Přidat fotografie",
"share_add_title": "Přidat název", "share_add_title": "Přidat název",
+4
View File
@@ -391,6 +391,7 @@
"setting_image_viewer_original_title": "Indlæs originalbillede", "setting_image_viewer_original_title": "Indlæs originalbillede",
"setting_image_viewer_preview_subtitle": "Slå indlæsning af et mediumstørrelse billede til. Slå fra for enten direkte at indlæse originalen eller kun at bruge miniaturebilledet.", "setting_image_viewer_preview_subtitle": "Slå indlæsning af et mediumstørrelse billede til. Slå fra for enten direkte at indlæse originalen eller kun at bruge miniaturebilledet.",
"setting_image_viewer_preview_title": "Indlæs forhåndsvisning af billedet", "setting_image_viewer_preview_title": "Indlæs forhåndsvisning af billedet",
"setting_image_viewer_title": "Images",
"setting_languages_apply": "Anvend", "setting_languages_apply": "Anvend",
"setting_languages_title": "Sprog", "setting_languages_title": "Sprog",
"setting_notifications_notify_failures_grace_period": "Giv besked om fejl med sikkerhedskopiering i baggrunden: {}", "setting_notifications_notify_failures_grace_period": "Giv besked om fejl med sikkerhedskopiering i baggrunden: {}",
@@ -407,6 +408,9 @@
"setting_notifications_total_progress_title": "Vis samlet baggrundsuploadstatus", "setting_notifications_total_progress_title": "Vis samlet baggrundsuploadstatus",
"setting_pages_app_bar_settings": "Indstillinger", "setting_pages_app_bar_settings": "Indstillinger",
"settings_require_restart": "Genstart venligst Immich for at anvende denne ændring", "settings_require_restart": "Genstart venligst Immich for at anvende denne ændring",
"setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
"setting_video_viewer_looping_title": "Looping",
"setting_video_viewer_title": "Videos",
"share_add": "Tilføj", "share_add": "Tilføj",
"share_add_photos": "Tilføj billeder", "share_add_photos": "Tilføj billeder",
"share_add_title": "Tilføj en titel", "share_add_title": "Tilføj en titel",
+15 -15
View File
@@ -52,10 +52,10 @@
"asset_list_settings_title": "Fotogitter", "asset_list_settings_title": "Fotogitter",
"asset_viewer_settings_title": "Fotoanzeige", "asset_viewer_settings_title": "Fotoanzeige",
"backup_album_selection_page_albums_device": "Alben auf dem Gerät ({})", "backup_album_selection_page_albums_device": "Alben auf dem Gerät ({})",
"backup_album_selection_page_albums_tap": "Einmalig tippen um einzuschließen, doppelt tippen um zu entfernen", "backup_album_selection_page_albums_tap": "Einmalig tippen um das Album zu verwenden, doppelt tippen um es zu entfernen.",
"backup_album_selection_page_assets_scatter": "Elemente können sich über mehrere Alben verteilen. Daher können diese vor der Sicherung eingeschlossen oder ausgeschlossen werden", "backup_album_selection_page_assets_scatter": "Elemente (Fotos / Videos) können sich über mehrere Alben verteilen. Daher können diese vor der Sicherung eingeschlossen oder ausgeschlossen werden.",
"backup_album_selection_page_select_albums": "Alben auswählen", "backup_album_selection_page_select_albums": "Alben auswählen",
"backup_album_selection_page_selection_info": "Auswahl", "backup_album_selection_page_selection_info": "Information",
"backup_album_selection_page_total_assets": "Elemente", "backup_album_selection_page_total_assets": "Elemente",
"backup_all": "Alle", "backup_all": "Alle",
"backup_background_service_backup_failed_message": "Fehler beim Sichern von Elementen. Probiere erneut...", "backup_background_service_backup_failed_message": "Fehler beim Sichern von Elementen. Probiere erneut...",
@@ -75,7 +75,7 @@
"backup_controller_page_background_battery_info_title": "Batterieoptimierungen", "backup_controller_page_background_battery_info_title": "Batterieoptimierungen",
"backup_controller_page_background_charging": "Nur während des Ladens", "backup_controller_page_background_charging": "Nur während des Ladens",
"backup_controller_page_background_configure_error": "Konnte Hintergrundservice nicht konfigurieren", "backup_controller_page_background_configure_error": "Konnte Hintergrundservice nicht konfigurieren",
"backup_controller_page_background_delay": "Sicherung neuer Elemente verzögern: {}", "backup_controller_page_background_delay": "Sicherung neuer Elemente verzögern um: {}",
"backup_controller_page_background_description": "Schalte den Hintergrundservice ein, um neue Elemente automatisch im Hintergrund zu sichern ohne die App zu öffnen", "backup_controller_page_background_description": "Schalte den Hintergrundservice ein, um neue Elemente automatisch im Hintergrund zu sichern ohne die App zu öffnen",
"backup_controller_page_background_is_off": "Automatische Sicherung im Hintergrund ist deaktiviert", "backup_controller_page_background_is_off": "Automatische Sicherung im Hintergrund ist deaktiviert",
"backup_controller_page_background_is_on": "Automatische Sicherung im Hintergrund ist aktiviert", "backup_controller_page_background_is_on": "Automatische Sicherung im Hintergrund ist aktiviert",
@@ -331,7 +331,7 @@
"profile_drawer_app_logs": "Logs", "profile_drawer_app_logs": "Logs",
"profile_drawer_client_out_of_date_major": "Mobile-App ist veraltet. Bitte aktualisiere auf die neueste Major-Version.", "profile_drawer_client_out_of_date_major": "Mobile-App ist veraltet. Bitte aktualisiere auf die neueste Major-Version.",
"profile_drawer_client_out_of_date_minor": "Mobile-App ist veraltet. Bitte aktualisiere auf die neueste Minor-Version.", "profile_drawer_client_out_of_date_minor": "Mobile-App ist veraltet. Bitte aktualisiere auf die neueste Minor-Version.",
"profile_drawer_client_server_up_to_date": "App und Server sind aktuell", "profile_drawer_client_server_up_to_date": "Die App-Version / Server-Version sind aktuell.",
"profile_drawer_documentation": "Dokumentation", "profile_drawer_documentation": "Dokumentation",
"profile_drawer_github": "GitHub", "profile_drawer_github": "GitHub",
"profile_drawer_server_out_of_date_major": "Server-Version ist veraltet. Bitte aktualisiere auf die neueste Major-Version.", "profile_drawer_server_out_of_date_major": "Server-Version ist veraltet. Bitte aktualisiere auf die neueste Major-Version.",
@@ -382,19 +382,19 @@
"select_additional_user_for_sharing_page_suggestions": "Vorschläge", "select_additional_user_for_sharing_page_suggestions": "Vorschläge",
"select_user_for_sharing_page_err_album": "Album konnte nicht erstellt werden", "select_user_for_sharing_page_err_album": "Album konnte nicht erstellt werden",
"select_user_for_sharing_page_share_suggestions": "Empfehlungen", "select_user_for_sharing_page_share_suggestions": "Empfehlungen",
"server_info_box_app_version": "App Version", "server_info_box_app_version": "App-Version",
"server_info_box_latest_release": "Neueste Version", "server_info_box_latest_release": "Neueste Version",
"server_info_box_server_url": "Server-URL", "server_info_box_server_url": "Server-URL",
"server_info_box_server_version": "Server Version", "server_info_box_server_version": "Server-Version",
"setting_image_viewer_help": "Der Detailbildbetrachter lädt zuerst die kleine Miniaturansicht, dann die Vorschau in mittlerer Größe (falls aktiviert) und schließlich das Original (falls aktiviert).", "setting_image_viewer_help": "Der Detailbildbetrachter lädt zuerst ein (kleines) Vorschaubild, dann ein Vorschaubild in mittlerer Größe (falls aktiviert) und schließlich das Original (falls aktiviert).",
"setting_image_viewer_original_subtitle": "Aktivieren, um das Originalbild in voller Auflösung (groß!) zu laden. Deaktivieren, um den Datenverbrauch zu reduzieren (sowohl im Netzwerk als auch im Gerätespeicher).", "setting_image_viewer_original_subtitle": "Aktivieren, um das Originalbild in voller Auflösung (groß!) zu laden. Deaktivieren, um den Datenverbrauch zu reduzieren (sowohl im Netzwerk als auch im Gerätespeicher).",
"setting_image_viewer_original_title": "Original laden", "setting_image_viewer_original_title": "Original laden",
"setting_image_viewer_preview_subtitle": "Aktivieren, um ein Bild mit mittlerer Auflösung zu laden. Deaktivieren, um entweder das Original direkt zu laden oder nur die Miniaturansicht zu verwenden.", "setting_image_viewer_preview_subtitle": "Aktivieren, um ein Bild mit mittlerer Auflösung zu laden. Deaktivieren, um entweder das Original direkt zu laden oder nur das Vorschaubild zu verwenden.",
"setting_image_viewer_preview_title": "Vorschaubild laden", "setting_image_viewer_preview_title": "Vorschaubild laden",
"setting_image_viewer_title": "Bilder", "setting_image_viewer_title": "Bilder",
"setting_languages_apply": "Anwenden", "setting_languages_apply": "Anwenden",
"setting_languages_title": "Sprachen", "setting_languages_title": "Sprachen",
"setting_notifications_notify_failures_grace_period": "Benachrichtigung über Fehler bei der Hintergrundsicherung: {}", "setting_notifications_notify_failures_grace_period": "Benachrichtigung bei Fehler/n in der Hintergrundsicherung: {}",
"setting_notifications_notify_hours": "{} Stunden", "setting_notifications_notify_hours": "{} Stunden",
"setting_notifications_notify_immediately": "sofort", "setting_notifications_notify_immediately": "sofort",
"setting_notifications_notify_minutes": "{} Minuten", "setting_notifications_notify_minutes": "{} Minuten",
@@ -407,10 +407,10 @@
"setting_notifications_total_progress_subtitle": "Gesamter Upload-Fortschritt (abgeschlossen/Anzahl Elemente)", "setting_notifications_total_progress_subtitle": "Gesamter Upload-Fortschritt (abgeschlossen/Anzahl Elemente)",
"setting_notifications_total_progress_title": "Zeige Gesamtfortschritt bei der Hintergrundsicherung", "setting_notifications_total_progress_title": "Zeige Gesamtfortschritt bei der Hintergrundsicherung",
"setting_pages_app_bar_settings": "Einstellungen", "setting_pages_app_bar_settings": "Einstellungen",
"setting_video_viewer_looping_subtitle": "Aktivieren, damit sich ein Video in der Detailansicht automatisch wiederholt.",
"setting_video_viewer_looping_title": "Wiederholen",
"setting_video_viewer_title": "Videos",
"settings_require_restart": "Bitte starte Immich neu, um diese Einstellung anzuwenden.", "settings_require_restart": "Bitte starte Immich neu, um diese Einstellung anzuwenden.",
"setting_video_viewer_looping_subtitle": "Aktiviere diese Option, um ein Video in der Detailansicht automatisch in einer Schleife anzuzeigen.",
"setting_video_viewer_looping_title": "Schleife / Looping",
"setting_video_viewer_title": "Videos",
"share_add": "Hinzufügen", "share_add": "Hinzufügen",
"share_add_photos": "Fotos hinzufügen", "share_add_photos": "Fotos hinzufügen",
"share_add_title": "Titel hinzufügen", "share_add_title": "Titel hinzufügen",
@@ -485,12 +485,12 @@
"tab_controller_nav_photos": "Fotos", "tab_controller_nav_photos": "Fotos",
"tab_controller_nav_search": "Suche", "tab_controller_nav_search": "Suche",
"tab_controller_nav_sharing": "Teilen", "tab_controller_nav_sharing": "Teilen",
"theme_setting_asset_list_storage_indicator_title": "Zeige Sicherungsstatus auf Miniaturbild", "theme_setting_asset_list_storage_indicator_title": "Zeige Sicherungsstatus auf Vorschaubild",
"theme_setting_asset_list_tiles_per_row_title": "Anzahl der Elemente pro Reihe ({})", "theme_setting_asset_list_tiles_per_row_title": "Anzahl der Elemente pro Reihe ({})",
"theme_setting_dark_mode_switch": "Dunkler Modus", "theme_setting_dark_mode_switch": "Dunkler Modus",
"theme_setting_image_viewer_quality_subtitle": "Einstellen der Qualität des Detailbildbetrachters", "theme_setting_image_viewer_quality_subtitle": "Einstellen der Qualität des Detailbildbetrachters",
"theme_setting_image_viewer_quality_title": "Qualität des Bildbetrachters", "theme_setting_image_viewer_quality_title": "Qualität des Bildbetrachters",
"theme_setting_system_theme_switch": "Automatisch (Systemeinstellung folgen)", "theme_setting_system_theme_switch": "Automatisch (Systemeinstellung)",
"theme_setting_theme_subtitle": "Wählen Sie die Themeneinstellung der App", "theme_setting_theme_subtitle": "Wählen Sie die Themeneinstellung der App",
"theme_setting_theme_title": "Theme", "theme_setting_theme_title": "Theme",
"theme_setting_three_stage_loading_subtitle": "Das dreistufige Ladeverfahren kann die Performance beim Laden verbessern, erhöht allerdings den Datenverbrauch deutlich", "theme_setting_three_stage_loading_subtitle": "Das dreistufige Ladeverfahren kann die Performance beim Laden verbessern, erhöht allerdings den Datenverbrauch deutlich",
+4
View File
@@ -391,6 +391,7 @@
"setting_image_viewer_original_title": "Load original image", "setting_image_viewer_original_title": "Load original image",
"setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.", "setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.",
"setting_image_viewer_preview_title": "Load preview image", "setting_image_viewer_preview_title": "Load preview image",
"setting_image_viewer_title": "Images",
"setting_languages_apply": "Apply", "setting_languages_apply": "Apply",
"setting_languages_title": "Languages", "setting_languages_title": "Languages",
"setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
@@ -407,6 +408,9 @@
"setting_notifications_total_progress_title": "Show background backup total progress", "setting_notifications_total_progress_title": "Show background backup total progress",
"setting_pages_app_bar_settings": "Settings", "setting_pages_app_bar_settings": "Settings",
"settings_require_restart": "Please restart Immich to apply this setting", "settings_require_restart": "Please restart Immich to apply this setting",
"setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
"setting_video_viewer_looping_title": "Looping",
"setting_video_viewer_title": "Videos",
"share_add": "Add", "share_add": "Add",
"share_add_photos": "Add photos", "share_add_photos": "Add photos",
"share_add_title": "Add a title", "share_add_title": "Add a title",
+2 -2
View File
@@ -102,7 +102,7 @@
"backup_controller_page_status_off": "Automatic foreground backup is off", "backup_controller_page_status_off": "Automatic foreground backup is off",
"backup_controller_page_status_on": "Automatic foreground backup is on", "backup_controller_page_status_on": "Automatic foreground backup is on",
"backup_controller_page_storage_format": "{} of {} used", "backup_controller_page_storage_format": "{} of {} used",
"backup_controller_page_to_backup": "Albums to be backup", "backup_controller_page_to_backup": "Albums to be backed up",
"backup_controller_page_total": "Total", "backup_controller_page_total": "Total",
"backup_controller_page_total_sub": "All unique photos and videos from selected albums", "backup_controller_page_total_sub": "All unique photos and videos from selected albums",
"backup_controller_page_turn_off": "Turn off foreground backup", "backup_controller_page_turn_off": "Turn off foreground backup",
@@ -407,10 +407,10 @@
"setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)",
"setting_notifications_total_progress_title": "Show background backup total progress", "setting_notifications_total_progress_title": "Show background backup total progress",
"setting_pages_app_bar_settings": "Settings", "setting_pages_app_bar_settings": "Settings",
"settings_require_restart": "Please restart Immich to apply this setting",
"setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.", "setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
"setting_video_viewer_looping_title": "Looping", "setting_video_viewer_looping_title": "Looping",
"setting_video_viewer_title": "Videos", "setting_video_viewer_title": "Videos",
"settings_require_restart": "Please restart Immich to apply this setting",
"share_add": "Add", "share_add": "Add",
"share_add_photos": "Add photos", "share_add_photos": "Add photos",
"share_add_title": "Add a title", "share_add_title": "Add a title",
+4
View File
@@ -391,6 +391,7 @@
"setting_image_viewer_original_title": "Cargar imagen original", "setting_image_viewer_original_title": "Cargar imagen original",
"setting_image_viewer_preview_subtitle": "Activar para cargar una imagen de resolución media. Deshabilitar para cargar directamente la imagen original o usar una miniatura.", "setting_image_viewer_preview_subtitle": "Activar para cargar una imagen de resolución media. Deshabilitar para cargar directamente la imagen original o usar una miniatura.",
"setting_image_viewer_preview_title": "Cargar imagen de previsualización", "setting_image_viewer_preview_title": "Cargar imagen de previsualización",
"setting_image_viewer_title": "Images",
"setting_languages_apply": "Aplicar", "setting_languages_apply": "Aplicar",
"setting_languages_title": "Idiomas", "setting_languages_title": "Idiomas",
"setting_notifications_notify_failures_grace_period": "Notificar fallos de copia de seguridad en segundo plano: {}", "setting_notifications_notify_failures_grace_period": "Notificar fallos de copia de seguridad en segundo plano: {}",
@@ -407,6 +408,9 @@
"setting_notifications_total_progress_title": "Mostrar progreso total de copia de seguridad en segundo plano", "setting_notifications_total_progress_title": "Mostrar progreso total de copia de seguridad en segundo plano",
"setting_pages_app_bar_settings": "Ajustes", "setting_pages_app_bar_settings": "Ajustes",
"settings_require_restart": "Por favor, reinicia Immich para aplicar este ajuste", "settings_require_restart": "Por favor, reinicia Immich para aplicar este ajuste",
"setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
"setting_video_viewer_looping_title": "Looping",
"setting_video_viewer_title": "Videos",
"share_add": "Agregar", "share_add": "Agregar",
"share_add_photos": "Agregar fotos", "share_add_photos": "Agregar fotos",
"share_add_title": "Agregar un título", "share_add_title": "Agregar un título",
+4
View File
@@ -391,6 +391,7 @@
"setting_image_viewer_original_title": "Cargar imagen original", "setting_image_viewer_original_title": "Cargar imagen original",
"setting_image_viewer_preview_subtitle": "Activar para cargar una imagen de resolución media. Deshabilitar para cargar directamente la imagen original o usar una miniatura.", "setting_image_viewer_preview_subtitle": "Activar para cargar una imagen de resolución media. Deshabilitar para cargar directamente la imagen original o usar una miniatura.",
"setting_image_viewer_preview_title": "Cargar imagen de previsualización", "setting_image_viewer_preview_title": "Cargar imagen de previsualización",
"setting_image_viewer_title": "Images",
"setting_languages_apply": "Apply", "setting_languages_apply": "Apply",
"setting_languages_title": "Languages", "setting_languages_title": "Languages",
"setting_notifications_notify_failures_grace_period": "Notificar fallos de copia de seguridad en segundo plano: {}", "setting_notifications_notify_failures_grace_period": "Notificar fallos de copia de seguridad en segundo plano: {}",
@@ -407,6 +408,9 @@
"setting_notifications_total_progress_title": "Mostrar progreso total de copia de seguridad en segundo plano", "setting_notifications_total_progress_title": "Mostrar progreso total de copia de seguridad en segundo plano",
"setting_pages_app_bar_settings": "Ajustes", "setting_pages_app_bar_settings": "Ajustes",
"settings_require_restart": "Por favor, reinicia Immich para aplicar este ajuste", "settings_require_restart": "Por favor, reinicia Immich para aplicar este ajuste",
"setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
"setting_video_viewer_looping_title": "Looping",
"setting_video_viewer_title": "Videos",
"share_add": "Agregar", "share_add": "Agregar",
"share_add_photos": "Agregar fotos", "share_add_photos": "Agregar fotos",
"share_add_title": "Agregar un título", "share_add_title": "Agregar un título",
+4
View File
@@ -391,6 +391,7 @@
"setting_image_viewer_original_title": "Cargar imagen original", "setting_image_viewer_original_title": "Cargar imagen original",
"setting_image_viewer_preview_subtitle": "Activar para cargar una imagen de resolución media. Deshabilitar para cargar directamente la imagen original o usar una miniatura.", "setting_image_viewer_preview_subtitle": "Activar para cargar una imagen de resolución media. Deshabilitar para cargar directamente la imagen original o usar una miniatura.",
"setting_image_viewer_preview_title": "Cargar imagen de previsualización", "setting_image_viewer_preview_title": "Cargar imagen de previsualización",
"setting_image_viewer_title": "Images",
"setting_languages_apply": "Apply", "setting_languages_apply": "Apply",
"setting_languages_title": "Languages", "setting_languages_title": "Languages",
"setting_notifications_notify_failures_grace_period": "Notificar fallos de copia de seguridad en segundo plano: {}", "setting_notifications_notify_failures_grace_period": "Notificar fallos de copia de seguridad en segundo plano: {}",
@@ -407,6 +408,9 @@
"setting_notifications_total_progress_title": "Mostrar progreso total de copia de seguridad en segundo plano", "setting_notifications_total_progress_title": "Mostrar progreso total de copia de seguridad en segundo plano",
"setting_pages_app_bar_settings": "Ajustes", "setting_pages_app_bar_settings": "Ajustes",
"settings_require_restart": "Por favor, reinicia Immich para aplicar este ajuste", "settings_require_restart": "Por favor, reinicia Immich para aplicar este ajuste",
"setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
"setting_video_viewer_looping_title": "Looping",
"setting_video_viewer_title": "Videos",
"share_add": "Agregar", "share_add": "Agregar",
"share_add_photos": "Agregar fotos", "share_add_photos": "Agregar fotos",
"share_add_title": "Agregar un título", "share_add_title": "Agregar un título",
+4
View File
@@ -391,6 +391,7 @@
"setting_image_viewer_original_title": "Cargar imagen original", "setting_image_viewer_original_title": "Cargar imagen original",
"setting_image_viewer_preview_subtitle": "Activar para cargar una imagen de resolución media. Deshabilitar para cargar directamente la imagen original o usar una miniatura.", "setting_image_viewer_preview_subtitle": "Activar para cargar una imagen de resolución media. Deshabilitar para cargar directamente la imagen original o usar una miniatura.",
"setting_image_viewer_preview_title": "Cargar imagen de previsualización", "setting_image_viewer_preview_title": "Cargar imagen de previsualización",
"setting_image_viewer_title": "Images",
"setting_languages_apply": "Apply", "setting_languages_apply": "Apply",
"setting_languages_title": "Languages", "setting_languages_title": "Languages",
"setting_notifications_notify_failures_grace_period": "Notificar fallos de copia de seguridad en segundo plano: {}", "setting_notifications_notify_failures_grace_period": "Notificar fallos de copia de seguridad en segundo plano: {}",
@@ -407,6 +408,9 @@
"setting_notifications_total_progress_title": "Mostrar progreso total de copia de seguridad en segundo plano", "setting_notifications_total_progress_title": "Mostrar progreso total de copia de seguridad en segundo plano",
"setting_pages_app_bar_settings": "Configuración", "setting_pages_app_bar_settings": "Configuración",
"settings_require_restart": "Por favor, reinicia Immich para aplicar este ajuste", "settings_require_restart": "Por favor, reinicia Immich para aplicar este ajuste",
"setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
"setting_video_viewer_looping_title": "Looping",
"setting_video_viewer_title": "Videos",
"share_add": "Agregar", "share_add": "Agregar",
"share_add_photos": "Agregar fotos", "share_add_photos": "Agregar fotos",
"share_add_title": "Agregar un título", "share_add_title": "Agregar un título",
+6 -2
View File
@@ -218,7 +218,7 @@
"home_page_share_err_local": "Paikallisia kohteita ei voitu jakaa linkkien avulla. Hypätään yli", "home_page_share_err_local": "Paikallisia kohteita ei voitu jakaa linkkien avulla. Hypätään yli",
"home_page_upload_err_limit": "Voit lähettää palvelimelle enintään 30 kohdetta kerrallaan, ohitetaan", "home_page_upload_err_limit": "Voit lähettää palvelimelle enintään 30 kohdetta kerrallaan, ohitetaan",
"image_viewer_page_state_provider_download_error": "Lataus epäonnistui", "image_viewer_page_state_provider_download_error": "Lataus epäonnistui",
"image_viewer_page_state_provider_download_started": "Download Started", "image_viewer_page_state_provider_download_started": "Lataaminen aloitettu",
"image_viewer_page_state_provider_download_success": "Lataus onnistui", "image_viewer_page_state_provider_download_success": "Lataus onnistui",
"image_viewer_page_state_provider_share_error": "Jakovirhe", "image_viewer_page_state_provider_share_error": "Jakovirhe",
"library_page_albums": "Albumit", "library_page_albums": "Albumit",
@@ -391,6 +391,7 @@
"setting_image_viewer_original_title": "Lataa alkuperäinen kuva", "setting_image_viewer_original_title": "Lataa alkuperäinen kuva",
"setting_image_viewer_preview_subtitle": "Ota käyttöön ladataksesi keskitarkkuuksinen kuva. Poista käytöstä ladataksesi alkuperäinen kuva tai käyttääksesi vain esikatselukuvaa.", "setting_image_viewer_preview_subtitle": "Ota käyttöön ladataksesi keskitarkkuuksinen kuva. Poista käytöstä ladataksesi alkuperäinen kuva tai käyttääksesi vain esikatselukuvaa.",
"setting_image_viewer_preview_title": "Lataa esikatselukuva", "setting_image_viewer_preview_title": "Lataa esikatselukuva",
"setting_image_viewer_title": "Kuvat",
"setting_languages_apply": "Käytä", "setting_languages_apply": "Käytä",
"setting_languages_title": "Kieli", "setting_languages_title": "Kieli",
"setting_notifications_notify_failures_grace_period": "Ilmoita taustavarmuuskopioinnin epäonnistumisista: {}", "setting_notifications_notify_failures_grace_period": "Ilmoita taustavarmuuskopioinnin epäonnistumisista: {}",
@@ -407,10 +408,13 @@
"setting_notifications_total_progress_title": "Näytä taustavarmuuskopioinnin kokonaisedistyminen", "setting_notifications_total_progress_title": "Näytä taustavarmuuskopioinnin kokonaisedistyminen",
"setting_pages_app_bar_settings": "Asetukset", "setting_pages_app_bar_settings": "Asetukset",
"settings_require_restart": "Käynnistä Immich uudelleen ottaaksesti tämän asetuksen käyttöön", "settings_require_restart": "Käynnistä Immich uudelleen ottaaksesti tämän asetuksen käyttöön",
"setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
"setting_video_viewer_looping_title": "Looping",
"setting_video_viewer_title": "Videot",
"share_add": "Lisää", "share_add": "Lisää",
"share_add_photos": "Lisää kuvia", "share_add_photos": "Lisää kuvia",
"share_add_title": "Lisää nimi", "share_add_title": "Lisää nimi",
"share_assets_selected": "{} selected", "share_assets_selected": "{} valittu",
"share_create_album": "Luo albumi", "share_create_album": "Luo albumi",
"shared_album_activities_input_disable": "Kommentointi on kytketty pois päältä", "shared_album_activities_input_disable": "Kommentointi on kytketty pois päältä",
"shared_album_activities_input_hint": "Sano jotain", "shared_album_activities_input_hint": "Sano jotain",
+4
View File
@@ -391,6 +391,7 @@
"setting_image_viewer_original_title": "Charger l'image originale", "setting_image_viewer_original_title": "Charger l'image originale",
"setting_image_viewer_preview_subtitle": "Activer pour charger une image de résolution moyenne. Désactiver pour charger directement l'original ou utiliser uniquement la vignette.", "setting_image_viewer_preview_subtitle": "Activer pour charger une image de résolution moyenne. Désactiver pour charger directement l'original ou utiliser uniquement la vignette.",
"setting_image_viewer_preview_title": "Charger l'image d'aperçu", "setting_image_viewer_preview_title": "Charger l'image d'aperçu",
"setting_image_viewer_title": "Images",
"setting_languages_apply": "Apply", "setting_languages_apply": "Apply",
"setting_languages_title": "Languages", "setting_languages_title": "Languages",
"setting_notifications_notify_failures_grace_period": "Notifier les échecs de la sauvegarde en arrière-plan: {}", "setting_notifications_notify_failures_grace_period": "Notifier les échecs de la sauvegarde en arrière-plan: {}",
@@ -407,6 +408,9 @@
"setting_notifications_total_progress_title": "Afficher la progression totale de la sauvegarde en arrière-plan", "setting_notifications_total_progress_title": "Afficher la progression totale de la sauvegarde en arrière-plan",
"setting_pages_app_bar_settings": "Paramètres", "setting_pages_app_bar_settings": "Paramètres",
"settings_require_restart": "Veuillez redémarrer Immich pour appliquer ce paramètre", "settings_require_restart": "Veuillez redémarrer Immich pour appliquer ce paramètre",
"setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
"setting_video_viewer_looping_title": "Looping",
"setting_video_viewer_title": "Videos",
"share_add": "Ajouter", "share_add": "Ajouter",
"share_add_photos": "Ajouter des photos", "share_add_photos": "Ajouter des photos",
"share_add_title": "Ajouter un titre", "share_add_title": "Ajouter un titre",
+33 -29
View File
@@ -1,8 +1,8 @@
{ {
"action_common_back": "Back", "action_common_back": "Retour",
"action_common_cancel": "Annuler", "action_common_cancel": "Annuler",
"action_common_clear": "Clear", "action_common_clear": "Vider",
"action_common_confirm": "Confirm", "action_common_confirm": "Confirmer",
"action_common_update": "Mise à jour", "action_common_update": "Mise à jour",
"add_to_album_bottom_sheet_added": "Ajouté à {album}", "add_to_album_bottom_sheet_added": "Ajouté à {album}",
"add_to_album_bottom_sheet_already_exists": "Déjà dans {album}", "add_to_album_bottom_sheet_already_exists": "Déjà dans {album}",
@@ -22,7 +22,7 @@
"album_thumbnail_card_shared": " · Partagé", "album_thumbnail_card_shared": " · Partagé",
"album_thumbnail_owned": "Possédé", "album_thumbnail_owned": "Possédé",
"album_thumbnail_shared_by": "Partagé par {}", "album_thumbnail_shared_by": "Partagé par {}",
"album_viewer_appbar_delete_confirm": "Are you sure you want to delete this album from your account?", "album_viewer_appbar_delete_confirm": "Êtes-vous sur de vouloir supprimer cet album de votre compte?",
"album_viewer_appbar_share_delete": "Supprimer l'album", "album_viewer_appbar_share_delete": "Supprimer l'album",
"album_viewer_appbar_share_err_delete": "Échec de la suppression de l'album", "album_viewer_appbar_share_err_delete": "Échec de la suppression de l'album",
"album_viewer_appbar_share_err_leave": "Impossible de quitter l'album", "album_viewer_appbar_share_err_leave": "Impossible de quitter l'album",
@@ -41,7 +41,7 @@
"archive_page_title": "Archive ({})", "archive_page_title": "Archive ({})",
"asset_action_delete_err_read_only": "Impossible de supprimer le(s) élément(s) en lecture seule.", "asset_action_delete_err_read_only": "Impossible de supprimer le(s) élément(s) en lecture seule.",
"asset_action_share_err_offline": "Impossible de récupérer le(s) élément(s) hors ligne.", "asset_action_share_err_offline": "Impossible de récupérer le(s) élément(s) hors ligne.",
"asset_list_group_by_sub_title": "Groupe par", "asset_list_group_by_sub_title": "Regrouper par",
"asset_list_layout_settings_dynamic_layout_title": "Affichage dynamique", "asset_list_layout_settings_dynamic_layout_title": "Affichage dynamique",
"asset_list_layout_settings_group_automatically": "Automatique", "asset_list_layout_settings_group_automatically": "Automatique",
"asset_list_layout_settings_group_by": "Grouper les éléments par", "asset_list_layout_settings_group_by": "Grouper les éléments par",
@@ -115,7 +115,7 @@
"backup_manual_in_progress": "Téléchargement déjà en cours. Essayez après un instant", "backup_manual_in_progress": "Téléchargement déjà en cours. Essayez après un instant",
"backup_manual_success": "Succès ", "backup_manual_success": "Succès ",
"backup_manual_title": "Statut du téléchargement ", "backup_manual_title": "Statut du téléchargement ",
"backup_options_page_title": "Backup options", "backup_options_page_title": "Options de sauvegarde",
"cache_settings_album_thumbnails": "Miniatures de la page bibliothèque ({} éléments)", "cache_settings_album_thumbnails": "Miniatures de la page bibliothèque ({} éléments)",
"cache_settings_clear_cache_button": "Effacer le cache", "cache_settings_clear_cache_button": "Effacer le cache",
"cache_settings_clear_cache_button_title": "Efface le cache de l'application. Cela aura un impact significatif sur les performances de l'application jusqu'à ce que le cache soit reconstruit.", "cache_settings_clear_cache_button_title": "Efface le cache de l'application. Cela aura un impact significatif sur les performances de l'application jusqu'à ce que le cache soit reconstruit.",
@@ -218,7 +218,7 @@
"home_page_share_err_local": "Impossible de partager par lien les médias locaux, cette opération est donc ignorée.", "home_page_share_err_local": "Impossible de partager par lien les médias locaux, cette opération est donc ignorée.",
"home_page_upload_err_limit": "Limite de téléchargement de 30 éléments en même temps, demande ignorée", "home_page_upload_err_limit": "Limite de téléchargement de 30 éléments en même temps, demande ignorée",
"image_viewer_page_state_provider_download_error": "Erreur de téléchargement", "image_viewer_page_state_provider_download_error": "Erreur de téléchargement",
"image_viewer_page_state_provider_download_started": "Download Started", "image_viewer_page_state_provider_download_started": "Téléchargement Démarré",
"image_viewer_page_state_provider_download_success": "Téléchargement réussi", "image_viewer_page_state_provider_download_success": "Téléchargement réussi",
"image_viewer_page_state_provider_share_error": "Erreur de partage", "image_viewer_page_state_provider_share_error": "Erreur de partage",
"library_page_albums": "Albums", "library_page_albums": "Albums",
@@ -286,7 +286,7 @@
"map_settings_dialog_save": "Sauvegarder", "map_settings_dialog_save": "Sauvegarder",
"map_settings_dialog_title": "Paramètres de la carte", "map_settings_dialog_title": "Paramètres de la carte",
"map_settings_include_show_archived": "Inclure les archives", "map_settings_include_show_archived": "Inclure les archives",
"map_settings_include_show_partners": "Include Partners", "map_settings_include_show_partners": "Inclure les partenaires",
"map_settings_only_relative_range": "Plage de dates", "map_settings_only_relative_range": "Plage de dates",
"map_settings_only_show_favorites": "Afficher uniquement les favoris", "map_settings_only_show_favorites": "Afficher uniquement les favoris",
"map_settings_theme_settings": "Thème de la carte", "map_settings_theme_settings": "Thème de la carte",
@@ -299,15 +299,15 @@
"motion_photos_page_title": "Photos avec mouvement", "motion_photos_page_title": "Photos avec mouvement",
"multiselect_grid_edit_date_time_err_read_only": "Impossible de modifier la date d'un élément d'actif en lecture seule.", "multiselect_grid_edit_date_time_err_read_only": "Impossible de modifier la date d'un élément d'actif en lecture seule.",
"multiselect_grid_edit_gps_err_read_only": "Impossible de modifier l'emplacement d'un élément en lecture seule.", "multiselect_grid_edit_gps_err_read_only": "Impossible de modifier l'emplacement d'un élément en lecture seule.",
"no_assets_to_show": "Aucun élément à afficher", "no_assets_to_show": "Aucuns éléments à afficher",
"notification_permission_dialog_cancel": "Annuler", "notification_permission_dialog_cancel": "Annuler",
"notification_permission_dialog_content": "Pour activer les notifications, allez dans Paramètres et sélectionnez Autoriser.", "notification_permission_dialog_content": "Pour activer les notifications, allez dans Paramètres et sélectionnez Autoriser.",
"notification_permission_dialog_settings": "Paramètres", "notification_permission_dialog_settings": "Paramètres",
"notification_permission_list_tile_content": "Accordez la permission d'activer les notifications.", "notification_permission_list_tile_content": "Accordez la permission d'activer les notifications.",
"notification_permission_list_tile_enable_button": "Activer les notifications", "notification_permission_list_tile_enable_button": "Activer les notifications",
"notification_permission_list_tile_title": "Permission de notification", "notification_permission_list_tile_title": "Permission de notification",
"partner_list_user_photos": "{user}'s photos", "partner_list_user_photos": "Photos de {user}",
"partner_list_view_all": "View all", "partner_list_view_all": "Voir tous",
"partner_page_add_partner": "Ajouter un partenaire", "partner_page_add_partner": "Ajouter un partenaire",
"partner_page_empty_message": "Vos photos ne sont pas encore partagées avec un partenaire.", "partner_page_empty_message": "Vos photos ne sont pas encore partagées avec un partenaire.",
"partner_page_no_more_users": "Plus d'utilisateurs à ajouter", "partner_page_no_more_users": "Plus d'utilisateurs à ajouter",
@@ -342,18 +342,18 @@
"recently_added_page_title": "Récemment ajouté", "recently_added_page_title": "Récemment ajouté",
"scaffold_body_error_occurred": "Une erreur s'est produite", "scaffold_body_error_occurred": "Une erreur s'est produite",
"search_bar_hint": "Rechercher vos photos", "search_bar_hint": "Rechercher vos photos",
"search_filter_apply": "Apply filter", "search_filter_apply": "Appliquer le filtre",
"search_filter_camera_make": "Make", "search_filter_camera_make": "Fabricant",
"search_filter_camera_model": "Model", "search_filter_camera_model": "Modéle",
"search_filter_display_option_archive": "Archive", "search_filter_display_option_archive": "Achive",
"search_filter_display_option_favorite": "Favorite", "search_filter_display_option_favorite": "Favoris",
"search_filter_display_option_not_in_album": "Not in album", "search_filter_display_option_not_in_album": "Pas dans un album",
"search_filter_location_city": "City", "search_filter_location_city": "Ville",
"search_filter_location_country": "Country", "search_filter_location_country": "Pays",
"search_filter_location_state": "State", "search_filter_location_state": "Région",
"search_filter_media_type_all": "All", "search_filter_media_type_all": "Tous",
"search_filter_media_type_image": "Image", "search_filter_media_type_image": "Image",
"search_filter_media_type_video": "Video", "search_filter_media_type_video": "Vidéo",
"search_page_categories": "Catégories", "search_page_categories": "Catégories",
"search_page_favorites": "Favoris", "search_page_favorites": "Favoris",
"search_page_motion_photos": "Photos animées", "search_page_motion_photos": "Photos animées",
@@ -391,8 +391,9 @@
"setting_image_viewer_original_title": "Charger l'image originale", "setting_image_viewer_original_title": "Charger l'image originale",
"setting_image_viewer_preview_subtitle": "Activer pour charger une image de résolution moyenne. Désactiver pour charger directement l'original ou utiliser uniquement la miniature.", "setting_image_viewer_preview_subtitle": "Activer pour charger une image de résolution moyenne. Désactiver pour charger directement l'original ou utiliser uniquement la miniature.",
"setting_image_viewer_preview_title": "Charger l'image d'aperçu", "setting_image_viewer_preview_title": "Charger l'image d'aperçu",
"setting_languages_apply": "Apply", "setting_image_viewer_title": "Images",
"setting_languages_title": "Languages", "setting_languages_apply": "Appliquer",
"setting_languages_title": "Langues",
"setting_notifications_notify_failures_grace_period": "Notifier les échecs de la sauvegarde en arrière-plan : {}", "setting_notifications_notify_failures_grace_period": "Notifier les échecs de la sauvegarde en arrière-plan : {}",
"setting_notifications_notify_hours": "{} heures", "setting_notifications_notify_hours": "{} heures",
"setting_notifications_notify_immediately": "immédiatement", "setting_notifications_notify_immediately": "immédiatement",
@@ -407,10 +408,13 @@
"setting_notifications_total_progress_title": "Afficher la progression totale de la sauvegarde en arrière-plan", "setting_notifications_total_progress_title": "Afficher la progression totale de la sauvegarde en arrière-plan",
"setting_pages_app_bar_settings": "Paramètres", "setting_pages_app_bar_settings": "Paramètres",
"settings_require_restart": "Veuillez redémarrer Immich pour appliquer ce paramètre", "settings_require_restart": "Veuillez redémarrer Immich pour appliquer ce paramètre",
"setting_video_viewer_looping_subtitle": "Activer pour voir les vidéos en boucle dans le lecteur détaillé",
"setting_video_viewer_looping_title": "Boucle",
"setting_video_viewer_title": "Vidéos",
"share_add": "Ajouter", "share_add": "Ajouter",
"share_add_photos": "Ajouter des photos", "share_add_photos": "Ajouter des photos",
"share_add_title": "Ajouter un titre", "share_add_title": "Ajouter un titre",
"share_assets_selected": "{} selected", "share_assets_selected": "{} séléctionné(s)",
"share_create_album": "Créer un album", "share_create_album": "Créer un album",
"shared_album_activities_input_disable": "Les commentaires sont désactivés", "shared_album_activities_input_disable": "Les commentaires sont désactivés",
"shared_album_activities_input_hint": "Dire quelque chose", "shared_album_activities_input_hint": "Dire quelque chose",
@@ -444,9 +448,9 @@
"shared_link_edit_expire_after_option_hours": "{} heures", "shared_link_edit_expire_after_option_hours": "{} heures",
"shared_link_edit_expire_after_option_minute": "1 minute", "shared_link_edit_expire_after_option_minute": "1 minute",
"shared_link_edit_expire_after_option_minutes": "{} minutes", "shared_link_edit_expire_after_option_minutes": "{} minutes",
"shared_link_edit_expire_after_option_months": "{} months", "shared_link_edit_expire_after_option_months": "{} mois",
"shared_link_edit_expire_after_option_never": "Jamais", "shared_link_edit_expire_after_option_never": "Jamais",
"shared_link_edit_expire_after_option_year": "{} year", "shared_link_edit_expire_after_option_year": "{} ans",
"shared_link_edit_password": "Mot de passe", "shared_link_edit_password": "Mot de passe",
"shared_link_edit_password_hint": "Saisir le mot de passe de partage", "shared_link_edit_password_hint": "Saisir le mot de passe de partage",
"shared_link_edit_show_meta": "Afficher les métadonnées", "shared_link_edit_show_meta": "Afficher les métadonnées",
@@ -463,12 +467,12 @@
"shared_link_expires_never": "Expire ∞", "shared_link_expires_never": "Expire ∞",
"shared_link_expires_second": "Expire dans {} seconde", "shared_link_expires_second": "Expire dans {} seconde",
"shared_link_expires_seconds": "Expire dans {} secondes", "shared_link_expires_seconds": "Expire dans {} secondes",
"shared_link_individual_shared": "Individual shared", "shared_link_individual_shared": "Partagé individuellement",
"shared_link_info_chip_download": "Téléchargement", "shared_link_info_chip_download": "Téléchargement",
"shared_link_info_chip_metadata": "EXIF", "shared_link_info_chip_metadata": "EXIF",
"shared_link_info_chip_upload": "Chargement", "shared_link_info_chip_upload": "Chargement",
"shared_link_manage_links": "Gérer les liens partagés", "shared_link_manage_links": "Gérer les liens partagés",
"shared_link_public_album": "Public album", "shared_link_public_album": "Album public",
"share_done": "Fait", "share_done": "Fait",
"share_invite": "Inviter à l'album", "share_invite": "Inviter à l'album",
"sharing_page_album": "Albums partagés", "sharing_page_album": "Albums partagés",
+9 -5
View File
@@ -82,7 +82,7 @@
"backup_controller_page_background_turn_off": "כבה שירות ברקע", "backup_controller_page_background_turn_off": "כבה שירות ברקע",
"backup_controller_page_background_turn_on": "הפעל שירות ברקע", "backup_controller_page_background_turn_on": "הפעל שירות ברקע",
"backup_controller_page_background_wifi": "רק ברשת אלחוטית", "backup_controller_page_background_wifi": "רק ברשת אלחוטית",
"backup_controller_page_backup": יבוי", "backup_controller_page_backup": ובו",
"backup_controller_page_backup_selected": "נבחרו:", "backup_controller_page_backup_selected": "נבחרו:",
"backup_controller_page_backup_sub": "תמונות וסרטונים מגובים", "backup_controller_page_backup_sub": "תמונות וסרטונים מגובים",
"backup_controller_page_cancel": "ביטול", "backup_controller_page_cancel": "ביטול",
@@ -94,7 +94,7 @@
"backup_controller_page_id": "מזהה: {}", "backup_controller_page_id": "מזהה: {}",
"backup_controller_page_info": "פרטי גיבוי", "backup_controller_page_info": "פרטי גיבוי",
"backup_controller_page_none_selected": "לא נבחרו", "backup_controller_page_none_selected": "לא נבחרו",
"backup_controller_page_remainder": "יתרה", "backup_controller_page_remainder": "בתור לגיבוי",
"backup_controller_page_remainder_sub": "תמונות וסרטונים שנותרו לגבות מתוך בחירה", "backup_controller_page_remainder_sub": "תמונות וסרטונים שנותרו לגבות מתוך בחירה",
"backup_controller_page_select": "בחר", "backup_controller_page_select": "בחר",
"backup_controller_page_server_storage": "אחסון שרת", "backup_controller_page_server_storage": "אחסון שרת",
@@ -107,7 +107,7 @@
"backup_controller_page_total_sub": "כל התמונות והסרטונים הייחודיים מאלבומים שנבחרו", "backup_controller_page_total_sub": "כל התמונות והסרטונים הייחודיים מאלבומים שנבחרו",
"backup_controller_page_turn_off": "כבה גיבוי חזית", "backup_controller_page_turn_off": "כבה גיבוי חזית",
"backup_controller_page_turn_on": "הפעל גיבוי חזית", "backup_controller_page_turn_on": "הפעל גיבוי חזית",
"backup_controller_page_uploading_file_info": עלה פרטי קובץ", "backup_controller_page_uploading_file_info": ידע על הקובץ",
"backup_err_only_album": "לא ניתן להסיר את האלבום היחידי", "backup_err_only_album": "לא ניתן להסיר את האלבום היחידי",
"backup_info_card_assets": "נכסים", "backup_info_card_assets": "נכסים",
"backup_manual_cancelled": "בוטל", "backup_manual_cancelled": "בוטל",
@@ -218,7 +218,7 @@
"home_page_share_err_local": "לא ניתן לשתף נכסים מקומיים על ידי קישור, מדלג", "home_page_share_err_local": "לא ניתן לשתף נכסים מקומיים על ידי קישור, מדלג",
"home_page_upload_err_limit": "יכול רק להעלות מקסימום של 30 נכסים בכל פעם, מדלג", "home_page_upload_err_limit": "יכול רק להעלות מקסימום של 30 נכסים בכל פעם, מדלג",
"image_viewer_page_state_provider_download_error": "שגיאת הורדה", "image_viewer_page_state_provider_download_error": "שגיאת הורדה",
"image_viewer_page_state_provider_download_started": "Download Started", "image_viewer_page_state_provider_download_started": "ההורדה החלה",
"image_viewer_page_state_provider_download_success": "הצלחת הורדה", "image_viewer_page_state_provider_download_success": "הצלחת הורדה",
"image_viewer_page_state_provider_share_error": "שיתוף שגיאה", "image_viewer_page_state_provider_share_error": "שיתוף שגיאה",
"library_page_albums": "אלבומים", "library_page_albums": "אלבומים",
@@ -391,6 +391,7 @@
"setting_image_viewer_original_title": "טען תמונה מקורית", "setting_image_viewer_original_title": "טען תמונה מקורית",
"setting_image_viewer_preview_subtitle": "אפשר לטעון תמונה ברזלוציה בינונית. השבת כדי או לטעון את המקורית או רק להשתמש בתמונה הממוזערת.", "setting_image_viewer_preview_subtitle": "אפשר לטעון תמונה ברזלוציה בינונית. השבת כדי או לטעון את המקורית או רק להשתמש בתמונה הממוזערת.",
"setting_image_viewer_preview_title": "טען תמונת תצוגה מקדימה", "setting_image_viewer_preview_title": "טען תמונת תצוגה מקדימה",
"setting_image_viewer_title": "תמונות",
"setting_languages_apply": "החל", "setting_languages_apply": "החל",
"setting_languages_title": "שפות", "setting_languages_title": "שפות",
"setting_notifications_notify_failures_grace_period": "להודיע על כשלים בגיבוי ברקע: {}", "setting_notifications_notify_failures_grace_period": "להודיע על כשלים בגיבוי ברקע: {}",
@@ -407,10 +408,13 @@
"setting_notifications_total_progress_title": "הראה סה״כ התקדמות גיבוי ברקע", "setting_notifications_total_progress_title": "הראה סה״כ התקדמות גיבוי ברקע",
"setting_pages_app_bar_settings": "הגדרות", "setting_pages_app_bar_settings": "הגדרות",
"settings_require_restart": "אנא הפעל מחדש את Immich כדי להחיל הגדרה זו", "settings_require_restart": "אנא הפעל מחדש את Immich כדי להחיל הגדרה זו",
"setting_video_viewer_looping_subtitle": "אפשר וידיאו ברצף אוטומטית בחלון המידע",
"setting_video_viewer_looping_title": "לולאה",
"setting_video_viewer_title": "סרטונים",
"share_add": "הוסף", "share_add": "הוסף",
"share_add_photos": "הוסף תמונות", "share_add_photos": "הוסף תמונות",
"share_add_title": "הוסף כותרת", "share_add_title": "הוסף כותרת",
"share_assets_selected": "{} selected", "share_assets_selected": "{} נבחרו",
"share_create_album": "צור אלבום", "share_create_album": "צור אלבום",
"shared_album_activities_input_disable": "התגובה מושבתת", "shared_album_activities_input_disable": "התגובה מושבתת",
"shared_album_activities_input_hint": "הגב/י משהו", "shared_album_activities_input_hint": "הגב/י משהו",
+5 -1
View File
@@ -102,7 +102,7 @@
"backup_controller_page_status_off": "Automatic foreground backup is off", "backup_controller_page_status_off": "Automatic foreground backup is off",
"backup_controller_page_status_on": "Automatic foreground backup is on", "backup_controller_page_status_on": "Automatic foreground backup is on",
"backup_controller_page_storage_format": "{} of {} used", "backup_controller_page_storage_format": "{} of {} used",
"backup_controller_page_to_backup": "Albums to be backup", "backup_controller_page_to_backup": "Albums to be backed up",
"backup_controller_page_total": "Total", "backup_controller_page_total": "Total",
"backup_controller_page_total_sub": "All unique photos and videos from selected albums", "backup_controller_page_total_sub": "All unique photos and videos from selected albums",
"backup_controller_page_turn_off": "Turn off foreground backup", "backup_controller_page_turn_off": "Turn off foreground backup",
@@ -391,6 +391,7 @@
"setting_image_viewer_original_title": "Load original image", "setting_image_viewer_original_title": "Load original image",
"setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.", "setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.",
"setting_image_viewer_preview_title": "Load preview image", "setting_image_viewer_preview_title": "Load preview image",
"setting_image_viewer_title": "Images",
"setting_languages_apply": "Apply", "setting_languages_apply": "Apply",
"setting_languages_title": "Languages", "setting_languages_title": "Languages",
"setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
@@ -407,6 +408,9 @@
"setting_notifications_total_progress_title": "Show background backup total progress", "setting_notifications_total_progress_title": "Show background backup total progress",
"setting_pages_app_bar_settings": "Settings", "setting_pages_app_bar_settings": "Settings",
"settings_require_restart": "Please restart Immich to apply this setting", "settings_require_restart": "Please restart Immich to apply this setting",
"setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
"setting_video_viewer_looping_title": "Looping",
"setting_video_viewer_title": "Videos",
"share_add": "Add", "share_add": "Add",
"share_add_photos": "Add photos", "share_add_photos": "Add photos",
"share_add_title": "Add a title", "share_add_title": "Add a title",
+10 -6
View File
@@ -194,7 +194,7 @@
"exif_bottom_sheet_location": "HELY", "exif_bottom_sheet_location": "HELY",
"exif_bottom_sheet_location_add": "Hely hozzáadása", "exif_bottom_sheet_location_add": "Hely hozzáadása",
"exif_bottom_sheet_people": "EMBEREK", "exif_bottom_sheet_people": "EMBEREK",
"exif_bottom_sheet_person_add_person": "Név hozzáadása", "exif_bottom_sheet_person_add_person": "Elnevez",
"experimental_settings_new_asset_list_subtitle": "Fejlesztés alatt", "experimental_settings_new_asset_list_subtitle": "Fejlesztés alatt",
"experimental_settings_new_asset_list_title": "Kisérleti képrács engedélyezése", "experimental_settings_new_asset_list_title": "Kisérleti képrács engedélyezése",
"experimental_settings_subtitle": "Csak saját felelősségre használd!", "experimental_settings_subtitle": "Csak saját felelősségre használd!",
@@ -363,9 +363,9 @@
"search_page_person_add_name_dialog_cancel": "Mégsem", "search_page_person_add_name_dialog_cancel": "Mégsem",
"search_page_person_add_name_dialog_hint": "Név", "search_page_person_add_name_dialog_hint": "Név",
"search_page_person_add_name_dialog_save": "Mentés", "search_page_person_add_name_dialog_save": "Mentés",
"search_page_person_add_name_dialog_title": "Név hozzáadása", "search_page_person_add_name_dialog_title": "Elnevez",
"search_page_person_add_name_subtitle": "Név szerint gyorsan megtalálhatod a keresőben", "search_page_person_add_name_subtitle": "Név szerint gyorsan megtalálhatod a keresőben",
"search_page_person_add_name_title": "Név hozzáadása", "search_page_person_add_name_title": "Elnevez",
"search_page_person_edit_name": "Név módosítása", "search_page_person_edit_name": "Név módosítása",
"search_page_places": "Helyek", "search_page_places": "Helyek",
"search_page_recently_added": "Nemrég hozzáadott", "search_page_recently_added": "Nemrég hozzáadott",
@@ -374,8 +374,8 @@
"search_page_things": "Dolgok", "search_page_things": "Dolgok",
"search_page_videos": "Videók", "search_page_videos": "Videók",
"search_page_view_all_button": "Összes mutatása", "search_page_view_all_button": "Összes mutatása",
"search_page_your_activity": "Tevékenységeid", "search_page_your_activity": "Tevékenységek",
"search_page_your_map": "Térképed", "search_page_your_map": "Térkép",
"search_result_page_new_search_hint": "Új keresés", "search_result_page_new_search_hint": "Új keresés",
"search_suggestion_list_smart_search_hint_1": "Az intelligens keresés alapértelmezetten be van kapcsolva, metaadatokat így kereshetsz", "search_suggestion_list_smart_search_hint_1": "Az intelligens keresés alapértelmezetten be van kapcsolva, metaadatokat így kereshetsz",
"search_suggestion_list_smart_search_hint_2": "m:keresési-kifejezés", "search_suggestion_list_smart_search_hint_2": "m:keresési-kifejezés",
@@ -386,11 +386,12 @@
"server_info_box_latest_release": "Legfrissebb Verzió", "server_info_box_latest_release": "Legfrissebb Verzió",
"server_info_box_server_url": "Szerver Címe", "server_info_box_server_url": "Szerver Címe",
"server_info_box_server_version": "Szerver Verzió", "server_info_box_server_version": "Szerver Verzió",
"setting_image_viewer_help": "A képnézegető először a kis bélyegképet tölti be, aztán a közepes méretű előnézetet (ha elérhető), végül az eredetit (ha elérhető).", "setting_image_viewer_help": "Az Elem Megjelenítő először a kis bélyegképet tölti be, aztán a közepes méretű előnézetet (ha elérhető), végül az eredetit (ha elérhető).",
"setting_image_viewer_original_subtitle": "Engedélyezi az eredeti teljes felbontású kép betöltését (nagy!). Kikapcsolva csökkenti az adathasználatot (a neten és az eszköz gyorsítótárán is).", "setting_image_viewer_original_subtitle": "Engedélyezi az eredeti teljes felbontású kép betöltését (nagy!). Kikapcsolva csökkenti az adathasználatot (a neten és az eszköz gyorsítótárán is).",
"setting_image_viewer_original_title": "Eredeti kép betöltése", "setting_image_viewer_original_title": "Eredeti kép betöltése",
"setting_image_viewer_preview_subtitle": "Engedélyezi a közepes felbontású kép betöltését. Kikapcsolva vagy az eredeti kép töltődik be, vagy csak a bélyegkép.", "setting_image_viewer_preview_subtitle": "Engedélyezi a közepes felbontású kép betöltését. Kikapcsolva vagy az eredeti kép töltődik be, vagy csak a bélyegkép.",
"setting_image_viewer_preview_title": "Előnézet betöltése", "setting_image_viewer_preview_title": "Előnézet betöltése",
"setting_image_viewer_title": "Képek",
"setting_languages_apply": "Alkalmaz", "setting_languages_apply": "Alkalmaz",
"setting_languages_title": "Nyelvek", "setting_languages_title": "Nyelvek",
"setting_notifications_notify_failures_grace_period": "Értesítés a háttérben történő mentés hibáiról: {}", "setting_notifications_notify_failures_grace_period": "Értesítés a háttérben történő mentés hibáiról: {}",
@@ -407,6 +408,9 @@
"setting_notifications_total_progress_title": "Mutassa a háttérben történő mentés teljes folyamatát", "setting_notifications_total_progress_title": "Mutassa a háttérben történő mentés teljes folyamatát",
"setting_pages_app_bar_settings": "Beállítások", "setting_pages_app_bar_settings": "Beállítások",
"settings_require_restart": "Ennek a beállításnak az érvénybe lépéséhez indítsd újra az Immich-et", "settings_require_restart": "Ennek a beállításnak az érvénybe lépéséhez indítsd újra az Immich-et",
"setting_video_viewer_looping_subtitle": "Engedélyezi a videók folyamatosan ismételt lejátszását az elem megjelenítőben",
"setting_video_viewer_looping_title": "Ismétlés",
"setting_video_viewer_title": "Videók",
"share_add": "Hozzáadás", "share_add": "Hozzáadás",
"share_add_photos": "Fotók hozzáadása", "share_add_photos": "Fotók hozzáadása",
"share_add_title": "Album neve", "share_add_title": "Album neve",
+4
View File
@@ -391,6 +391,7 @@
"setting_image_viewer_original_title": "Carica l'immagine originale", "setting_image_viewer_original_title": "Carica l'immagine originale",
"setting_image_viewer_preview_subtitle": "Abilita per caricare un'immagine a risoluzione media.\nDisabilita per caricare direttamente l'immagine originale o usare la thumbnail.", "setting_image_viewer_preview_subtitle": "Abilita per caricare un'immagine a risoluzione media.\nDisabilita per caricare direttamente l'immagine originale o usare la thumbnail.",
"setting_image_viewer_preview_title": "Carica immagine di anteprima", "setting_image_viewer_preview_title": "Carica immagine di anteprima",
"setting_image_viewer_title": "Images",
"setting_languages_apply": "Applica", "setting_languages_apply": "Applica",
"setting_languages_title": "Lingue", "setting_languages_title": "Lingue",
"setting_notifications_notify_failures_grace_period": "Notifica caricamenti falliti in background: {}", "setting_notifications_notify_failures_grace_period": "Notifica caricamenti falliti in background: {}",
@@ -407,6 +408,9 @@
"setting_notifications_total_progress_title": "Mostra avanzamento del backup in background", "setting_notifications_total_progress_title": "Mostra avanzamento del backup in background",
"setting_pages_app_bar_settings": "Impostazioni", "setting_pages_app_bar_settings": "Impostazioni",
"settings_require_restart": "Si prega di riavviare Immich perché vengano applicate le impostazioni", "settings_require_restart": "Si prega di riavviare Immich perché vengano applicate le impostazioni",
"setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
"setting_video_viewer_looping_title": "Looping",
"setting_video_viewer_title": "Videos",
"share_add": "Aggiungi", "share_add": "Aggiungi",
"share_add_photos": "Aggiungi foto", "share_add_photos": "Aggiungi foto",
"share_add_title": "Aggiungi un titolo ", "share_add_title": "Aggiungi un titolo ",

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