Author SHA1 Message Date
Codex 132af2fc74 Prepare local14 web and Android release
Build Mobile / pre-job (push) Canceled after 0s
CLI Build / CLI Publish (push) Canceled after 0s
CodeQL / Analyze (javascript) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
Docker / pre-job (push) Canceled after 0s
Docs build / pre-job (push) Canceled after 0s
Migration Order / Verify migration ORDER (push) Canceled after 0s
Static Code Analysis / pre-job (push) Canceled after 0s
Test / pre-job (push) Canceled after 0s
Test / ShellCheck (push) Canceled after 0s
Test / OpenAPI Clients (push) Canceled after 0s
Test / SQL Schema Checks (push) Canceled after 0s
Build Mobile / Build and sign Android (push) Canceled after 0s
Build Mobile / Build and sign iOS (push) Canceled after 0s
CLI Build / Docker (push) Canceled after 0s
Docker / Re-Tag ML (armnn, immich-machine-learning, linux/arm64, -armnn) (push) Canceled after 0s
Docker / Re-Tag ML (cpu, immich-machine-learning) (push) Canceled after 0s
Docker / Re-Tag ML (cuda, immich-machine-learning, linux/amd64, -cuda) (push) Canceled after 0s
Docker / Re-Tag ML (openvino, immich-machine-learning, linux/amd64, {"linux/amd64": "pokedex-large"}, -openvino) (push) Canceled after 0s
Docker / Re-Tag ML (rknn, immich-machine-learning, linux/arm64, -rknn) (push) Canceled after 0s
Docker / Re-Tag ML (rocm, immich-machine-learning, linux/amd64, {"linux/amd64": "pokedex-large"}, -rocm) (push) Canceled after 0s
Docker / Re-Tag Server (push) Canceled after 0s
Docker / Build and Push ML (armnn, immich-machine-learning, linux/arm64, -armnn) (push) Canceled after 0s
Docker / Build and Push ML (cpu, immich-machine-learning) (push) Canceled after 0s
Docker / Build and Push ML (cuda, immich-machine-learning, linux/amd64, -cuda) (push) Canceled after 0s
Docker / Build and Push ML (openvino, immich-machine-learning, linux/amd64, {"linux/amd64": "pokedex-large"}, -openvino) (push) Canceled after 0s
Docker / Build and Push ML (rknn, immich-machine-learning, linux/arm64, -rknn) (push) Canceled after 0s
Docker / Build and Push ML (rocm, immich-machine-learning, linux/amd64, {"linux/amd64": "pokedex-large"}, -rocm) (push) Canceled after 0s
Docker / Build and Push Server (push) Canceled after 0s
Docker / Mirror to Docker Hub (armnn, immich-machine-learning, linux/arm64, -armnn) (push) Canceled after 0s
Docker / Mirror to Docker Hub (cpu, immich-machine-learning) (push) Canceled after 0s
Docker / Mirror to Docker Hub (cuda, immich-machine-learning, linux/amd64, -cuda) (push) Canceled after 0s
Docker / Mirror to Docker Hub (immich-server) (push) Canceled after 0s
Docker / Mirror to Docker Hub (openvino, immich-machine-learning, linux/amd64, {"linux/amd64": "pokedex-large"}, -openvino) (push) Canceled after 0s
Docker / Mirror to Docker Hub (rknn, immich-machine-learning, linux/arm64, -rknn) (push) Canceled after 0s
Docker / Mirror to Docker Hub (rocm, immich-machine-learning, linux/amd64, {"linux/amd64": "pokedex-large"}, -rocm) (push) Canceled after 0s
Docker / Docker Build & Push Server Success (push) Canceled after 0s
Docker / Docker Build & Push ML Success (push) Canceled after 0s
Docs build / Docs Build (push) Canceled after 0s
Static Code Analysis / Run Dart Code Analysis (push) Canceled after 0s
Test / Scripts unit tests (push) Canceled after 0s
Test / Test & Lint Server (push) Canceled after 0s
Test / Unit Test CLI (push) Canceled after 0s
Test / Unit Test CLI (Windows) (push) Canceled after 0s
Test / Lint Web (push) Canceled after 0s
Test / Test Web (push) Canceled after 0s
Test / Test i18n (push) Canceled after 0s
Test / End-to-End Lint (push) Canceled after 0s
Test / Medium Tests (Server) (push) Canceled after 0s
Test / End-to-End Tests (Server & CLI) (ubuntu-24.04-arm) (push) Canceled after 0s
Test / End-to-End Tests (Server & CLI) (ubuntu-latest) (push) Canceled after 0s
Test / End-to-End Tests (Web) (ubuntu-24.04-arm) (push) Canceled after 0s
Test / End-to-End Tests (Web) (ubuntu-latest) (push) Canceled after 0s
Test / End-to-End Tests Success (push) Canceled after 0s
Test / Unit Test Mobile (push) Canceled after 0s
Test / Unit Test ML (push) Canceled after 0s
Test / .github Files Formatting (push) Canceled after 0s
2026-10-29 20:44:43 +03:00
Zack PollardandCodex 9e5bdda89c chore(deps): update base-image to v202610291416 (#32810)
(cherry picked from commit a0c5b32fb8bfd3c3660e3a5a1ee038d3997bdd5b)
2026-10-29 20:43:01 +03:00
Zack PollardandCodex 79ea67f6be feat(server): compress database backups with pigz when it is installed (#32806)
(cherry picked from commit 97af09ca3cbd7f08ea033f176e777c9a9de8fdb5)
2026-10-29 20:43:01 +03:00
BaneinaandCodex 79b175465e fix(server): normalize bounding boxes when matching detected faces to existing faces (#32486)
(cherry picked from commit 5583cd7857a414f10039a9fb5bf03867bd4390c8)
2026-10-29 20:43:01 +03:00
Bautrell12andCodex c76bd08f73 feat(web): enable the shortcut "f" to favorite/unfavorite in multiselection mode (#31411)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
(cherry picked from commit aeee9be333f4faac877b99cacd355329a5001c25)
2026-10-29 20:43:01 +03:00
Daniel DietzlerandCodex f2e05e9d98 chore: migrate oauth claim validation to zod (#32373)
(cherry picked from commit 0808f24a37cf376a509f1b4046cd52fd2d72b20d)
2026-10-29 20:43:01 +03:00
DawidKrynskiandCodex c8a957a116 fix(mobile): use context search once server features are loaded (#31800)
Co-authored-by: Dawid Krynski <188586034+DawidKrynski@users.noreply.github.com>
Co-authored-by: Santo Shakil <santoshakil@users.noreply.github.com>
(cherry picked from commit be8e42526700d9ec5e72061e33f5039d50e40bae)
2026-10-29 20:43:01 +03:00
Ethan FerrierandCodex f7697264ed fix(mobile): ocr delete by asset (#32611)
(cherry picked from commit 56ed326acda2220fc7e3b21b17b3c8d24daf99cc)
2026-10-29 20:43:01 +03:00
Jason RasmussenandCodex 648dea2072 chore: remove isar reference (#32800)
(cherry picked from commit 323e750eb50ee0728110c8a1c5902c63cbda5850)
2026-10-29 20:42:12 +03:00
renovate[bot]andCodex 895b755f53 chore(deps): update base-image to v202610271109 (#32799)
(cherry picked from commit a56303f393ebb3a16f191a6f9a442d15285398cb)
2026-10-29 20:42:12 +03:00
renovate[bot]andCodex 568a9d977e chore(deps): update dependency @immich/ui to v0.91.1 (#32798)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
(cherry picked from commit d5918c00420601f1ec97e6d67425c81958b3eb80)
2026-10-29 20:42:12 +03:00
Brent SaltzmanandCodex 3c383e46d1 fix: avoid duplicate manufacturer in camera name (#32657)
* fix(web,mobile): avoid duplicate manufacturer in camera name

Some cameras (e.g. Canon) already include the manufacturer in the EXIF
model, which made the details view show "Canon Canon EOS R6m2". Only
prepend the make when the model does not already start with it.

* refactor(web,mobile): address review feedback on camera name helper

- Use toLowerCase instead of toLocaleLowerCase for the comparison
- Hoist the whitespace regex to a static in the Dart helper
- Match any word of the make, so "EASTMAN KODAK COMPANY" with
  "KODAK EASYSHARE C875" is deduplicated too
- Compute the camera name once in DetailPanel

---------

Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
(cherry picked from commit cb2ea1e308d8a85f2f340f50b646f161d3528670)
2026-10-29 20:41:02 +03:00
xuanandCodex 477fcc2338 docs(i18n): remove the deleted star-history chart from the remaining 16 translated READMEs (#32549)
(cherry picked from commit d31befb8514fe2f0ade360b6477abb90a940fd73)
2026-10-29 20:41:02 +03:00
renovate[bot]andCodex ff52f491fb chore(deps): update dependency onnxruntime to v1.31.0 (#32582)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 257090aaaedf0d7dcff9e2295f55524f786dd9ae)
2026-10-29 20:41:02 +03:00
Valentin AgachiandCodex c3933169de feat(server): add OCR extraction workflow filter (#32464)
feat(server): add OCR workflow filter

(cherry picked from commit 2e0eb35e93dcf8468398d7015daa5ab231c7dd9c)
2026-10-29 20:41:01 +03:00
Codex 2c1f321627 Align local web integrations with updated lint rules and Cast types 2026-10-28 21:00:29 +03:00
Codex e8c1fd297d Prepare local13 release and align dependency lockfile 2026-10-28 20:46:38 +03:00
bo0tzzandCodex 8e3e3a48ae perf(server): only serialize exif for returned search rows (#32620)
(cherry picked from commit e51de38c40a29fd5eb1347cd991bbbafccfe62c6)
2026-10-28 20:41:41 +03:00
austinmdillowandCodex dc5553b239 feat: workflow filter by missing EXIF property (#32713)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
(cherry picked from commit a1a80cb1c416d3a480963a78d7b9df329d867bf6)
2026-10-28 20:41:41 +03:00
renovate[bot]andCodex 32e02a05d2 chore(deps): update terraform cloudflare to v5 (#32794)
(cherry picked from commit d8aaa950099f6687a8f6861f63466242a748ac1b)
2026-10-28 20:41:41 +03:00
shenlongandCodex c581bce715 fix: include partner shared assets for people even without showInTimeline (#32675)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
(cherry picked from commit 21ed7454836073a646273c7475b347e043439a90)
2026-10-28 20:41:41 +03:00
Daniel DietzlerandCodex dd23264385 chore: deploy docs when terraform changes (#32795)
(cherry picked from commit 4ea9c1f7c520f1e1c6b6f22968abdb7f9b4aa29f)
2026-10-28 20:41:41 +03:00
Jason RasmussenandCodex 6dbe72b6a1 fix: gc time for long-lived queries (#32793)
(cherry picked from commit 079568691580605e92c5d3c3c9037cfbd657f374)
2026-10-28 20:41:41 +03:00
Daniel DietzlerandCodex 72f96701f0 feat: cluster group admin permission for regenerate people (#32787)
(cherry picked from commit 9f273a604d5bdbd9583758c3a79d2fc0141c7a77)
2026-10-28 20:41:41 +03:00
Jason RasmussenandCodex b4ae7f0dcf feat: passkey feature level (#32791)
(cherry picked from commit f5d33ff6618510917fb9f7bce705ec9583a207fb)
2026-10-28 20:41:41 +03:00
renovate[bot]andCodex 09bb2a9fb5 fix(deps): update typescript-projects (#32759)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
(cherry picked from commit f26b9c83adb12848e94bcfc77e1b67be1756bdc9)
2026-10-28 20:41:41 +03:00
bo0tzzandCodex 8f2bfa099e fix(server): scope duplicate group resolution to the caller's assets (#32603)
(cherry picked from commit 7bb91afbe15e82502f8f6c92b61edb3865ce76f3)
2026-10-28 20:41:28 +03:00
bo0tzzandCodex 07083aec22 fix(server): anchor the folder view path match (#32612)
(cherry picked from commit c76e1517321df98df6e9b2f856493ac49ec05877)
2026-10-28 20:41:28 +03:00
bo0tzzandCodex 64d50dc4b7 perf(server): index asset (ownerId, updateId) for sync (#32624)
(cherry picked from commit 73603bcafe5d1e52434f736476d07c9578f3b888)
2026-10-28 20:41:28 +03:00
renovate[bot]andCodex 7fb7699c06 chore(deps): update dependency eslint-plugin-unicorn to v78 (#32588)
Co-authored-by: Jason Rasmussen <jason@rasm.me>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
(cherry picked from commit 72e2bc8b0bc4a7e0a84ac16c0bde7f5e538729fb)
2026-10-28 20:41:28 +03:00
Santo ShakilandCodex 3127dbec71 refactor(mobile): move the native core decoders into a codecs crate (#32765)
* refactor(mobile): move the native core decoders into a codecs crate

* make Format private

* rename the decode export and options for what they hold

* drop the unused ios and server groups and rename the target params

(cherry picked from commit 11bf875313782b8a9e10269bb6f85226b7bdcd72)
2026-10-28 20:40:53 +03:00
Horatiu George TodoranandCodex f418967d55 perf: RKNN half RAM model usage + no weight duplication in multi-core (#23958)
* perf(ml): share RKNN weights across NPU cores and halve model RAM

* perf(ml): default rknn threads to npu cores and cache shape setup

* perf(ml): serve rknn runs in arrival order

(cherry picked from commit e659fb62f2bb3f6b1db08ed643d3d054a539c415)
2026-10-28 20:40:53 +03:00
YarosandCodex 1f422ea4c4 fix(server): add album-shared assets to albums (#32609)
Co-authored-by: Jason Rasmussen <jason@rasm.me>
(cherry picked from commit 37c34343e1b98edf07dc3ff34c5da461de03c746)
2026-10-28 20:40:53 +03:00
Codex 8119a5ee22 Finalize local12 formatting and document guarded browser APIs 2026-10-27 23:27:18 +03:00
Codex 54881128d6 Adapt local features and dependency locks for SvelteKit 3 and local12 2026-10-27 23:17:33 +03:00
renovate[bot]andCodex d15be0dd83 chore(deps): update rust crate libc to v0.2.191 (#32753)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 12381a3b4bca8634f785c23cd4c6bf4063477b3b)
2026-10-27 23:12:52 +03:00
Eduard Ereza MartínezandCodex 47cc38061e fix(docs): update README_ca_ES.md to match the English version (#30300)
* Update README_ca_ES.md to match the English version

* fix(docs): update README_ca_ES.md to match the English version

(cherry picked from commit 0fe886d1b573d579fdb3fd243a477e0e6ac08f52)
2026-10-27 23:12:52 +03:00
MertandCodex db411a4497 feat(server): add jxl as target format (#32732)
(cherry picked from commit 9560989cb97fb93cd6ab74f7811acce133a9b768)
2026-10-27 23:12:52 +03:00
renovate[bot]andCodex f1a09c1389 chore(deps): update dependency fastapi to v0.160.0 (#32756)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit cadb945108d7a547845ba5279c745fca64ba2cf8)
2026-10-27 23:12:52 +03:00
renovate[bot]andCodex c8222bba94 chore(deps): update dependency terragrunt to v1.2.0 (#32757)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit e50ada17c817499a131eefb58a112850dfd22ea6)
2026-10-27 23:12:51 +03:00
TimonandCodex af66681b99 chore: bump openapi-generator version to v7.26 (#32454)
(cherry picked from commit b3c1b3d260b0186a9d6e82277f0e57a6e6d88738)
2026-10-27 23:12:42 +03:00
bo0tzzandCodex a93bbdf3e7 feat: inline hashes in helmet CSP (#30529)
* feat: inline hashes in helmet CSP

* tests

* fix: svelte3 merge

* chore: clean up

---------

Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
(cherry picked from commit 02a5d4b2060dad6f5779c196b7bb2b43f871ffb2)
2026-10-27 23:12:41 +03:00
bo0tzzandCodex 1707635743 fix(server): require update access on the motion video when linking a live photo (#32606)
* fix(server): require update access on the motion video when linking a live photo

* chore: allow linking with existing shared link assets

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
(cherry picked from commit a7afb4c9e9d2f099f9204a41f62e17b54fca3b17)
2026-10-27 23:12:41 +03:00
renovate[bot]andCodex 7a6123f8e6 fix(deps): update dependency @immich/ui to ^0.91.0 (#32587)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
(cherry picked from commit d073539afdd20530baaf85ba414f396b94112b5c)
2026-10-27 23:12:40 +03:00
renovate[bot]andCodex 4960eddaf3 fix(deps): update typescript-projects (#32755)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
(cherry picked from commit 8aa9054366fac2738799a2356f9b760a2a441709)
2026-10-27 23:12:40 +03:00
bo0tzzandCodex d35a6613e6 chore: tighten auto-close template check (#32719)
(cherry picked from commit 2400de96668d3510a1a50dd1c658d483e9ad01aa)
2026-10-27 23:12:21 +03:00
Mees FrenselandCodex f96816b55f refactor(web): bulk (un)archive actions (#32189)
(cherry picked from commit d26c0eec10f1bb49117b71b667c88a8a715b2489)
2026-10-27 23:12:21 +03:00
YarosandCodex eca952720a fix: permission for admin notification endpoints (#32567)
(cherry picked from commit 045d295d2d0b506af6f2abe14d1ed2cb89c5235f)
2026-10-27 23:12:13 +03:00
Kairui LiuandCodex c482c1e53e fix: preserve stack primary asset selection (#31380)
* fix: preserve stack primary asset selection

* test: select stacked assets by id

* chore: limit dart formatting changes

* fix: address stack viewer review feedback

* fix: align stack ordering with server

* fix: keep stack primary asset first

---------

Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
(cherry picked from commit 5a5d7be11084969c3c00a294ce6c0527042f88e7)
2026-10-27 23:12:13 +03:00
cevdetandCodex 75a1d7c5c8 perf(web): reduce favicon assets (#32594)
(cherry picked from commit 5177913e0a1dba27d5d46ae4ba46222e94e17d0b)
2026-10-27 23:12:13 +03:00
Mees FrenselandCodex 5630e541e3 refactor(web): clean up (#32627)
(cherry picked from commit 5ff8436781681755042fe6416024d0c4885b851d)
2026-10-27 23:12:13 +03:00
Mees FrenselandCodex 27ccbee82d refactor(web): use svelte builtin id generator (#32631)
(cherry picked from commit 285552785f1bd7cb5ad431c8b3423d1d5e858188)
2026-10-27 23:12:13 +03:00
Antonio SarroandCodex 3458767d66 fix(web): downloads aborted when switching assets in the viewer (#32663)
* fix(web): downloads aborted when switching assets in the viewer

The service worker also handled the download request, so the viewer cancelling its image urls killed the download too. Only handle image requests there.

* chore: clean up

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
(cherry picked from commit 8c3fdda7ead1a70158780e45a67a11413b208dba)
2026-10-27 23:12:13 +03:00
Mees FrenselandCodex cd28300f94 fix(web): show metadata field if it is missing for one duplicate asset (#32601)
(cherry picked from commit b203227fdddff02c4686ff8db9d6054818900929)
2026-10-27 23:12:13 +03:00
Maximilian RinkandCodex d8030f3d14 fix(server): index edited assets in smart search (#32300)
(cherry picked from commit adca900fe277bd6dff93b96b6a7841a8632ce62b)
2026-10-27 23:12:13 +03:00
renovate[bot]andCodex 0ff6470791 chore(deps): update dependency opentofu to v1.13.2 (#32751)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 7cae55ec4ee7ab53d610cdf82b04a817e4ad8e8e)
2026-10-27 23:12:13 +03:00
renovate[bot]andCodex 717152b191 chore(deps): update terraform cloudflare to v4.52.10 (#32754)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 054e6cdce4b0f94c15fa3f4c0d0a076742f4a4d0)
2026-10-27 23:11:57 +03:00
renovate[bot]andCodex 06dbcf7c84 chore(deps): update prom/prometheus docker digest to fa33022 (#32749)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 1a1f15639011971d1674b124d6f0bbdaaf053822)
2026-10-27 23:11:57 +03:00
renovate[bot]andCodex 9767ea9c02 chore(deps): update node.js to 43b9ba1 (#32748)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 0632acf701f3d9f559199bee89b5d3cd86b84822)
2026-10-27 23:11:45 +03:00
JakksonnandCodex 071b8db397 docs: add Polish README translation (#32745)
* docs: added Polish README translation

* docs: improved translations

(cherry picked from commit 5ce14cff4db45fc7ed544f6ec52eef4bc35ef6e2)
2026-10-27 23:11:45 +03:00
renovate[bot]andCodex 1c9813e130 chore(deps): update github-actions (#32758)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
(cherry picked from commit 8875673b6856eb1b793137cd7b0e2be76acc6f0b)
2026-10-27 23:11:45 +03:00
Santo ShakilandCodex 6569aad605 feat(server): accept the iOS app origin for mobile passkeys (#32733)
(cherry picked from commit 1119e9c7a7c9f5ad657aa4e893b897772f75a124)
2026-10-27 23:11:30 +03:00
renovate[bot]andCodex a2794f0986 chore(deps): update ghcr.io/jdx/mise docker tag to v2026.10.20 (#32752)
(cherry picked from commit 506c83e8265bba829505deb23ad05a62eca579fc)
2026-10-27 23:11:30 +03:00
renovate[bot]andCodex 6ffc365b75 fix(deps): update dependency chokidar to v6 (#32760)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
(cherry picked from commit 4c33587fe88c52e50cb63dc8cea347fac6ef01ac)
2026-10-27 23:11:30 +03:00
renovate[bot]andCodex dc19d3a259 fix(deps): update opentelemetry-js monorepo to v3 (#32762)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
(cherry picked from commit 14977d6cc8c27e226334681ba878aa1243887055)
2026-10-27 23:11:29 +03:00
renovate[bot]andCodex f2a502448d chore(deps): update dependency @sveltejs/enhanced-img to v1 (#32395)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
(cherry picked from commit df50249c03ddac9417a2172cfbbaef848163ed4f)
2026-10-27 23:11:28 +03:00
Jason RasmussenandCodex a3ffe3b3c3 refactor: asset face queries (#32679)
(cherry picked from commit 5077a85d5e6775c48b38de7fcf25f2a3864b6a83)
2026-10-27 23:11:20 +03:00
Daniel DietzlerandCodex 8125af4859 chore: migrate web imports from .js to .ts (#32742)
(cherry picked from commit f97c2243fbe5f64a4e1bf96128b82a37cc481506)
2026-10-27 23:11:20 +03:00
Daniel DietzlerandCodex e5b4dd1041 feat: sveltekit 3 (#32737)
(cherry picked from commit 2576ee28cfb790e99ddf401a129e1d9a18713121)
2026-10-27 23:11:06 +03:00
938 changed files with 8324 additions and 6083 deletions
+9 -2
View File
@@ -26,10 +26,17 @@ jobs:
env: env:
BODY: ${{ github.event.pull_request.body }} BODY: ${{ github.event.pull_request.body }}
run: | run: |
BODY=$(printf '%s\n' "$BODY" | tr -d '\r' | perl -0pe 's/<!--.*?-->//gs')
section() { printf '%s\n' "$BODY" | awk -v h="$1" 'index($0, h) == 1 { f = 1; next } /^## / { f = 0 } f'; }
OK=true OK=true
while IFS= read -r header; do while IFS= read -r header; do
printf '%s\n' "$BODY" | grep -qF "$header" || OK=false printf '%s\n' "$BODY" | grep -qF "$header" || OK=false
done < <(sed '/<!--/,/-->/d' .github/pull_request_template.md | grep "^## ") done < <(perl -0pe 's/<!--.*?-->//gs' .github/pull_request_template.md | grep "^## ")
section "## Checklist" | grep -qE '^\s*- \[[xX]\]' || OK=false
[[ "$(section "## Please describe to which degree" | tr -d '[:space:]')" =~ ^(\.\.\.)?$ ]] && OK=false
echo "uses_template=$OK" | tee --append "$GITHUB_OUTPUT" echo "uses_template=$OK" | tee --append "$GITHUB_OUTPUT"
close_template: close_template:
@@ -51,7 +58,7 @@ jobs:
run: | run: |
gh api graphql \ gh api graphql \
-f prId="$NODE_ID" \ -f prId="$NODE_ID" \
-f body="This PR has been automatically closed as the description doesn't follow [our template](https://github.com/immich-app/immich/blob/main/.github/pull_request_template.md). After you edit it to match the template, the PR will automatically be reopened." \ -f body="This PR has been automatically closed as the description doesn't follow [our template](https://github.com/immich-app/immich/blob/main/.github/pull_request_template.md). Please make sure every section is present and filled in. After you edit it to match the template, the PR will automatically be reopened." \
-f query=' -f query='
mutation CommentAndClosePR($prId: ID!, $body: String!) { mutation CommentAndClosePR($prId: ID!, $body: String!) {
addComment(input: { addComment(input: {
+1 -1
View File
@@ -230,7 +230,7 @@ jobs:
working_directory: ./mobile working_directory: ./mobile
- name: Setup Ruby - name: Setup Ruby
uses: ruby/setup-ruby@651ff0d62c5c79bed6d53ec518387aae20e72c1a # v1.329.0 uses: ruby/setup-ruby@e1bd6146574da5e0795075b5319b300edb30c5b0 # v1.333.0
with: with:
ruby-version: '3.4' ruby-version: '3.4'
bundler-cache: true # zizmor: ignore[cache-poisoning] release branches cannot read caches written by pull requests bundler-cache: true # zizmor: ignore[cache-poisoning] release branches cannot read caches written by pull requests
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Check for breaking API changes - name: Check for breaking API changes
uses: oasdiff/oasdiff-action/breaking@5e81b5c380accc6b523f9d32a637ca630e33620b # v0.1.17 uses: oasdiff/oasdiff-action/breaking@9ce3a29eb0e6478dd0140932a678c7fb59d08f22 # v0.1.20
with: with:
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
revision: open-api/immich-openapi-specs.json revision: open-api/immich-openapi-specs.json
+5 -5
View File
@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@@ -71,10 +71,10 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 uses: github/codeql-action/autobuild@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
# ℹ️ Command-line programs to run using the OS shell. # в„№пёЏ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # рџ“љ See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines. # If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
@@ -84,6 +84,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh # ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
with: with:
category: '/language:${{matrix.language}}' category: '/language:${{matrix.language}}'
+1
View File
@@ -35,6 +35,7 @@ jobs:
filters: | filters: |
docs: docs:
- 'docs/**' - 'docs/**'
- 'deployment/**'
open-api: open-api:
- 'open-api/immich-openapi-specs.json' - 'open-api/immich-openapi-specs.json'
force-filters: | force-filters: |
+1
View File
@@ -33,6 +33,7 @@
<a href="readme_i18n/README_uk_UA.md">Українська</a> <a href="readme_i18n/README_uk_UA.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_bg_BG.md">Български</a> <a href="readme_i18n/README_bg_BG.md">Български</a>
<a href="readme_i18n/README_pl_PL.md">Polski</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_sv_SE.md">Svenska</a>
<a href="readme_i18n/README_ar_JO.md">العربية</a> <a href="readme_i18n/README_ar_JO.md">العربية</a>
+42 -42
View File
@@ -1,69 +1,69 @@
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html # @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
[[tools.opentofu]] [[tools.opentofu]]
version = "1.13.1" version = "1.13.2"
backend = "aqua:opentofu/opentofu" backend = "aqua:opentofu/opentofu"
[tools.opentofu."platforms.linux-arm64"] [tools.opentofu."platforms.linux-arm64"]
checksum = "sha256:9c1ef375aa1852db0b2888aa921b640c71f8140d4682aa4fec99378a64fa7dc3" checksum = "sha256:88e2d9e3f94f97b76ab2faf55ade87578f56aed19c5e45fa139d70e994366d1f"
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_linux_arm64.tar.gz" checksum_source = "mise-versions"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708055" url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_linux_arm64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742799"
[tools.opentofu."platforms.linux-x64"] [tools.opentofu."platforms.linux-x64"]
checksum = "sha256:378ada19d4bc70c43732004e8159be771b23b9a5afdf059e5f8a2b3fa2c70a69" checksum = "sha256:ac05d9952662c7db28dd57e93f0a68018837bfdf9342384ec0987977c16a9645"
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_linux_amd64.tar.gz" checksum_source = "mise-versions"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708017" url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_linux_amd64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742636"
[tools.opentofu."platforms.macos-arm64"] [tools.opentofu."platforms.macos-arm64"]
checksum = "sha256:be78f659f04ef06a9dbd9b3934d46af95d787a3aa38396d459dea395261816a9" checksum = "sha256:0d1cdc85f38992055b48e7fb161304a2ddfe2e5ee0ff68ffec9d0349c8bd1b59"
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_darwin_arm64.tar.gz" checksum_source = "mise-versions"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708059" url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_darwin_arm64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742637"
[tools.opentofu."platforms.macos-x64"] [tools.opentofu."platforms.macos-x64"]
checksum = "sha256:a73720443ba38712d7d96dc1e857add02c15a790919c653ad07492e9952f8c27" checksum = "sha256:dc6e4eb1d3ed6550c2ef052596f933dbcd8c806957665ea83ae7537147cb3074"
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_darwin_amd64.tar.gz" checksum_source = "mise-versions"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708142" url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_darwin_amd64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742982"
[tools.opentofu."platforms.windows-x64"] [tools.opentofu."platforms.windows-x64"]
checksum = "sha256:6825ae311e9a15747ce2ede7b7e97c8bb744a4d271fa8113259eeeee3d09438c" checksum = "sha256:e74fc2bda0c81ce696b6b7016a4d5c1f557638161239e4101e9ee7d8e0faba59"
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_windows_amd64.tar.gz" checksum_source = "mise-versions"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708102" url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_windows_amd64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742635"
[[tools.terragrunt]] [[tools.terragrunt]]
version = "1.0.3" version = "1.2.0"
backend = "aqua:gruntwork-io/terragrunt" backend = "aqua:gruntwork-io/terragrunt"
[tools.terragrunt."platforms.linux-arm64"] [tools.terragrunt."platforms.linux-arm64"]
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe" checksum = "sha256:1e13b7f0646fa7962bc6c5bfba14a30d6a589325b82561e21b66be5981d86395"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz" checksum_source = "mise-versions"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654253" url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_linux_arm64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662437526"
[tools.terragrunt."platforms.linux-arm64-musl"]
checksum = "sha256:e5b60ab05b5214db694e6bc215d8124fb626e277cdb56b86f6147ae110d510fe"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_arm64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654253"
[tools.terragrunt."platforms.linux-x64"] [tools.terragrunt."platforms.linux-x64"]
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073" checksum = "sha256:23b5f2f9203d34919f7fd270a4b635b721b4903beca9762a1e4853b159bc9be8"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz" checksum_source = "mise-versions"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654196" url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_linux_amd64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662437263"
[tools.terragrunt."platforms.linux-x64-musl"]
checksum = "sha256:6d48049baf82e0bf9c804368dc85cbfeadc10955e33777e9e8de3e020b94b073"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_linux_amd64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654196"
[tools.terragrunt."platforms.macos-arm64"] [tools.terragrunt."platforms.macos-arm64"]
checksum = "sha256:aacb5be2ca5475300cbce246dfbd8a45eb47510fbaa70fab8561c49ef5db03aa" checksum = "sha256:79790d841ce71fe94c689fb6f5034afd31ad53d5904937c7a10934cca9d9734b"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_arm64.tar.gz" checksum_source = "mise-versions"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654084" url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_darwin_arm64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662436516"
[tools.terragrunt."platforms.macos-x64"] [tools.terragrunt."platforms.macos-x64"]
checksum = "sha256:3133c2251e191aede8e3dd2a5b3aee2e91c5f08f88f117aee40eed9a24c8ef6b" checksum = "sha256:0b0e63e80509eb1ffb4f89b892bc6fbf7728f27f0a73974400f1bd82a80b8627"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_darwin_amd64.tar.gz" checksum_source = "mise-versions"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406653970" url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_darwin_amd64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662436142"
[tools.terragrunt."platforms.windows-x64"] [tools.terragrunt."platforms.windows-x64"]
checksum = "sha256:183b2745b4e04980a6bfa4450ff81956a12596ca22d70f7aaa793980f5b036db" checksum = "sha256:c9a146b9aad456a289614023ad5778123d819af3cf9efd1c6130f196cfcb1999"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.0.3/terragrunt_windows_amd64.exe.tar.gz" checksum_source = "mise-versions"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/406654412" url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_windows_amd64.exe.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662438255"
+2 -2
View File
@@ -1,6 +1,6 @@
[tools] [tools]
terragrunt = "1.1.1" terragrunt = "1.2.0"
opentofu = "1.13.1" opentofu = "1.13.2"
[tasks."tg:fmt"] [tasks."tg:fmt"]
run = "terragrunt hclfmt" run = "terragrunt hclfmt"
+18 -28
View File
@@ -2,34 +2,24 @@
# 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.52.9" version = "5.28.1"
constraints = "4.52.9" constraints = "5.28.1"
hashes = [ hashes = [
"h1:LRBYMKjZyuld8kgVhzoceRtdtkht+w8LunrdpgqrL28=", "h1:9YL6g+iEfg3OzX6qSZOYsictVO0fNP09D6jRwIZKwQ0=",
"h1:Q2NeLtrjSTZwHHsMZXKpqOAbxF8DWgqpmH5j+1pUZGw=", "h1:GjOKfCmvcyE06fUp5WrAzfCLkTQaCg2Ko/kFlq8OfpM=",
"h1:Q4wiux11bDVWRmJScdjp8jLbxMRsj8k3xGtPtY4BPDo=", "h1:QTIohIPsVCqEmknrR2PdbVXyK1fjS805rMaZpEEpzFQ=",
"h1:VVW111jEbRMTu7hXeGQMmZ/oSTB8LIYhjM5ztyymqAI=", "h1:SFb952CVUFUoFjfor+GUqAV6DQ6bnUfaFcWL2yJlFBo=",
"h1:Z7sTk8/B9XYFAEpTq1ib/WsB2QvpegGwef395tHCUSo=", "h1:aso3s6IuIQju5L/OLrywCfQl7aA5TF+sb5pZzo8JG20=",
"h1:eakzS6UsflV4gN/oQWdRPYvvmySKkhgKPyykCfPSVH0=", "h1:cxGa9LRKCzwNacgnLjXnHUygkvAEd6pb8g8sRwzPSRE=",
"h1:eesNMA1JUNBKCnuEvRLW2CT3tNrIejbXJG4A176nA9Q=", "h1:kBW0qcDqNupOnsi7vvx/0TQRxXLtmDKAHwOwH5l03pc=",
"h1:o8txgjqnDDODxQQ6ITwHsvPqHllCbMGnJkjCWNJK5ME=", "h1:t6x20B8TpQAG0TsUlyWLrAWNO+V8JE8K3Ij0hW0feSU=",
"h1:rDTPIzab3tbGdtU3NmES08+tRlKG8jygNAFDFbheiuM=", "zh:219d464dd727c0e5c90a97b44aa5d7988acc4eec47f3684f7ecd006b17c46d9c",
"h1:sIoAt5vaxgFBrst1Esfm7ixQQ/T7/JNPZu0m4bw+Bc4=", "zh:2511f2380209c81121e6eb943d7e68d03602a4fcf4f0311dfc67e198179b9303",
"h1:sPCs3KnD/db5TMz2CNgiYix7sRS8+FP92T4ipSYT9vw=", "zh:2ae200482bf592cd703981c4d4fe87a52c0dfae81a70261295f59b1bb0d01a6c",
"h1:uXPLsMQUYVJjRGUfmfDQXsKzYr1scvF6PYKj+3f0Nr4=", "zh:7df6fe6573379c2faf2e801fdbca58ac909266dbee604feaa993439b8616c9b3",
"h1:vQibC9lgBRQjRQ5KxFH61wd3/q2I40groLPjZ/jiikQ=", "zh:9fb5a295588351ce565515dc2a770047e8bab786a4a9665511ab9a9ca2db5f1b",
"zh:0826cad9b57368f06466ec3e1f766086f44a373288753e144f681ddc0e61ab49", "zh:a182f4aec86c74e9db4affa22323c5a21f5d92c1fcf91b934570753d04f4daf8",
"zh:15bf8d8abdcd202652466f3ef91110f143a7fad13c6173c7180e3dc0dbe9254b", "zh:a39bb8624fb6c714e38bc0e632aae117a9fede5a18a248ab15dd1b60013c0324",
"zh:1a60d1d2e55a17942902b1ccc91e8f6cb150c035843d3eefc074f1291a0e7d5f", "zh:bef2ffbdb68569549fe432b550592e07456759a7288f84f7c37c9d933a493f54",
"zh:23373a63fe06bbfa90c6c105c08429031719bedafa2458973cbdad32a384b5a7",
"zh:256a97b79cedee2575002f77ac3bf18888ce0fc97170ae8a357db406bb0a88e3",
"zh:3dd9f93c167cd59491ae77781032491d36a80843a1e44f9ee68b41b3f8e3ab8e",
"zh:5182d2d61930dd83584d8feb56102614f80ddeec532b939b58085ad74b796a6e",
"zh:5e3ca1888ba3b3bd8abd0f7dfcdd82fb5a8819affee899ed7fd891a33c17e0cb",
"zh:7a3ce2119e39fb3a90a6a459a26863851309dbc92e0735336cbc98692539cb4d",
"zh:c095dc680a646cc078c10bb0551e2ea8f08e7abdbcbc58cfd332c56b58788631",
"zh:d2400e010e069c19916c99136e200cf873eba6db2b18d3eb9541b38eb1f565fa",
"zh:dbc2f19f09aaefbd69334005bca2bcb0c9b690de1ed76f7ba97ce8e59a6e85df",
"zh:e685225f432da3e23b8717848de8c0ad0e22363450b2ac92cd24a56739ab5e9b",
] ]
} }
@@ -5,7 +5,7 @@ terraform {
required_providers { required_providers {
cloudflare = { cloudflare = {
source = "cloudflare/cloudflare" source = "cloudflare/cloudflare"
version = "4.52.9" version = "5.28.1"
} }
} }
} }
+18 -28
View File
@@ -2,34 +2,24 @@
# 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.52.9" version = "5.28.1"
constraints = "4.52.9" constraints = "5.28.1"
hashes = [ hashes = [
"h1:LRBYMKjZyuld8kgVhzoceRtdtkht+w8LunrdpgqrL28=", "h1:9YL6g+iEfg3OzX6qSZOYsictVO0fNP09D6jRwIZKwQ0=",
"h1:Q2NeLtrjSTZwHHsMZXKpqOAbxF8DWgqpmH5j+1pUZGw=", "h1:GjOKfCmvcyE06fUp5WrAzfCLkTQaCg2Ko/kFlq8OfpM=",
"h1:Q4wiux11bDVWRmJScdjp8jLbxMRsj8k3xGtPtY4BPDo=", "h1:QTIohIPsVCqEmknrR2PdbVXyK1fjS805rMaZpEEpzFQ=",
"h1:VVW111jEbRMTu7hXeGQMmZ/oSTB8LIYhjM5ztyymqAI=", "h1:SFb952CVUFUoFjfor+GUqAV6DQ6bnUfaFcWL2yJlFBo=",
"h1:Z7sTk8/B9XYFAEpTq1ib/WsB2QvpegGwef395tHCUSo=", "h1:aso3s6IuIQju5L/OLrywCfQl7aA5TF+sb5pZzo8JG20=",
"h1:eakzS6UsflV4gN/oQWdRPYvvmySKkhgKPyykCfPSVH0=", "h1:cxGa9LRKCzwNacgnLjXnHUygkvAEd6pb8g8sRwzPSRE=",
"h1:eesNMA1JUNBKCnuEvRLW2CT3tNrIejbXJG4A176nA9Q=", "h1:kBW0qcDqNupOnsi7vvx/0TQRxXLtmDKAHwOwH5l03pc=",
"h1:o8txgjqnDDODxQQ6ITwHsvPqHllCbMGnJkjCWNJK5ME=", "h1:t6x20B8TpQAG0TsUlyWLrAWNO+V8JE8K3Ij0hW0feSU=",
"h1:rDTPIzab3tbGdtU3NmES08+tRlKG8jygNAFDFbheiuM=", "zh:219d464dd727c0e5c90a97b44aa5d7988acc4eec47f3684f7ecd006b17c46d9c",
"h1:sIoAt5vaxgFBrst1Esfm7ixQQ/T7/JNPZu0m4bw+Bc4=", "zh:2511f2380209c81121e6eb943d7e68d03602a4fcf4f0311dfc67e198179b9303",
"h1:sPCs3KnD/db5TMz2CNgiYix7sRS8+FP92T4ipSYT9vw=", "zh:2ae200482bf592cd703981c4d4fe87a52c0dfae81a70261295f59b1bb0d01a6c",
"h1:uXPLsMQUYVJjRGUfmfDQXsKzYr1scvF6PYKj+3f0Nr4=", "zh:7df6fe6573379c2faf2e801fdbca58ac909266dbee604feaa993439b8616c9b3",
"h1:vQibC9lgBRQjRQ5KxFH61wd3/q2I40groLPjZ/jiikQ=", "zh:9fb5a295588351ce565515dc2a770047e8bab786a4a9665511ab9a9ca2db5f1b",
"zh:0826cad9b57368f06466ec3e1f766086f44a373288753e144f681ddc0e61ab49", "zh:a182f4aec86c74e9db4affa22323c5a21f5d92c1fcf91b934570753d04f4daf8",
"zh:15bf8d8abdcd202652466f3ef91110f143a7fad13c6173c7180e3dc0dbe9254b", "zh:a39bb8624fb6c714e38bc0e632aae117a9fede5a18a248ab15dd1b60013c0324",
"zh:1a60d1d2e55a17942902b1ccc91e8f6cb150c035843d3eefc074f1291a0e7d5f", "zh:bef2ffbdb68569549fe432b550592e07456759a7288f84f7c37c9d933a493f54",
"zh:23373a63fe06bbfa90c6c105c08429031719bedafa2458973cbdad32a384b5a7",
"zh:256a97b79cedee2575002f77ac3bf18888ce0fc97170ae8a357db406bb0a88e3",
"zh:3dd9f93c167cd59491ae77781032491d36a80843a1e44f9ee68b41b3f8e3ab8e",
"zh:5182d2d61930dd83584d8feb56102614f80ddeec532b939b58085ad74b796a6e",
"zh:5e3ca1888ba3b3bd8abd0f7dfcdd82fb5a8819affee899ed7fd891a33c17e0cb",
"zh:7a3ce2119e39fb3a90a6a459a26863851309dbc92e0735336cbc98692539cb4d",
"zh:c095dc680a646cc078c10bb0551e2ea8f08e7abdbcbc58cfd332c56b58788631",
"zh:d2400e010e069c19916c99136e200cf873eba6db2b18d3eb9541b38eb1f565fa",
"zh:dbc2f19f09aaefbd69334005bca2bcb0c9b690de1ed76f7ba97ce8e59a6e85df",
"zh:e685225f432da3e23b8717848de8c0ad0e22363450b2ac92cd24a56739ab5e9b",
] ]
} }
+1 -1
View File
@@ -5,7 +5,7 @@ terraform {
required_providers { required_providers {
cloudflare = { cloudflare = {
source = "cloudflare/cloudflare" source = "cloudflare/cloudflare"
version = "4.52.9" version = "5.28.1"
} }
} }
} }
+1 -1
View File
@@ -85,7 +85,7 @@ services:
container_name: immich_prometheus container_name: immich_prometheus
ports: ports:
- 9090:9090 - 9090:9090
image: prom/prometheus@sha256:5ce7540c3c00ef4ab0c9d2c995c6a5b9c421f44b4a115d97a2c7af3b1c21cbb0 image: prom/prometheus@sha256:fa3302279710be2f21a8301ec17a8b4f2301fd829bd113839b3b6b02b6f191a2
volumes: volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml - ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus - prometheus-data:/prometheus
+1 -1
View File
@@ -34,7 +34,7 @@ The mobile app is written in [Dart](https://dart.dev/) using [Flutter](https://f
<MobileArchitecture className="p-4 dark:bg-immich-dark-primary my-4" /> <MobileArchitecture className="p-4 dark:bg-immich-dark-primary my-4" />
The diagrams shows the target architecture, the current state of the code-base is not always following the architecture yet. New code and contributions should follow this architecture. The diagrams shows the target architecture, the current state of the code-base is not always following the architecture yet. New code and contributions should follow this architecture.
Currently, it uses [Isar Database](https://isar.dev/) for a local database and [Riverpod](https://riverpod.dev/) for state management (providers). Currently, it uses [Drift](https://drift.simonbinder.eu/) (SQLite) for a local database and [Riverpod](https://riverpod.dev/) for state management (providers).
Entities and Models are the two types of data classes used. While entities are stored in the on-device database, models are ephemeral and only kept in memory. Entities and Models are the two types of data classes used. While entities are stored in the on-device database, models are ephemeral and only kept in memory.
The Repositories should be the only place where other data classes are used internally (such as OpenAPI DTOs). However, their interfaces must not use foreign data classes! The Repositories should be the only place where other data classes are used internally (such as OpenAPI DTOs). However, their interfaces must not use foreign data classes!
@@ -85,7 +85,9 @@ You do not need to redo any machine learning jobs after enabling hardware accele
- RKNPU driver V0.9.8 or later must be available in the host server - RKNPU driver V0.9.8 or later must be available in the host server
- You may confirm this by running `cat /sys/kernel/debug/rknpu/version` to check the version - You may confirm this by running `cat /sys/kernel/debug/rknpu/version` to check the version
- Optional: Configure your `.env` file, see [environment variables](/install/environment-variables) for RKNN specific settings - Optional: Configure your `.env` file, see [environment variables](/install/environment-variables) for RKNN specific settings
- In particular, setting `MACHINE_LEARNING_RKNN_THREADS` to 2 or 3 can _dramatically_ improve performance for RK3576 and RK3588 compared to the default of 1, at the expense of multiplying the amount of RAM each model uses by that amount. - By default each model gets one context per NPU core (1 on RK3566/RK3568, 2 on RK3576, 3 on RK3588), so concurrent requests run on all cores at once. The contexts of a model share its weights, but each one needs its own working memory. Extra contexts therefore cost little for models that are mostly weights, such as CLIP, but for OCR detection each one can add close to the model's own memory again, more so at higher OCR resolutions. Set `MACHINE_LEARNING_RKNN_THREADS=1` to trade throughput for memory.
- Keep `MACHINE_LEARNING_WORKERS=1` unless you have a specific reason not to: each worker process still loads its own copy of the weights.
- Raise Immich job concurrency (Smart Search, Face Detection, OCR) a bit above the number of NPU cores, e.g. `4`–`6`, so a prepared image is always waiting when a core frees up.
## Setup ## Setup
@@ -100,7 +102,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
You can confirm the device is being recognized and used by checking its utilization. There are many tools to display this, such as `nvtop` for NVIDIA or Intel, `intel_gpu_top` for Intel, and `radeontop` for AMD. You can confirm the device is being recognized and used by checking its utilization. There are many tools to display this, such as `nvtop` for NVIDIA or Intel, `intel_gpu_top` for Intel, and `radeontop` for AMD.
You can also check the logs of the `immich-machine-learning` container. When a Smart Search or Face Detection job begins, or when you search with text in Immich, you should either see a log for `Available ORT providers` containing the relevant provider (e.g. `CUDAExecutionProvider` in the case of CUDA), or a `Loaded ANN model` log entry without errors in the case of ARM NN. You can also check the logs of the `immich-machine-learning` container. When a Smart Search or Face Detection job begins, or when you search with text in Immich, you should either see a log for `Available ORT providers` containing the relevant provider (e.g. `CUDAExecutionProvider` in the case of CUDA), a `Loaded ANN model` log entry without errors in the case of ARM NN, or a `Loaded RKNN model ... threads sharing its weights` log entry for RKNN.
#### Single Compose File #### Single Compose File
@@ -174,7 +176,7 @@ Note that you should increase job concurrencies to increase overall utilization
- Less heat generation - Less heat generation
- Very slightly lower accuracy (RKNPU always uses FP16, while ARM NN by default uses higher precision FP32 unless `MACHINE_LEARNING_ANN_FP16_TURBO` is enabled) - Very slightly lower accuracy (RKNPU always uses FP16, while ARM NN by default uses higher precision FP32 unless `MACHINE_LEARNING_ANN_FP16_TURBO` is enabled)
- Varying speed (tested on RK3588): - Varying speed (tested on RK3588):
- If `MACHINE_LEARNING_RKNN_THREADS` is at the default of 1, RKNPU will have substantially lower throughput for ML jobs than ARM NN in most cases, but similar latency (such as when searching) - With a single context (`MACHINE_LEARNING_RKNN_THREADS=1`), RKNPU will have substantially lower throughput for ML jobs than ARM NN in most cases, but similar latency (such as when searching)
- If `MACHINE_LEARNING_RKNN_THREADS` is set to 3, it will be somewhat faster than ARM NN at FP32, but somewhat slower than ARM NN if `MACHINE_LEARNING_ANN_FP16_TURBO` is enabled - With three contexts (the default on RK3588), it will be somewhat faster than ARM NN at FP32, but somewhat slower than ARM NN if `MACHINE_LEARNING_ANN_FP16_TURBO` is enabled
- When other tasks also use the GPU (like transcoding), RKNPU has a significant advantage over ARM NN as it uses the otherwise idle NPU instead of competing for GPU usage - When other tasks also use the GPU (like transcoding), RKNPU has a significant advantage over ARM NN as it uses the otherwise idle NPU instead of competing for GPU usage
- Lower RAM usage if `MACHINE_LEARNING_RKNN_THREADS` is at the default of 1, but significantly higher if greater than 1 (which is necessary for it to fully utilize the NPU and hence be comparable in speed to ARM NN) - Lower RAM usage than earlier RKNN builds: the contexts of a model share its weights instead of each loading their own copy. Keep `MACHINE_LEARNING_WORKERS=1` and raise Immich ML job concurrency a bit above the core count so every NPU core stays busy.
+1 -1
View File
@@ -179,7 +179,7 @@ Redis (Sentinel) URL example JSON before encoding:
| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | `4` | machine learning | | `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | `4` | machine learning |
| `MACHINE_LEARNING_MAX_BATCH_SIZE__OCR` | Set the maximum number of boxes that will be processed at once by the OCR model | `6` | machine learning | | `MACHINE_LEARNING_MAX_BATCH_SIZE__OCR` | Set the maximum number of boxes that will be processed at once by the OCR model | `6` | machine learning |
| `MACHINE_LEARNING_RKNN` | Enable RKNN hardware acceleration if supported | `True` | machine learning | | `MACHINE_LEARNING_RKNN` | Enable RKNN hardware acceleration if supported | `True` | machine learning |
| `MACHINE_LEARNING_RKNN_THREADS` | How many threads of RKNN runtime should be spun up while inferencing. | `1` | machine learning | | `MACHINE_LEARNING_RKNN_THREADS` | Number of RKNN contexts per model, which share its weights | number of NPU cores | machine learning |
| `MACHINE_LEARNING_MODEL_ARENA` | Pre-allocates CPU memory to avoid memory fragmentation | true | machine learning | | `MACHINE_LEARNING_MODEL_ARENA` | Pre-allocates CPU memory to avoid memory fragmentation | true | machine learning |
| `MACHINE_LEARNING_MODEL_REVISION` | Model revision to download; `main` selects the original exports and `v2` selects the newer exports | `main` | machine learning | | `MACHINE_LEARNING_MODEL_REVISION` | Model revision to download; `main` selects the original exports and `v2` selects the newer exports | `main` | machine learning |
+1 -1
View File
@@ -1,7 +1,7 @@
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html # @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
[[tools.wrangler]] [[tools.wrangler]]
version = "4.151.0" version = "4.152.0"
backend = "npm:wrangler" backend = "npm:wrangler"
[tools.wrangler.options] [tools.wrangler.options]
+1 -1
View File
@@ -28,4 +28,4 @@ run = "prettier --write ."
run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}" run = "wrangler pages deploy build --project-name=${PROJECT_NAME} --branch=${BRANCH_NAME}"
[tools] [tools]
wrangler = "4.151.0" wrangler = "4.152.0"
+1 -1
View File
@@ -40,7 +40,7 @@
"eslint": "^10.0.0", "eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^75.0.0", "eslint-plugin-unicorn": "^78.0.0",
"exiftool-vendored": "^39.0.0", "exiftool-vendored": "^39.0.0",
"globals": "^17.0.0", "globals": "^17.0.0",
"luxon": "^3.4.4", "luxon": "^3.4.4",
+3 -9
View File
@@ -628,25 +628,19 @@ describe('/albums', () => {
]); ]);
}); });
it('should not be able to remove foreign asset from foreign album', async () => { it('should be able to remove foreign asset from shared album as an editor', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/albums/${user1Albums[0].id}/assets`) .delete(`/albums/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`) .set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [user1Asset1.id] }); .send({ ids: [user1Asset1.id] });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual([ expect(body).toEqual([expect.objectContaining({ id: user1Asset1.id, success: true })]);
expect.objectContaining({
id: user1Asset1.id,
success: false,
error: 'no_permission',
}),
]);
}); });
it('should be able to remove own asset from own album', async () => { it('should be able to remove own asset from own album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/albums/${user1Albums[0].id}/assets`) .delete(`/albums/${user1Albums[2].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset1.id] }); .send({ ids: [user1Asset1.id] });
+1 -1
View File
@@ -355,7 +355,7 @@ describe('/asset', () => {
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ livePhotoVideoId: asset.id }); .send({ livePhotoVideoId: asset.id });
expect(body).toEqual(errorDto.badRequest('Live photo video does not belong to the user')); expect(body).toEqual(errorDto.badRequest('Not found or no asset.update access'));
expect(status).toEqual(400); expect(status).toEqual(400);
}); });
+1 -1
View File
@@ -103,7 +103,7 @@ test.describe('Shared Links', () => {
test('show error for invalid shared link', async ({ page }) => { test('show error for invalid shared link', async ({ page }) => {
await page.goto('/share/invalid'); await page.goto('/share/invalid');
await page.getByRole('heading', { name: 'Invalid share key' }).waitFor(); await page.getByRole('heading', { name: 'Page not found' }).waitFor();
}); });
test('auth on navigation from shared link to timeline', async ({ context, page }) => { test('auth on navigation from shared link to timeline', async ({ context, page }) => {
@@ -14,9 +14,7 @@ type DayDistributionStrategy = (
rng: SeededRandom, rng: SeededRandom,
) => MockTimelineAsset[]; ) => MockTimelineAsset[];
/** /** Strategies for determining total asset count per month */
* Strategies for determining total asset count per month
*/
export const ASSET_DISTRIBUTION: Record<MonthDistribution, AssetDistributionStrategy | null> = { export const ASSET_DISTRIBUTION: Record<MonthDistribution, AssetDistributionStrategy | null> = {
empty: null, // Special case - handled separately empty: null, // Special case - handled separately
sparse: (rng) => rng.nextInt(3, 9), // 3-8 assets sparse: (rng) => rng.nextInt(3, 9), // 3-8 assets
@@ -25,9 +23,7 @@ export const ASSET_DISTRIBUTION: Record<MonthDistribution, AssetDistributionStra
'very-dense': (rng) => rng.nextInt(80, 151), // 80-150 assets 'very-dense': (rng) => rng.nextInt(80, 151), // 80-150 assets
}; };
/** /** Strategies for distributing assets across days within a month */
* Strategies for distributing assets across days within a month
*/
export const DAY_DISTRIBUTION: Record<DayPattern, DayDistributionStrategy> = { export const DAY_DISTRIBUTION: Record<DayPattern, DayDistributionStrategy> = {
'single-day': (year, month, daysInMonth, totalAssets, ownerId, rng) => { 'single-day': (year, month, daysInMonth, totalAssets, ownerId, rng) => {
// All assets on one day in the middle of the month // All assets on one day in the middle of the month
@@ -1,6 +1,4 @@
/** /** Generator functions for timeline model objects */
* Generator functions for timeline model objects
*/
import { faker } from '@faker-js/faker'; import { faker } from '@faker-js/faker';
import { AssetVisibility } from '@immich/sdk'; import { AssetVisibility } from '@immich/sdk';
@@ -12,9 +10,7 @@ import { ASSET_DISTRIBUTION, DAY_DISTRIBUTION } from './distribution-patterns';
import type { MockTimelineAsset, MockTimelineData, SerializedTimelineData, TimelineConfig } from './timeline-config'; import type { MockTimelineAsset, MockTimelineData, SerializedTimelineData, TimelineConfig } from './timeline-config';
import { ASPECT_RATIO_WEIGHTS, GENERATION_CONSTANTS, validateTimelineConfig } from './timeline-config'; import { ASPECT_RATIO_WEIGHTS, GENERATION_CONSTANTS, validateTimelineConfig } from './timeline-config';
/** /** Generate a random aspect ratio based on weighted probabilities */
* Generate a random aspect ratio based on weighted probabilities
*/
export function generateAspectRatio(rng: SeededRandom): string { export function generateAspectRatio(rng: SeededRandom): string {
const random = rng.next(); const random = rng.next();
let cumulative = 0; let cumulative = 0;
@@ -89,9 +85,7 @@ export function generateAsset(
return asset; return asset;
} }
/** /** Generate assets for a specific day */
* Generate assets for a specific day
*/
export function generateDayAssets( export function generateDayAssets(
year: number, year: number,
month: number, month: number,
@@ -104,10 +98,10 @@ export function generateDayAssets(
} }
/** /**
* Distribute assets evenly across consecutive days Distribute assets evenly across consecutive days
*
* @returns Array of generated timeline assets @returns Array of generated timeline assets
*/ */
export function generateConsecutiveDays( export function generateConsecutiveDays(
year: number, year: number,
month: number, month: number,
@@ -133,9 +127,7 @@ export function generateConsecutiveDays(
return assets; return assets;
} }
/** /** Generate assets for a month with specified distribution pattern */
* Generate assets for a month with specified distribution pattern
*/
export function generateMonthAssets( export function generateMonthAssets(
year: number, year: number,
month: number, month: number,
@@ -173,9 +165,7 @@ export function generateMonthAssets(
return assets; return assets;
} }
/** /** Main generator function for timeline data */
* Main generator function for timeline data
*/
export function generateTimelineData(config: TimelineConfig): MockTimelineData { export function generateTimelineData(config: TimelineConfig): MockTimelineData {
validateTimelineConfig(config); validateTimelineConfig(config);
+30 -34
View File
@@ -1,6 +1,4 @@
/** /** REST API output functions for converting timeline data to API response formats */
* REST API output functions for converting timeline data to API response formats
*/
import { import {
AlbumUserRole, AlbumUserRole,
@@ -19,9 +17,7 @@ import { signupDto } from 'src/fixtures.js';
import { parseTimeBucketKey } from 'src/ui/generators/timeline/utils.js'; import { parseTimeBucketKey } from 'src/ui/generators/timeline/utils.js';
import type { MockTimelineAsset, MockTimelineData } from './timeline-config'; import type { MockTimelineAsset, MockTimelineData } from './timeline-config';
/** /** Convert timeline/asset models to columnar format (parallel arrays) */
* Convert timeline/asset models to columnar format (parallel arrays)
*/
export function toColumnarFormat(assets: MockTimelineAsset[]): TimeBucketAssetResponseDto { export function toColumnarFormat(assets: MockTimelineAsset[]): TimeBucketAssetResponseDto {
const result: TimeBucketAssetResponseDto = { const result: TimeBucketAssetResponseDto = {
id: [], id: [],
@@ -70,11 +66,11 @@ export function toColumnarFormat(assets: MockTimelineAsset[]): TimeBucketAssetRe
} }
/** /**
* Extract a single bucket from timeline data (mimics getTimeBucket API) Extract a single bucket from timeline data (mimics getTimeBucket API)
* Automatically handles both ISO timestamp and simple month formats Automatically handles both ISO timestamp and simple month formats
* Returns data in columnar format matching the actual API Returns data in columnar format matching the actual API
* When albumId is provided, only returns assets from that album When albumId is provided, only returns assets from that album
*/ */
export function getTimeBucket( export function getTimeBucket(
timelineData: MockTimelineData, timelineData: MockTimelineData,
timeBucket: string, timeBucket: string,
@@ -141,16 +137,16 @@ export type Changes = {
}; };
/** /**
* Helper function to determine if an asset should be included based on filter criteria Helper function to determine if an asset should be included based on filter criteria
* @param asset - The asset to check @param asset - The asset to check
* @param isTrashed - Filter for trashed status (undefined means no filter) @param isTrashed - Filter for trashed status (undefined means no filter)
* @param isArchived - Filter for archived status (undefined means no filter) @param isArchived - Filter for archived status (undefined means no filter)
* @param isFavorite - Filter for favorite status (undefined means no filter) @param isFavorite - Filter for favorite status (undefined means no filter)
* @param deletedAssetIds - Set of IDs for assets that have been deleted @param deletedAssetIds - Set of IDs for assets that have been deleted
* @param archivedAssetIds - Set of IDs for assets that have been archived @param archivedAssetIds - Set of IDs for assets that have been archived
* @param favoritedAssetIds - Set of IDs for assets that have been favorited @param favoritedAssetIds - Set of IDs for assets that have been favorited
* @returns true if the asset matches all filter criteria @returns true if the asset matches all filter criteria
*/ */
function shouldIncludeAsset( function shouldIncludeAsset(
asset: MockTimelineAsset, asset: MockTimelineAsset,
isTrashed: boolean | undefined, isTrashed: boolean | undefined,
@@ -175,9 +171,9 @@ function shouldIncludeAsset(
return isFavorite === undefined || actuallyFavorited === isFavorite; return isFavorite === undefined || actuallyFavorited === isFavorite;
} }
/** /**
* Get summary for all buckets (mimics getTimeBuckets API) Get summary for all buckets (mimics getTimeBuckets API)
* When albumId is provided, only includes buckets that contain assets from that album When albumId is provided, only includes buckets that contain assets from that album
*/ */
export function getTimeBuckets( export function getTimeBuckets(
timelineData: MockTimelineData, timelineData: MockTimelineData,
isTrashed: boolean | undefined, isTrashed: boolean | undefined,
@@ -279,9 +275,9 @@ const createDefaultOwner = (ownerId: string) => {
}; };
/** /**
* Convert a TimelineAssetConfig to a full AssetResponseDto Convert a TimelineAssetConfig to a full AssetResponseDto
* This matches the response from GET /api/assets/:id This matches the response from GET /api/assets/:id
*/ */
export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserResponseDto): AssetResponseDto { export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserResponseDto): AssetResponseDto {
const now = new Date().toISOString(); const now = new Date().toISOString();
@@ -292,7 +288,7 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons
make: null, make: null,
model: null, model: null,
exifImageWidth: asset.ratio > 1 ? 4000 : 3000, exifImageWidth: asset.ratio > 1 ? 4000 : 3000,
exifImageHeight: asset.ratio > 1 ? Math.round(4000 / asset.ratio) : Math.round(3000 * asset.ratio), exifImageHeight: Math.round(asset.ratio > 1 ? 4000 : 3000 / asset.ratio),
fileSizeInByte: asset.fileSizeInByte, fileSizeInByte: asset.fileSizeInByte,
orientation: '1', orientation: '1',
dateTimeOriginal: asset.fileCreatedAt, dateTimeOriginal: asset.fileCreatedAt,
@@ -348,9 +344,9 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons
} }
/** /**
* Get a single asset by ID from timeline data Get a single asset by ID from timeline data
* This matches the response from GET /api/assets/:id This matches the response from GET /api/assets/:id
*/ */
export function getAsset( export function getAsset(
timelineData: MockTimelineData, timelineData: MockTimelineData,
assetId: string, assetId: string,
@@ -368,9 +364,9 @@ export function getAsset(
} }
/** /**
* Get a mock album from timeline data Get a mock album from timeline data
* This matches the response from GET /api/albums/:id This matches the response from GET /api/albums/:id
*/ */
export function getAlbum( export function getAlbum(
timelineData: MockTimelineData, timelineData: MockTimelineData,
ownerId: string, ownerId: string,
@@ -57,9 +57,7 @@ export type MonthSpec = {
pattern: DayPattern; pattern: DayPattern;
}; };
/** /** Configuration for timeline data generation */
* Configuration for timeline data generation
*/
export type TimelineConfig = { export type TimelineConfig = {
ownerId?: string; ownerId?: string;
months: MonthSpec[]; months: MonthSpec[];
@@ -88,9 +86,7 @@ export type SerializedTimelineData = {
album: MockAlbum; album: MockAlbum;
}; };
/** /** Validates a TimelineConfig object to ensure all values are within expected ranges */
* Validates a TimelineConfig object to ensure all values are within expected ranges
*/
export function validateTimelineConfig(config: TimelineConfig): void { export function validateTimelineConfig(config: TimelineConfig): void {
if (!config.months || config.months.length === 0) { if (!config.months || config.months.length === 0) {
throw new Error('TimelineConfig must contain at least one month'); throw new Error('TimelineConfig must contain at least one month');
@@ -146,9 +142,7 @@ export function validateTimelineConfig(config: TimelineConfig): void {
} }
} }
/** /** Create a default timeline configuration */
* Create a default timeline configuration
*/
export function createDefaultTimelineConfig(): TimelineConfig { export function createDefaultTimelineConfig(): TimelineConfig {
const months: MonthSpec[] = [ const months: MonthSpec[] = [
// 2024 - Mix of patterns // 2024 - Mix of patterns
+19 -30
View File
@@ -1,9 +1,7 @@
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { GENERATION_CONSTANTS, MockTimelineAsset } from 'src/ui/generators/timeline/timeline-config.js'; import { GENERATION_CONSTANTS, MockTimelineAsset } from 'src/ui/generators/timeline/timeline-config.js';
/** /** Linear Congruential Generator for deterministic pseudo-random numbers */
* Linear Congruential Generator for deterministic pseudo-random numbers
*/
export class SeededRandom { export class SeededRandom {
private seed: number; private seed: number;
@@ -11,38 +9,32 @@ export class SeededRandom {
this.seed = seed; this.seed = seed;
} }
/** /** Generate next random number in range [0, 1) */
* Generate next random number in range [0, 1)
*/
next(): number { next(): number {
// LCG parameters from Numerical Recipes // LCG parameters from Numerical Recipes
this.seed = (this.seed * 1_664_525 + 1_013_904_223) % 2_147_483_647; this.seed = (this.seed * 1_664_525 + 1_013_904_223) % 2_147_483_647;
return this.seed / 2_147_483_647; return this.seed / 2_147_483_647;
} }
/** /** Generate random integer in range [min, max) */
* Generate random integer in range [min, max)
*/
nextInt(min: number, max: number): number { nextInt(min: number, max: number): number {
return Math.floor(this.next() * (max - min)) + min; return Math.floor(this.next() * (max - min)) + min;
} }
/** /** Generate random boolean with given probability */
* Generate random boolean with given probability
*/
nextBoolean(probability = 0.5): boolean { nextBoolean(probability = 0.5): boolean {
return this.next() < probability; return this.next() < probability;
} }
} }
/** /**
* Select random days using seed variation to avoid collisions. Select random days using seed variation to avoid collisions.
*
* @param daysInMonth - Total number of days in the month @param daysInMonth - Total number of days in the month
* @param numDays - Number of days to select @param numDays - Number of days to select
* @param rng - Random number generator instance @param rng - Random number generator instance
* @returns Array of selected day numbers, sorted in descending order @returns Array of selected day numbers, sorted in descending order
*/ */
export function selectRandomDays(daysInMonth: number, numDays: number, rng: SeededRandom): number[] { export function selectRandomDays(daysInMonth: number, numDays: number, rng: SeededRandom): number[] {
const selectedDays = new Set<number>(); const selectedDays = new Set<number>();
const maxAttempts = numDays * GENERATION_CONSTANTS.MAX_SELECT_ATTEMPTS; // Safety limit const maxAttempts = numDays * GENERATION_CONSTANTS.MAX_SELECT_ATTEMPTS; // Safety limit
@@ -64,9 +56,7 @@ export function selectRandomDays(daysInMonth: number, numDays: number, rng: Seed
return [...selectedDays].toSorted((a, b) => b - a); return [...selectedDays].toSorted((a, b) => b - a);
} }
/** /** Select item from array using seeded random */
* Select item from array using seeded random
*/
export function selectRandom<T>(arr: T[], rng: SeededRandom): T { export function selectRandom<T>(arr: T[], rng: SeededRandom): T {
if (arr.length === 0) { if (arr.length === 0) {
throw new Error('Cannot select from empty array'); throw new Error('Cannot select from empty array');
@@ -75,9 +65,7 @@ export function selectRandom<T>(arr: T[], rng: SeededRandom): T {
return arr[index]; return arr[index];
} }
/** /** Select multiple random items from array using seeded random without duplicates */
* Select multiple random items from array using seeded random without duplicates
*/
export function selectRandomMultiple<T>(arr: T[], count: number, rng: SeededRandom): T[] { export function selectRandomMultiple<T>(arr: T[], count: number, rng: SeededRandom): T[] {
if (arr.length === 0) { if (arr.length === 0) {
throw new Error('Cannot select from empty array'); throw new Error('Cannot select from empty array');
@@ -104,11 +92,11 @@ export function selectRandomMultiple<T>(arr: T[], count: number, rng: SeededRand
} }
/** /**
* Parse timeBucket parameter to extract year-month key Parse timeBucket parameter to extract year-month key
* Handles both formats: Handles both formats:
* - ISO timestamp: "2024-12-01T00:00:00.000Z" -> "2024-12-01" - ISO timestamp: "2024-12-01T00:00:00.000Z" -> "2024-12-01"
* - Simple format: "2024-12-01" -> "2024-12-01" - Simple format: "2024-12-01" -> "2024-12-01"
*/ */
export function parseTimeBucketKey(timeBucket: string): string { export function parseTimeBucketKey(timeBucket: string): string {
if (!timeBucket) { if (!timeBucket) {
throw new Error('timeBucket parameter cannot be empty'); throw new Error('timeBucket parameter cannot be empty');
@@ -150,6 +138,7 @@ export function getMockAsset(
return null; return null;
} }
// eslint-disable-next-line unicorn/no-unnecessary-parameters
const isInDifferentPeriod = (date1: DateTime, date2: DateTime): boolean => { const isInDifferentPeriod = (date1: DateTime, date2: DateTime): boolean => {
if (unit === 'day') { if (unit === 'day') {
return !date1.startOf('day').equals(date2.startOf('day')); return !date1.startOf('day').equals(date2.startOf('day'));
@@ -100,7 +100,7 @@ test.describe('OCR with stacked assets', () => {
const stackThumbnails = page.locator('#stack-slideshow [data-asset]'); const stackThumbnails = page.locator('#stack-slideshow [data-asset]');
await expect(stackThumbnails).toHaveCount(2); await expect(stackThumbnails).toHaveCount(2);
await stackThumbnails.nth(1).click(); await page.locator(`#stack-slideshow [data-asset="${secondAssetDto.id}"]`).click();
// refreshOcr() clears showOverlay when switching assets, so re-enable it // refreshOcr() clears showOverlay when switching assets, so re-enable it
await expect(ocrBoxes).toHaveCount(0); await expect(ocrBoxes).toHaveCount(0);
@@ -242,6 +242,7 @@ test.describe('OCR text interaction', () => {
return; return;
} }
// eslint-disable-next-line unicorn/no-unnecessary-parameters
const createTouch = (id: number, x: number, y: number) => { const createTouch = (id: number, x: number, y: number) => {
return new Touch({ return new Touch({
identifier: id, identifier: id,
@@ -75,8 +75,8 @@ test.describe('asset-viewer stack', () => {
await page.goto(`/photos/${fixture.primaryAsset.id}`); await page.goto(`/photos/${fixture.primaryAsset.id}`);
await ensureDetailPanelVisible(page); await ensureDetailPanelVisible(page);
const stackAssets = page.locator('#stack-slideshow [data-asset]'); const secondAsset = page.locator(`#stack-slideshow [data-asset="${secondAssetDto.id}"]`);
await stackAssets.nth(1).click(); await secondAsset.click();
const tags = page.getByTestId('detail-panel-tags').getByRole('link'); const tags = page.getByTestId('detail-panel-tags').getByRole('link');
await expect(tags.first()).toHaveText('test/2'); await expect(tags.first()).toHaveText('test/2');
@@ -150,10 +150,10 @@ test.describe('Timeline', () => {
}); });
test.describe('keyboard', () => { test.describe('keyboard', () => {
/** /**
* This text tests keyboard nativation, and also ensures that the scroll-to-asset behavior This text tests keyboard nativation, and also ensures that the scroll-to-asset behavior
* scrolls the minimum amount. That is, if you are navigating using right arrow (auto scrolling scrolls the minimum amount. That is, if you are navigating using right arrow (auto scrolling
* as necessary downwards), then the asset should always be at the lowest row of the grid. as necessary downwards), then the asset should always be at the lowest row of the grid.
*/ */
test('Next/previous asset - ArrowRight/ArrowLeft', async ({ page }) => { test('Next/previous asset - ArrowRight/ArrowLeft', async ({ page }) => {
await pageUtils.openPhotosPage(page); await pageUtils.openPhotosPage(page);
await thumbnailUtils.withAssetId(page, assets[0].id).focus(); await thumbnailUtils.withAssetId(page, assets[0].id).focus();
+5 -5
View File
@@ -270,13 +270,13 @@ export const utils = {
} }
}, },
waitForWebsocketEvent: ({ event, id, total: count, timeout: ms }: WaitOptions): Promise<void> => { waitForWebsocketEvent: ({ event, id, total: count, timeout: ms = 10_000 }: WaitOptions): Promise<void> => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
if (!id && !count) { if (!id && !count) {
reject(new Error('id or count must be provided for waitForWebsocketEvent')); reject(new Error('id or count must be provided for waitForWebsocketEvent'));
} }
const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${event} event`)), ms || 10_000); const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${event} event`)), ms);
const type = id ? `id=${id}` : `count=${count}`; const type = id ? `id=${id}` : `count=${count}`;
console.log(`Waiting for ${event} [${type}]`); console.log(`Waiting for ${event} [${type}]`);
const set = events[event]; const set = events[event];
@@ -550,7 +550,7 @@ export const utils = {
{ {
headers: asBearerAuth(accessToken), headers: asBearerAuth(accessToken),
fetch: (...args: Parameters<typeof fetch>) => fetch: (...args: Parameters<typeof fetch>) =>
// eslint-disable-next-line unicorn/no-invalid-argument-count, unicorn/prefer-await // eslint-disable-next-line unicorn/prefer-await
fetch(...args).then((response) => { fetch(...args).then((response) => {
setCookie = response.headers.getSetCookie(); setCookie = response.headers.getSetCookie();
return response; return response;
@@ -660,10 +660,10 @@ export const utils = {
return !jobCounts.active && (!jobCounts.waiting || queues[queue].queueStatus.isPaused); return !jobCounts.active && (!jobCounts.waiting || queues[queue].queueStatus.isPaused);
}, },
waitForQueueFinish: (accessToken: string, queue: keyof QueuesResponseLegacyDto, ms?: number) => { waitForQueueFinish: (accessToken: string, queue: keyof QueuesResponseLegacyDto, ms: number = 15_000) => {
// eslint-disable-next-line no-async-promise-executor // eslint-disable-next-line no-async-promise-executor
return new Promise<void>(async (resolve, reject) => { return new Promise<void>(async (resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Timed out waiting for queue to empty')), ms || 15_000); const timeout = setTimeout(() => reject(new Error('Timed out waiting for queue to empty')), ms);
while (true) { while (true) {
const done = await utils.isQueueEmpty(accessToken, queue); const done = await utils.isQueueEmpty(accessToken, queue);
+5 -3
View File
@@ -87,7 +87,7 @@
"failed_job_command": "Command {command} failed for job: {job}", "failed_job_command": "Command {command} failed for job: {job}",
"force_delete_user_warning": "WARNING: This will immediately remove the user and all assets. This cannot be undone and the files cannot be recovered.", "force_delete_user_warning": "WARNING: This will immediately remove the user and all assets. This cannot be undone and the files cannot be recovered.",
"image_format": "Format", "image_format": "Format",
"image_format_description": "WebP produces smaller files than JPEG, but is slower to encode.", "image_format_description": "WebP produces smaller files than JPEG, but is slower to encode. JPEG XL is the best option for high-quality compression, but not all browsers and apps can display it.",
"image_fullsize_description": "Full-size image with stripped metadata, used when zoomed in", "image_fullsize_description": "Full-size image with stripped metadata, used when zoomed in",
"image_fullsize_enabled": "Enable full-size image generation", "image_fullsize_enabled": "Enable full-size image generation",
"image_fullsize_enabled_description": "Generate full-size image for non-web-friendly formats. When \"Prefer embedded preview\" is enabled, embedded previews are used directly without conversion. Does not affect web-friendly formats like JPEG.", "image_fullsize_enabled_description": "Generate full-size image for non-web-friendly formats. When \"Prefer embedded preview\" is enabled, embedded previews are used directly without conversion. Does not affect web-friendly formats like JPEG.",
@@ -101,7 +101,7 @@
"image_preview_quality_description": "Preview quality from 1-100. Higher is better, but produces larger files and can reduce app responsiveness. Setting a low value may affect machine learning quality.", "image_preview_quality_description": "Preview quality from 1-100. Higher is better, but produces larger files and can reduce app responsiveness. Setting a low value may affect machine learning quality.",
"image_preview_title": "Preview Settings", "image_preview_title": "Preview Settings",
"image_progressive": "Progressive", "image_progressive": "Progressive",
"image_progressive_description": "Encode JPEG images progressively for gradual loading display. This has no effect on WebP images.", "image_progressive_description": "Encode JPEG images progressively for gradual loading display. This has no effect on other formats.",
"image_quality": "Quality", "image_quality": "Quality",
"image_resolution": "Resolution", "image_resolution": "Resolution",
"image_resolution_description": "Higher resolutions can preserve more detail but take longer to encode, have larger file sizes and can reduce app responsiveness.", "image_resolution_description": "Higher resolutions can preserve more detail but take longer to encode, have larger file sizes and can reduce app responsiveness.",
@@ -2263,6 +2263,8 @@
"trigger": "Trigger", "trigger": "Trigger",
"trigger_asset_metadata_extraction": "Asset Metadata Extraction", "trigger_asset_metadata_extraction": "Asset Metadata Extraction",
"trigger_asset_metadata_extraction_description": "Triggered when the EXIF metadata of an asset is extracted", "trigger_asset_metadata_extraction_description": "Triggered when the EXIF metadata of an asset is extracted",
"trigger_asset_ocr_extraction": "Asset OCR Extraction",
"trigger_asset_ocr_extraction_description": "Triggered when the OCR text of an asset is extracted",
"trigger_asset_tagged": "Asset Tagged", "trigger_asset_tagged": "Asset Tagged",
"trigger_asset_tagged_description": "Triggered when a tag is added to an asset", "trigger_asset_tagged_description": "Triggered when a tag is added to an asset",
"trigger_asset_uploaded": "Asset Upload", "trigger_asset_uploaded": "Asset Upload",
@@ -2338,7 +2340,7 @@
"user": "User", "user": "User",
"user_has_been_deleted": "This user has been deleted.", "user_has_been_deleted": "This user has been deleted.",
"user_id": "User ID", "user_id": "User ID",
"user_liked": "{user} liked {type, select, photo {this photo} video {this video} asset {this asset} other {it}}", "user_liked": "{user} liked {type, select, image {this photo} video {this video} other {this}}",
"user_pin_code_settings": "PIN Code", "user_pin_code_settings": "PIN Code",
"user_pin_code_settings_description": "Manage your PIN code", "user_pin_code_settings_description": "Manage your PIN code",
"user_privacy": "User Privacy", "user_privacy": "User Privacy",
+1 -1
View File
@@ -70,7 +70,7 @@ class Settings(BaseSettings):
ann_fp16_turbo: bool = False ann_fp16_turbo: bool = False
ann_tuning_level: int = 2 ann_tuning_level: int = 2
rknn: bool = True rknn: bool = True
rknn_threads: int = 1 rknn_threads: int | None = None
preload: PreloadModelData | None = None preload: PreloadModelData | None = None
max_batch_size: MaxBatchSize = MaxBatchSize() max_batch_size: MaxBatchSize = MaxBatchSize()
model_organization: ModelOrganization = ModelOrganization.APP model_organization: ModelOrganization = ModelOrganization.APP
@@ -100,8 +100,7 @@ SUPPORTED_PROVIDERS = [
"CPUExecutionProvider", "CPUExecutionProvider",
] ]
RKNN_SUPPORTED_SOCS = ["rk3566", "rk3568", "rk3576", "rk3588"] RKNN_NPU_CORES = {"rk3566": 1, "rk3568": 1, "rk3576": 2, "rk3588": 3}
RKNN_COREMASK_SUPPORTED_SOCS = ["rk3576", "rk3588"]
WEBLATE_TO_FLORES200 = { WEBLATE_TO_FLORES200 = {
@@ -7,10 +7,14 @@ import numpy as np
import orjson import orjson
from numpy.typing import NDArray from numpy.typing import NDArray
from PIL import Image from PIL import Image
from pillow_jxl import JpegXLImagePlugin
_PIL_RESAMPLING_METHODS = {resampling.name.lower(): resampling for resampling in Image.Resampling} _PIL_RESAMPLING_METHODS = {resampling.name.lower(): resampling for resampling in Image.Resampling}
_PUNCTUATION_TRANS = str.maketrans("", "", string.punctuation) _PUNCTUATION_TRANS = str.maketrans("", "", string.punctuation)
# Requests are already decoded in parallel, so don't start a thread per core for each one
JpegXLImagePlugin.DECODE_THREADS = 0
def resize_pil(img: Image.Image, size: int, resample: Image.Resampling = Image.Resampling.BICUBIC) -> Image.Image: def resize_pil(img: Image.Image, size: int, resample: Image.Resampling = Image.Resampling.BICUBIC) -> Image.Image:
if img.width < img.height: if img.width < img.height:
@@ -9,9 +9,10 @@ import orjson
from numpy.typing import NDArray from numpy.typing import NDArray
from immich_ml.config import log, settings from immich_ml.config import log, settings
from immich_ml.models.constants import RKNN_NPU_CORES
from immich_ml.schemas import ModelInput, ModelTensor, SessionNode, Shape from immich_ml.schemas import ModelInput, ModelTensor, SessionNode, Shape
from .rknnpool import RknnNode, RknnPoolExecutor, is_available, native_outputs, soc_name from .rknnpool import RknnNode, RknnPoolExecutor, is_available, soc_name
is_available = is_available and settings.rknn is_available = is_available and settings.rknn
model_prefix = Path("rknpu") / soc_name if is_available and soc_name is not None else None model_prefix = Path("rknpu") / soc_name if is_available and soc_name is not None else None
@@ -21,12 +22,6 @@ def model_path(model_dir: Path, variant: str = "") -> Path:
return (model_dir / model_prefix if model_prefix else model_dir) / variant / "model.rknn" return (model_dir / model_prefix if model_prefix else model_dir) / variant / "model.rknn"
def run_inference(rknn_lite: Any, inputs: list[ModelTensor], data_format: str | None) -> list[NDArray[np.float32]]:
rknn_lite.rknn_runtime.set_inputs(inputs, None, data_format)
rknn_lite.rknn_runtime.run(False)
return native_outputs(rknn_lite)
def input_layout(compiled: tuple[int, ...], array: ModelTensor) -> str | None: def input_layout(compiled: tuple[int, ...], array: ModelTensor) -> str | None:
if array.ndim != 4: if array.ndim != 4:
return None # token ids carry no layout return None # token ids carry no layout
@@ -39,11 +34,11 @@ def input_layout(compiled: tuple[int, ...], array: ModelTensor) -> str | None:
class RknnSession: class RknnSession:
def __init__(self, model_path: Path) -> None: def __init__(self, model_path: Path) -> None:
self.tpe = settings.rknn_threads self.tpe = settings.rknn_threads or RKNN_NPU_CORES.get(soc_name or "", 1)
log.info(f"Loading RKNN model from {model_path} with {self.tpe} threads.") log.info(f"Loading RKNN model from {model_path} with {self.tpe} threads.")
self.rknnpool = RknnPoolExecutor(model_path=model_path.as_posix(), tpes=self.tpe, func=run_inference) self.rknnpool = RknnPoolExecutor(model_path=model_path.as_posix(), tpes=self.tpe)
log.info(f"Loaded RKNN model from {model_path} with {self.tpe} threads.") log.info(f"Loaded RKNN model from {model_path} with {self.tpe} threads sharing its weights.")
# the shapes the binary was compiled for, among which the runtime routes itself # the shapes the binary was compiled for, among which the runtime routes itself
batch = self.rknnpool.inputs[0].shape[0] batch = self.rknnpool.inputs[0].shape[0]
declared = orjson.loads(self.rknnpool.custom_string) if self.rknnpool.custom_string else {} declared = orjson.loads(self.rknnpool.custom_string) if self.rknnpool.custom_string else {}
@@ -77,7 +72,7 @@ class RknnSession:
input_feed: ModelInput, input_feed: ModelInput,
run_options: Any = None, run_options: Any = None,
) -> list[NDArray[np.float32]]: ) -> list[NDArray[np.float32]]:
inputs = [array if array.flags.c_contiguous else array.copy() for array in input_feed.values()] inputs = list(input_feed.values())
return self.rknnpool.run(inputs, input_layout(self.rknnpool.inputs[0].shape, inputs[0])) return self.rknnpool.run(inputs, input_layout(self.rknnpool.inputs[0].shape, inputs[0]))
@@ -1,18 +1,15 @@
# This code is from leafqycc/rknn-multi-threaded
# Following Apache License 2.0
import ctypes import ctypes
import logging
import threading
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from ctypes import byref, sizeof
from pathlib import Path from pathlib import Path
from typing import Any, Callable, NamedTuple from queue import SimpleQueue
from typing import Any, NamedTuple
import numpy as np import numpy as np
from numpy.typing import NDArray from numpy.typing import NDArray
from immich_ml.config import log from immich_ml.config import log
from immich_ml.models.constants import RKNN_COREMASK_SUPPORTED_SOCS, RKNN_SUPPORTED_SOCS from immich_ml.models.constants import RKNN_NPU_CORES
from immich_ml.schemas import ModelTensor from immich_ml.schemas import ModelTensor
@@ -20,7 +17,7 @@ def get_soc(device_tree_path: Path | str) -> str | None:
try: try:
with Path(device_tree_path).open() as f: with Path(device_tree_path).open() as f:
device_compatible_str = f.read() device_compatible_str = f.read()
for soc in RKNN_SUPPORTED_SOCS: for soc in RKNN_NPU_CORES:
if soc in device_compatible_str: if soc in device_compatible_str:
return soc return soc
log.warning("Device is not supported for RKNN") log.warning("Device is not supported for RKNN")
@@ -29,26 +26,34 @@ def get_soc(device_tree_path: Path | str) -> str | None:
return None return None
soc_name = None
is_available = False
try:
from rknnlite.api import RKNNLite, rknn_runtime
soc_name = get_soc("/proc/device-tree/compatible")
is_available = soc_name is not None
except ImportError:
log.debug("RKNN is not available")
class RknnNode(NamedTuple): class RknnNode(NamedTuple):
name: str name: str
shape: tuple[int, ...] shape: tuple[int, ...]
_CUSTOM_STRING_QUERY = 7 # RKNN_QUERY_CUSTOM_STRING # rknn_query_cmd
# RKNN_QUERY_OUTPUT_ATTR, and RKNN_QUERY_CURRENT_OUTPUT_ATTR for the shape a dynamic binary last produced _IN_OUT_NUM, _INPUT_ATTR, _OUTPUT_ATTR, _CUSTOM_STRING = 0, 1, 2, 7
_OUTPUT_QUERY, _CURRENT_OUTPUT_QUERY = 2, 15 _INPUT_DYNAMIC_RANGE, _CURRENT_OUTPUT_ATTR = 13, 15
_DTYPES = {0: np.float32, 1: np.float16} # rknn_tensor_type # rknn_tensor_format
_NCHW, _NHWC = 0, 1
# rknn_tensor_type, in enum order
_DTYPES: list[type[np.generic]] = [
np.float32,
np.float16,
np.int8,
np.uint8,
np.int16,
np.uint16,
np.int32,
np.uint32,
np.int64,
np.bool_,
]
_TYPES = {np.dtype(dtype): index for index, dtype in enumerate(_DTYPES)}
class _IoNum(ctypes.Structure):
_fields_ = [("n_input", ctypes.c_uint32), ("n_output", ctypes.c_uint32)]
class _CustomString(ctypes.Structure): class _CustomString(ctypes.Structure):
@@ -76,94 +81,194 @@ class _TensorAttr(ctypes.Structure): # rknn_tensor_attr
] ]
def native_outputs(rknn_lite: "RKNNLite") -> list[NDArray[Any]]: class _InputRange(ctypes.Structure):
"""The outputs in the precision the NPU computed them in, which RKNNLite would widen to fp32 on the CPU.""" _fields_ = [
runtime: Any = rknn_lite.rknn_runtime ("index", ctypes.c_uint32),
count = runtime.get_in_out_num()[1] ("shape_number", ctypes.c_uint32),
buffers = (rknn_runtime.RKNNOutput * count)() ("fmt", ctypes.c_int),
for index, buffer in enumerate(buffers): ("name", ctypes.c_char * 256),
buffer.index = index ("dyn_range", (ctypes.c_uint32 * 16) * 512),
if runtime.lib.rknn_outputs_get(runtime.context, count, buffers, None) != 0: ("n_dims", ctypes.c_uint32),
raise RuntimeError("RKNN inference failed!") ]
query = _CURRENT_OUTPUT_QUERY if runtime.is_dynamic_shape else _OUTPUT_QUERY
outputs = []
for buffer in buffers:
attr = _TensorAttr(index=buffer.index)
runtime.lib.rknn_query(runtime.context, query, ctypes.byref(attr), ctypes.sizeof(attr))
data = ctypes.cast(buffer.buf, ctypes.POINTER(ctypes.c_char * buffer.size)).contents
outputs.append(np.frombuffer(data, _DTYPES[attr.type]).reshape(attr.dims[: attr.n_dims]).copy())
runtime.lib.rknn_outputs_release(runtime.context, count, buffers)
return outputs
def tensor_nodes(rknn_lite: "RKNNLite") -> tuple[list[RknnNode], list[RknnNode]]: class _Input(ctypes.Structure):
runtime = rknn_lite.rknn_runtime _fields_ = [
n_inputs, n_outputs = runtime.get_in_out_num() ("index", ctypes.c_uint32),
("buf", ctypes.c_void_p),
def node(index: int, is_output: bool) -> RknnNode: ("size", ctypes.c_uint32),
attr = runtime.get_tensor_attr(index, is_output=is_output) ("pass_through", ctypes.c_uint8),
return RknnNode(attr.name.decode(), tuple(attr.dims[: attr.n_dims])) ("type", ctypes.c_int),
("fmt", ctypes.c_int),
return [node(i, False) for i in range(n_inputs)], [node(i, True) for i in range(n_outputs)] ]
def custom_string(rknn_lite: "RKNNLite") -> str: class _Output(ctypes.Structure):
runtime: Any = rknn_lite.rknn_runtime _fields_ = [
buffer = _CustomString() ("want_float", ctypes.c_uint8),
# rknnlite declares argtypes on the shared handle, so pass plain values and let ctypes convert ("is_prealloc", ctypes.c_uint8),
runtime.lib.rknn_query(runtime.context, _CUSTOM_STRING_QUERY, ctypes.byref(buffer), ctypes.sizeof(buffer)) ("index", ctypes.c_uint32),
declared: str = buffer.string.decode() ("buf", ctypes.c_void_p),
return declared ("size", ctypes.c_uint32),
]
def init_rknn(model_path: str) -> "RKNNLite": _Key = tuple[tuple[int, ...], ...]
if not is_available: _Spec = tuple[tuple[int, ...], np.dtype[Any]]
raise RuntimeError("rknn is not available!") _context = ctypes.c_uint64 # rknn_context on 64-bit
rknn_lite = RKNNLite() _ctx_p = ctypes.POINTER(_context)
rknn_lite.rknn_log.logger.setLevel(logging.ERROR) _u32 = ctypes.c_uint32
ret = rknn_lite.load_rknn(model_path)
soc_name = None
is_available = False
try:
lib = ctypes.CDLL("librknnrt.so") # CDLL releases the GIL for each call, so contexts run in parallel
lib.rknn_init.argtypes = _ctx_p, ctypes.c_char_p, _u32, _u32, ctypes.c_void_p
lib.rknn_dup_context.argtypes = _ctx_p, _ctx_p
lib.rknn_destroy.argtypes = (_context,)
lib.rknn_query.argtypes = _context, ctypes.c_int, ctypes.c_void_p, _u32
lib.rknn_set_input_shapes.argtypes = _context, _u32, ctypes.POINTER(_TensorAttr)
lib.rknn_inputs_set.argtypes = _context, _u32, ctypes.POINTER(_Input)
lib.rknn_run.argtypes = _context, ctypes.c_void_p
lib.rknn_outputs_get.argtypes = _context, _u32, ctypes.POINTER(_Output), ctypes.c_void_p
lib.rknn_outputs_release.argtypes = _context, _u32, ctypes.POINTER(_Output)
soc_name = get_soc("/proc/device-tree/compatible")
is_available = soc_name is not None
except OSError:
log.debug("RKNN is not available")
def _check(ret: int, call: str) -> None:
if ret != 0: if ret != 0:
raise RuntimeError("Failed to load RKNN model") raise RuntimeError(f"{call} failed with RKNN error {ret}")
if soc_name in RKNN_COREMASK_SUPPORTED_SOCS:
ret = rknn_lite.init_runtime(core_mask=RKNNLite.NPU_CORE_AUTO)
else:
ret = rknn_lite.init_runtime() # Please do not set this parameter on other platforms.
if ret != 0: def _query(ctx: int, cmd: int, info: ctypes.Structure) -> None:
raise RuntimeError("Failed to initialize RKNN runtime environment") _check(lib.rknn_query(ctx, cmd, byref(info), sizeof(info)), "rknn_query")
return rknn_lite
def _attr(ctx: int, cmd: int, index: int) -> _TensorAttr:
attr = _TensorAttr(index=index)
_query(ctx, cmd, attr)
return attr
def _specs(attrs: list[_TensorAttr]) -> list[_Spec]:
return [(tuple(attr.dims[: attr.n_dims]), np.dtype(_DTYPES[attr.type])) for attr in attrs]
class RknnPoolExecutor: class RknnPoolExecutor:
def __init__( """One context per thread, all but the first duplicated from it so that they share its weights.
self,
model_path: str,
tpes: int,
func: Callable[["RKNNLite", list[ModelTensor], str | None], list[NDArray[np.float32]]],
) -> None:
self.tpes = tpes
self.rknn_pool = [init_rknn(model_path) for _ in range(tpes)]
self.inputs, self.outputs = tensor_nodes(self.rknn_pool[0])
self.custom_string = custom_string(self.rknn_pool[0])
self.pool = ThreadPoolExecutor(max_workers=tpes)
self.func = func
self.num = 0
self.lock = threading.Lock()
def run(self, inputs: list[ModelTensor], data_format: str | None) -> list[NDArray[np.float32]]: The model is loaded by path rather than from a buffer, so the runtime holds the only copy of it."""
with self.lock:
idx = self.num % self.tpes
self.num += 1
fut = self.pool.submit(self.func, self.rknn_pool[idx], inputs, data_format) def __init__(self, model_path: str, tpes: int) -> None:
return fut.result() self.contexts: list[int] = []
self.executor = ThreadPoolExecutor(tpes, thread_name_prefix="rknn")
master = _context()
_check(lib.rknn_init(byref(master), model_path.encode(), 0, 0, None), "rknn_init")
self.contexts.append(master.value)
for _ in range(tpes - 1):
child = _context()
_check(lib.rknn_dup_context(byref(master), byref(child)), "rknn_dup_context")
self.contexts.append(child.value)
self.idle: SimpleQueue[int] = SimpleQueue()
for ctx in self.contexts:
self.idle.put(ctx)
io = _IoNum()
_query(master.value, _IN_OUT_NUM, io)
self.input_attrs = [_attr(master.value, _INPUT_ATTR, i) for i in range(io.n_input)]
self.output_attrs = [_attr(master.value, _OUTPUT_ATTR, i) for i in range(io.n_output)]
self.inputs = [RknnNode(a.name.decode(), tuple(a.dims[: a.n_dims])) for a in self.input_attrs]
self.outputs = [RknnNode(a.name.decode(), tuple(a.dims[: a.n_dims])) for a in self.output_attrs]
custom = _CustomString()
lib.rknn_query(master.value, _CUSTOM_STRING, byref(custom), sizeof(custom))
self.custom_string = custom.string.decode()
ranges = [_InputRange(index=index) for index in range(io.n_input)]
for rng in ranges:
lib.rknn_query(master.value, _INPUT_DYNAMIC_RANGE, byref(rng), sizeof(rng))
self.formats = [rng.fmt for rng in ranges]
self.compiled = [{tuple(rng.dyn_range[i][: rng.n_dims]) for i in range(rng.shape_number)} for rng in ranges]
self.dynamic = any(self.compiled)
self.shape_attrs: dict[_Key, ctypes.Array[_TensorAttr]] = {}
self.output_specs: dict[_Key, list[_Spec]] = {(): _specs(self.output_attrs)}
self.current: dict[int, _Key] = {}
def run(self, inputs: list[ModelTensor], data_format: str | None) -> list[NDArray[Any]]:
return self.executor.submit(self._next, inputs, data_format).result()
def _next(self, inputs: list[ModelTensor], data_format: str | None) -> list[NDArray[Any]]:
ctx = self.idle.get()
try:
return self._run(ctx, inputs, data_format)
finally:
self.idle.put(ctx)
def _run(self, ctx: int, inputs: list[ModelTensor], data_format: str | None) -> list[NDArray[Any]]:
fed: list[NDArray[Any]] = [
np.ascontiguousarray(array.transpose(0, 2, 3, 1) if data_format == "nchw" and array.ndim == 4 else array)
for array in inputs
]
key = self._set_shapes(ctx, fed) if self.dynamic else ()
feed = (_Input * len(fed))(
*(
_Input(index=i, buf=array.ctypes.data, size=array.nbytes, type=_TYPES[array.dtype], fmt=_NHWC)
for i, array in enumerate(fed)
)
)
_check(lib.rknn_inputs_set(ctx, len(fed), feed), "rknn_inputs_set")
_check(lib.rknn_run(ctx, None), "rknn_run")
specs = self.output_specs.get(key)
if specs is None:
attrs = [_attr(ctx, _CURRENT_OUTPUT_ATTR, i) for i in range(len(self.output_attrs))]
specs = self.output_specs[key] = _specs(attrs)
outputs = [np.empty(dims, dtype) for dims, dtype in specs]
buffers = (_Output * len(outputs))(
*(
_Output(is_prealloc=1, index=i, buf=array.ctypes.data, size=array.nbytes)
for i, array in enumerate(outputs)
)
)
_check(lib.rknn_outputs_get(ctx, len(outputs), buffers, None), "rknn_outputs_get")
lib.rknn_outputs_release(ctx, len(outputs), buffers)
return outputs
def _set_shapes(self, ctx: int, inputs: list[NDArray[Any]]) -> _Key:
key = tuple(
self._compiled_shape(fmt, compiled, array.shape)
for fmt, compiled, array in zip(self.formats, self.compiled, inputs)
)
if self.current.get(ctx) == key:
return key
attrs = self.shape_attrs.get(key)
if attrs is None:
attrs = self.shape_attrs[key] = (_TensorAttr * len(key))()
for i, (source, compiled, fmt, shape) in enumerate(zip(self.input_attrs, self.compiled, self.formats, key)):
attrs[i] = _TensorAttr.from_buffer_copy(source)
if compiled:
attrs[i].n_dims = len(shape)
attrs[i].dims[: len(shape)] = shape
attrs[i].fmt = fmt
_check(lib.rknn_set_input_shapes(ctx, len(key), attrs), "rknn_set_input_shapes")
self.current[ctx] = key
return key
@staticmethod
def _compiled_shape(fmt: int, compiled: set[tuple[int, ...]], fed: tuple[int, ...]) -> tuple[int, ...]:
shape = (fed[0], fed[3], fed[1], fed[2]) if len(fed) == 4 and fmt == _NCHW else fed
if compiled and shape not in compiled:
raise ValueError(f"binary was compiled for {sorted(compiled)}, fed {list(fed)}")
return shape
def release(self) -> None: def release(self) -> None:
self.pool.shutdown() self.executor.shutdown()
for rknn_lite in self.rknn_pool: for ctx in reversed(self.contexts): # duplicates before the context whose weights they share
rknn_lite.release() lib.rknn_destroy(ctx)
self.contexts = []
def __del__(self) -> None: def __del__(self) -> None:
self.release() self.release()
+2 -1
View File
@@ -15,6 +15,7 @@ dependencies = [
"opencv-python-headless>=4.7.0.72,<5.0", "opencv-python-headless>=4.7.0.72,<5.0",
"orjson>=3.9.5", "orjson>=3.9.5",
"pillow>=13,<14", "pillow>=13,<14",
"pillow-jxl-plugin>=1.3.8,<2.0",
"pydantic>=2.0.0,<3", "pydantic>=2.0.0,<3",
"pydantic-settings>=2.5.2,<3", "pydantic-settings>=2.5.2,<3",
"python-multipart>=0.0.6,<1.0", "python-multipart>=0.0.6,<1.0",
@@ -56,7 +57,7 @@ cuda = [
] ]
openvino = ["onnxruntime-openvino>=1.24.1,<2"] openvino = ["onnxruntime-openvino>=1.24.1,<2"]
armnn = ["onnxruntime>=1.23.2,<2"] armnn = ["onnxruntime>=1.23.2,<2"]
rknn = ["onnxruntime>=1.23.2,<2", "rknn-toolkit-lite2>=2.3.0,<3"] rknn = ["onnxruntime>=1.23.2,<2"]
rocm = ["onnxruntime-migraphx>=1.23.2,<2"] rocm = ["onnxruntime-migraphx>=1.23.2,<2"]
[tool.uv] [tool.uv]
+252 -4
View File
@@ -1,3 +1,4 @@
import ctypes
import json import json
import os import os
import pickle import pickle
@@ -63,8 +64,9 @@ from immich_ml.schemas import (
from immich_ml.sessions.ann import AnnSession from immich_ml.sessions.ann import AnnSession
from immich_ml.sessions.ort import Device, GraphSpec, OrtSession, flush_denormals, fresh, prepared from immich_ml.sessions.ort import Device, GraphSpec, OrtSession, flush_denormals, fresh, prepared
from immich_ml.sessions.policy import ShapePolicy, batches, runs from immich_ml.sessions.policy import ShapePolicy, batches, runs
from immich_ml.sessions.rknn import RknnSession, run_inference from immich_ml.sessions.rknn import RknnSession
from immich_ml.sessions.rknn import model_path as rknn_model_path from immich_ml.sessions.rknn import model_path as rknn_model_path
from immich_ml.sessions.rknn.rknnpool import RknnPoolExecutor
class TestBase: class TestBase:
@@ -960,9 +962,11 @@ class TestRknnSession:
mocker.patch("immich_ml.sessions.rknn.is_available", True) mocker.patch("immich_ml.sessions.rknn.is_available", True)
RknnSession(model_path) RknnSession(model_path)
rknn_session.assert_called_once_with(model_path=model_path.as_posix(), tpes=tpe, func=run_inference) rknn_session.assert_called_once_with(model_path=model_path.as_posix(), tpes=tpe)
info.assert_has_calls([mock.call(f"Loaded RKNN model from {model_path} with {tpe} threads.")]) info.assert_has_calls(
[mock.call(f"Loaded RKNN model from {model_path} with {tpe} threads sharing its weights.")]
)
def test_run_rknn(self, rknn_session: mock.Mock, mocker: MockerFixture) -> None: def test_run_rknn(self, rknn_session: mock.Mock, mocker: MockerFixture) -> None:
rknn_session.return_value.load.return_value = 123 rknn_session.return_value.load.return_value = 123
@@ -974,7 +978,20 @@ class TestRknnSession:
session.run(None, input_feed) session.run(None, input_feed)
rknn_session.return_value.run.assert_called_once_with([input1, input2], "nchw") rknn_session.return_value.run.assert_called_once_with([input1, input2], "nchw")
assert all(fed.flags.c_contiguous for fed in rknn_session.return_value.run.call_args.args[0])
@pytest.mark.parametrize(
("soc", "threads", "expected"),
[("rk3566", None, 1), ("rk3576", None, 2), ("rk3588", None, 3), ("rk3588", 2, 2)],
)
def test_uses_a_context_per_npu_core_unless_set(
self, soc: str, threads: int | None, expected: int, rknn_session: mock.Mock, mocker: MockerFixture
) -> None:
mocker.patch("immich_ml.sessions.rknn.soc_name", soc)
mocker.patch.object(settings, "rknn_threads", threads)
RknnSession(Path("ViT-B-32__openai"))
assert rknn_session.call_args.kwargs["tpes"] == expected
def test_run_rknn_rejects_a_shape_the_binary_was_not_compiled_for( def test_run_rknn_rejects_a_shape_the_binary_was_not_compiled_for(
self, rknn_session: mock.Mock, mocker: MockerFixture self, rknn_session: mock.Mock, mocker: MockerFixture
@@ -1011,6 +1028,237 @@ class TestRknnSession:
assert RknnSession(Path("buffalo_l")).shapes == (Shape(batch=1),) assert RknnSession(Path("buffalo_l")).shapes == (Shape(batch=1),)
class FakeRknn:
"""librknnrt for one input and one output, writing through the pointers as the runtime does."""
def __init__(self, ranges: list[tuple[int, ...]] | None = None) -> None:
self.ranges = ranges
self.contexts = 0
self.fed: list[tuple[int, int, int]] = [] # (context, type, fmt) per run
self.data: list[bytes] = []
self.buffers: list[int] = []
self.output_queries = 0
self.set_shapes: list[tuple[int, tuple[int, ...]]] = []
self.rknn_destroy = mock.Mock(return_value=0)
self.rknn_outputs_release = mock.Mock(return_value=0)
self.rknn_run = mock.Mock(return_value=0)
def rknn_init(self, ctx: Any, model: bytes, size: int, flag: int, extend: None) -> int:
self.contexts += 1
ctx._obj.value = self.contexts
return 0
def rknn_dup_context(self, ctx_in: Any, ctx_out: Any) -> int:
self.contexts += 1
ctx_out._obj.value = self.contexts
return 0
def rknn_query(self, ctx: int, cmd: int, info: Any, size: int) -> int:
obj = info._obj
if cmd == 0:
obj.n_input, obj.n_output = 1, 1
elif cmd == 1:
self._fill(obj, b"x", self.ranges[0] if self.ranges else (1, 48, 320, 3), 0)
obj.fmt = 1
elif cmd == 2:
self._fill(obj, b"logits", (1, 40, 97), 1)
elif cmd == 7:
obj.string = b'{"dims":[{}]}'
elif cmd == 13:
if self.ranges is None:
return -1
obj.shape_number, obj.n_dims, obj.fmt = len(self.ranges), 4, 1
for i, shape in enumerate(self.ranges):
obj.dyn_range[i][:4] = shape
elif cmd == 15:
self.output_queries += 1
width = dict(self.set_shapes)[ctx][2]
self._fill(obj, b"logits", (1, width // 8, 97), 1)
return 0
@staticmethod
def _fill(attr: Any, name: bytes, dims: tuple[int, ...], dtype: int) -> None:
attr.name, attr.n_dims, attr.type = name, len(dims), dtype
attr.dims[: len(dims)] = dims
def rknn_set_input_shapes(self, ctx: int, n: int, attrs: Any) -> int:
self.set_shapes.append((ctx, tuple(attrs[0].dims[: attrs[0].n_dims])))
return 0
def rknn_inputs_set(self, ctx: int, n: int, inputs: Any) -> int:
self.fed.append((ctx, inputs[0].type, inputs[0].fmt))
self.data.append(ctypes.string_at(inputs[0].buf, inputs[0].size))
self.buffers.append(inputs[0].buf)
return 0
def rknn_outputs_get(self, ctx: int, n: int, outputs: Any, extend: None) -> int:
assert outputs[0].is_prealloc and not outputs[0].want_float
np.frombuffer((ctypes.c_char * outputs[0].size).from_address(outputs[0].buf), np.uint8)[:] = 0
return 0
class TestRknnPool:
@pytest.fixture
def lib(self, mocker: MockerFixture) -> FakeRknn:
fake = FakeRknn()
mocker.patch("immich_ml.sessions.rknn.rknnpool.lib", fake, create=True)
return fake
@pytest.fixture
def dynamic_lib(self, mocker: MockerFixture) -> FakeRknn:
fake = FakeRknn(ranges=[(1, 48, 320, 3), (1, 48, 640, 3)])
mocker.patch("immich_ml.sessions.rknn.rknnpool.lib", fake, create=True)
return fake
def test_duplicates_one_context_per_thread(self, lib: FakeRknn) -> None:
pool = RknnPoolExecutor("model.rknn", 3)
assert pool.contexts == [1, 2, 3]
assert pool.inputs[0] == ("x", (1, 48, 320, 3))
assert pool.outputs[0] == ("logits", (1, 40, 97))
assert pool.custom_string == '{"dims":[{}]}'
assert not pool.dynamic
pool.release()
assert lib.rknn_destroy.call_args_list == [mock.call(3), mock.call(2), mock.call(1)]
def test_feeds_the_dtype_given_as_nhwc(self, lib: FakeRknn) -> None:
pool = RknnPoolExecutor("model.rknn", 1)
image = np.arange(48 * 320 * 3, dtype=np.uint8).reshape(1, 48, 320, 3)
pool.run([image], "nhwc")
pool.run([image.transpose(0, 3, 1, 2).astype(np.float32)], "nchw")
assert lib.fed == [(1, 3, 1), (1, 0, 1)] # (context, rknn_tensor_type, rknn_tensor_format)
assert lib.data[0] == image.tobytes()
assert lib.data[1] == image.astype(np.float32).tobytes() # transposed back to channels last
def test_feeds_a_channels_first_view_of_channels_last_data_without_a_copy(self, lib: FakeRknn) -> None:
pool = RknnPoolExecutor("model.rknn", 1)
image = np.zeros((1, 48, 320, 3), dtype=np.float32)
pool.run([image.transpose(0, 3, 1, 2)], "nchw")
assert lib.buffers == [image.ctypes.data]
def test_makes_every_input_contiguous(self, lib: FakeRknn) -> None:
pool = RknnPoolExecutor("model.rknn", 1)
image = np.arange(48 * 640 * 3, dtype=np.uint8).reshape(1, 48, 640, 3)[:, :, ::2]
pool.run([image], "nhwc")
assert lib.data == [np.ascontiguousarray(image).tobytes()]
def test_returns_outputs_in_the_precision_computed(self, lib: FakeRknn) -> None:
pool = RknnPoolExecutor("model.rknn", 1)
[output] = pool.run([np.zeros((1, 48, 320, 3), dtype=np.uint8)], "nhwc")
assert output.dtype == np.float16
assert output.shape == (1, 40, 97)
lib.rknn_outputs_release.assert_called_once()
def test_a_failed_init_destroys_nothing(self, lib: FakeRknn, mocker: MockerFixture) -> None:
mocker.patch.object(lib, "rknn_init", return_value=-6)
with pytest.raises(RuntimeError, match="rknn_init failed"):
RknnPoolExecutor("model.rknn", 3)
lib.rknn_destroy.assert_not_called()
def test_returns_the_context_when_a_run_fails(self, lib: FakeRknn) -> None:
pool = RknnPoolExecutor("model.rknn", 2)
lib.rknn_run.return_value = -1
with pytest.raises(RuntimeError, match="rknn_run failed"):
pool.run([np.zeros((1, 48, 320, 3), dtype=np.uint8)], "nhwc")
assert pool.idle.qsize() == 2
def test_runs_on_every_context_at_once(self, lib: FakeRknn) -> None:
pool = RknnPoolExecutor("model.rknn", 3)
barrier = threading.Barrier(3, timeout=5) # breaks unless all three are inside rknn_run together
lib.rknn_run.side_effect = lambda ctx, extend: barrier.wait() * 0
with ThreadPoolExecutor(3) as executor:
futures = [executor.submit(pool.run, [np.zeros((1, 48, 320, 3), dtype=np.uint8)], "nhwc") for _ in range(3)]
[future.result() for future in futures]
assert sorted(ctx for ctx, _, _ in lib.fed) == [1, 2, 3]
def test_a_waiting_run_is_not_overtaken_by_the_caller_that_just_ran(self, lib: FakeRknn) -> None:
pool = RknnPoolExecutor("model.rknn", 1)
def run(ctx: int, extend: None) -> int:
time.sleep(0.01)
return 0
lib.rknn_run.side_effect = run
busy, waiting = np.zeros((1, 48, 320, 3), np.uint8), np.ones((1, 48, 320, 3), np.uint8)
caller = threading.Thread(target=lambda: [pool.run([busy], "nhwc") for _ in range(20)])
caller.start()
time.sleep(0.005)
pool.run([waiting], "nhwc")
caller.join(5)
assert [data[0] for data in lib.data].index(1) <= 2
def test_does_not_set_shapes_for_a_static_binary(self, lib: FakeRknn) -> None:
pool = RknnPoolExecutor("model.rknn", 1)
pool.run([np.zeros((1, 48, 320, 3), dtype=np.uint8)], "nhwc")
assert lib.set_shapes == []
def test_routes_a_dynamic_binary_to_the_shape_fed(self, dynamic_lib: FakeRknn) -> None:
pool = RknnPoolExecutor("model.rknn", 1)
[wide] = pool.run([np.zeros((1, 3, 48, 640), dtype=np.float32)], "nchw")
[narrow] = pool.run([np.zeros((1, 48, 320, 3), dtype=np.uint8)], "nhwc")
assert pool.dynamic
assert dynamic_lib.set_shapes == [(1, (1, 48, 640, 3)), (1, (1, 48, 320, 3))]
assert wide.shape == (1, 80, 97)
assert narrow.shape == (1, 40, 97)
def test_sets_the_shape_on_the_context_that_runs(self, dynamic_lib: FakeRknn) -> None:
pool = RknnPoolExecutor("model.rknn", 2)
image = np.zeros((1, 48, 640, 3), dtype=np.uint8)
pool.run([image], "nhwc")
pool.run([image], "nhwc")
assert dynamic_lib.set_shapes == [(1, (1, 48, 640, 3)), (2, (1, 48, 640, 3))]
def test_sets_a_shape_again_only_when_it_changes(self, dynamic_lib: FakeRknn) -> None:
pool = RknnPoolExecutor("model.rknn", 1)
wide, narrow = np.zeros((1, 48, 640, 3), dtype=np.uint8), np.zeros((1, 48, 320, 3), dtype=np.uint8)
for image in (wide, wide, narrow, narrow, wide):
pool.run([image], "nhwc")
assert dynamic_lib.set_shapes == [(1, (1, 48, 640, 3)), (1, (1, 48, 320, 3)), (1, (1, 48, 640, 3))]
def test_queries_output_shapes_once_per_input_shape(self, dynamic_lib: FakeRknn) -> None:
pool = RknnPoolExecutor("model.rknn", 2)
image = np.zeros((1, 48, 640, 3), dtype=np.uint8)
outputs = [pool.run([image], "nhwc")[0] for _ in range(4)]
assert dynamic_lib.output_queries == 1
assert all(output.shape == (1, 80, 97) for output in outputs)
def test_rejects_a_shape_the_binary_was_not_compiled_for(self, dynamic_lib: FakeRknn) -> None:
pool = RknnPoolExecutor("model.rknn", 1)
with pytest.raises(ValueError, match="compiled for"):
pool.run([np.zeros((1, 48, 480, 3), dtype=np.uint8)], "nhwc")
assert pool.idle.qsize() == 1
class TestCLIP: class TestCLIP:
embedding = np.random.rand(512).astype(np.float32) embedding = np.random.rand(512).astype(np.float32)
cache_dir = Path("test_cache") cache_dir = Path("test_cache")
+73 -76
View File
@@ -436,7 +436,7 @@ wheels = [
[[package]] [[package]]
name = "fastapi" name = "fastapi"
version = "0.148.0" version = "0.162.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "annotated-doc" }, { name = "annotated-doc" },
@@ -446,9 +446,9 @@ dependencies = [
{ name = "typing-extensions" }, { name = "typing-extensions" },
{ name = "typing-inspection" }, { name = "typing-inspection" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/72/da/e962004c32f34fb630e8393456cadcd08d4300b6fa0099ec0ae5052b8418/fastapi-0.148.0.tar.gz", hash = "sha256:06ee16c50e429daf993a58ecbb5306d284c4c1ee8135380af39a5e1cfa715187", size = 482594, upload-time = "2026-10-19T12:26:05.615Z" } sdist = { url = "https://files.pythonhosted.org/packages/e9/eb/2cf8c80abf09b2800aef0a83b41066fbfda8ee5495b7a08b9d1af40f7046/fastapi-0.162.0.tar.gz", hash = "sha256:6a22c1e2e6e32e0633ee7e170f00009a29b8228b16feb219ee42f32fd8d88a8f", size = 527867, upload-time = "2026-10-23T18:47:33.264Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/11/8e/7d284c1f06093a5e96e1704369a4053b93a9cf4d41b5b40f5dbf94bde819/fastapi-0.148.0-py3-none-any.whl", hash = "sha256:2848088aa482c875c180ef92b7139fec345393d09cdcf258ba4b8555ab068a3b", size = 145110, upload-time = "2026-10-19T12:26:03.9Z" }, { url = "https://files.pythonhosted.org/packages/b4/4f/3f2c25c9b43970b5e9a08beb5e04776247edea684ddc0930f4f02f0bb6cb/fastapi-0.162.0-py3-none-any.whl", hash = "sha256:3785963de305dd09912426cb81f2d230fce98ab3521cdfe97265c8d14983128c", size = 148912, upload-time = "2026-10-23T18:47:31.734Z" },
] ]
[[package]] [[package]]
@@ -842,6 +842,7 @@ dependencies = [
{ name = "opencv-python-headless" }, { name = "opencv-python-headless" },
{ name = "orjson" }, { name = "orjson" },
{ name = "pillow" }, { name = "pillow" },
{ name = "pillow-jxl-plugin" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pydantic-settings" }, { name = "pydantic-settings" },
{ name = "python-multipart" }, { name = "python-multipart" },
@@ -867,7 +868,6 @@ openvino = [
] ]
rknn = [ rknn = [
{ name = "onnxruntime" }, { name = "onnxruntime" },
{ name = "rknn-toolkit-lite2" },
] ]
rocm = [ rocm = [
{ name = "onnxruntime-migraphx" }, { name = "onnxruntime-migraphx" },
@@ -936,12 +936,12 @@ requires-dist = [
{ name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" }, { name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" },
{ name = "orjson", specifier = ">=3.9.5" }, { name = "orjson", specifier = ">=3.9.5" },
{ name = "pillow", specifier = ">=13,<14" }, { name = "pillow", specifier = ">=13,<14" },
{ name = "pillow-jxl-plugin", specifier = ">=1.3.8,<2.0" },
{ name = "pydantic", specifier = ">=2.0.0,<3" }, { name = "pydantic", specifier = ">=2.0.0,<3" },
{ name = "pydantic-settings", specifier = ">=2.5.2,<3" }, { name = "pydantic-settings", specifier = ">=2.5.2,<3" },
{ name = "python-multipart", specifier = ">=0.0.6,<1.0" }, { name = "python-multipart", specifier = ">=0.0.6,<1.0" },
{ name = "rapidocr", specifier = ">=3.1.0" }, { name = "rapidocr", specifier = ">=3.1.0" },
{ name = "rich", specifier = ">=13.4.2" }, { name = "rich", specifier = ">=13.4.2" },
{ name = "rknn-toolkit-lite2", marker = "extra == 'rknn'", specifier = ">=2.3.0,<3" },
{ name = "tokenizers", specifier = ">=0.15.0,<1.0" }, { name = "tokenizers", specifier = ">=0.15.0,<1.0" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.22.0,<1.0" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.22.0,<1.0" },
] ]
@@ -1482,7 +1482,7 @@ wheels = [
[[package]] [[package]]
name = "onnxruntime" name = "onnxruntime"
version = "1.30.0" version = "1.31.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "flatbuffers" }, { name = "flatbuffers" },
@@ -1491,25 +1491,25 @@ dependencies = [
{ name = "protobuf" }, { name = "protobuf" },
] ]
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/31/6f/48169f2e62b405bff5053cbd1d73fb5ce41ef7ecd13bb3bfcc191e689b8a/onnxruntime-1.30.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:001ed726c9bd5e2bc92faade7d37d889e9606a350b7d5529f0227df2e3bb57fd", size = 21544867, upload-time = "2026-09-10T16:31:19.876Z" }, { url = "https://files.pythonhosted.org/packages/b3/bd/2ac094311163b803e3626c3937461d6900934bd56cca7601f6150ff860c3/onnxruntime-1.31.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:aaab9b3af536b06ca27ab5e35e3d429c97457ce76cf298af103f687e8b9975c0", size = 20882054, upload-time = "2026-10-09T04:18:18.811Z" },
{ url = "https://files.pythonhosted.org/packages/16/bd/cbc5b8f91963689fdd622f463508c01d0aa95d3f944747b1e0b1eb2160b8/onnxruntime-1.30.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:6c32a000d5139a38ba9349030b0032e3331acb559d596b22738d9d2b343a2b83", size = 21345202, upload-time = "2026-09-10T16:31:23.361Z" }, { url = "https://files.pythonhosted.org/packages/53/1a/561b43ca1536d9e81d1785bb8a1a260a9e314ef6d04976ba0411c652bda1/onnxruntime-1.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:35758d7606d578ec5b9d65f6e8a1f488013194c3f6097038a3223cb26d35ef9a", size = 21420804, upload-time = "2026-10-09T04:18:21.729Z" },
{ url = "https://files.pythonhosted.org/packages/34/35/e7f862dbacbc99fadd9b14a614e49c99bf0f35fd9927a82f096e3de33531/onnxruntime-1.30.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:fa688e7891a6aa206636fe7372e27ee75fd17713289f6b4fc7b190e0a7de9328", size = 23585654, upload-time = "2026-09-10T16:31:26.65Z" }, { url = "https://files.pythonhosted.org/packages/6c/44/1e9e762b95b7da0a8424913a1ed7c38cdaf88624a3c41ddba24ebac88bc9/onnxruntime-1.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:5e129d6c56abd53e659cb70f00a108d6824086470ff99c2e47a82e5786563db3", size = 23760984, upload-time = "2026-10-09T04:18:24.61Z" },
{ url = "https://files.pythonhosted.org/packages/a6/13/0f1699f6de549c9324bc9112a2a85b14c517904cd11b562a654643b755a1/onnxruntime-1.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:f3501472571f1b1eee50e017851e7929f5ea37312d2d8c2494a19e8fc58b4a38", size = 14311470, upload-time = "2026-09-10T16:31:31.273Z" }, { url = "https://files.pythonhosted.org/packages/be/ed/b12cea136ccd7b03d924f46b8393faf7ceac21115c0c50e729faa248cf23/onnxruntime-1.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:09d56445c1753e66e0912de69d3f0184016ad9a191dcd6925bf5dd570d2bfbe5", size = 14888841, upload-time = "2026-10-09T04:18:27.62Z" },
{ url = "https://files.pythonhosted.org/packages/a2/17/02b13e5f51461f0453b18ab854e2d0bc1b6ec353241b05a2ee6b79e35d87/onnxruntime-1.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:dc4c706f1935ebb62356e6a095b047859badd854482c40560888e95c328ed262", size = 14175072, upload-time = "2026-09-10T16:31:34.541Z" }, { url = "https://files.pythonhosted.org/packages/02/ad/37bbc51dcb5cd105c5b2fe98f122b23e90171c2719516964edc65bb1d4cc/onnxruntime-1.31.0-cp312-cp312-win_arm64.whl", hash = "sha256:5c54a0eb7b2b4eef3eb9dcfaf82f5ce880db07288dc309574f6657e9da5cc754", size = 14740604, upload-time = "2026-10-09T04:18:30.399Z" },
{ url = "https://files.pythonhosted.org/packages/f0/75/508454c5d01f31641dabc597fe559594c931a520a2673031179319d0afd8/onnxruntime-1.30.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:05e4fc41711d1f4abd19a9124b5be7a65a506cb2670a7f14b16162ef13c58134", size = 21544990, upload-time = "2026-09-10T16:31:37.685Z" }, { url = "https://files.pythonhosted.org/packages/e0/2b/117f94d73a3bac4276c285c47e384e1b3ea67b191aa4c7592df9d3f4a136/onnxruntime-1.31.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:0ba02a44acb6203040354d9a1f160e3f37a43feac7bb05caa3e0ea545efed505", size = 20881803, upload-time = "2026-10-09T04:18:33.62Z" },
{ url = "https://files.pythonhosted.org/packages/89/06/e603c71f43f4fe3fd156a053af79cbed6e27a2c649f0988a67d97fedd39f/onnxruntime-1.30.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5327cf6aa15a02bad805fac8bd6882a62571e8b72f6f2938a8f37e6bd1966ce9", size = 21344996, upload-time = "2026-09-10T16:31:40.915Z" }, { url = "https://files.pythonhosted.org/packages/8a/d0/3677fe93ec0fa3c637744aa4c3ae6ef89a93ee229cd3c5157820f267c7bd/onnxruntime-1.31.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ad663106f6eeff3d454f24a786450459d07f30e74863851104fc1b8b3f368127", size = 21420629, upload-time = "2026-10-09T04:18:36.731Z" },
{ url = "https://files.pythonhosted.org/packages/f1/a1/ede48ab5dc54907a2999362777f541e132639fb06628ded1932058aa8a36/onnxruntime-1.30.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:86f940afc801ea9681a4da8af84fbe95e1d9ea7d80903952cc1bfad54faad38f", size = 23585560, upload-time = "2026-09-10T16:31:44.941Z" }, { url = "https://files.pythonhosted.org/packages/0d/ac/67ebbaab4b3083f2a6b27ee6c4aa400c7f8d6c72b5499aac7e4cd6ba74f5/onnxruntime-1.31.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:37fd78cee5160c7a43a1730ccb3682ffd880af9c9e80385d625c0c2f8b125809", size = 23760708, upload-time = "2026-10-09T04:18:40.883Z" },
{ url = "https://files.pythonhosted.org/packages/3c/dd/c57c529dbc6dd55eca24b12cfbeab1b6a690de72083824eca689085f55b0/onnxruntime-1.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:4b63041bd623a9a9ac5e353948436c6fa7f43edd12d6b4a4ebc340bca959ba93", size = 14311378, upload-time = "2026-09-10T16:31:48.034Z" }, { url = "https://files.pythonhosted.org/packages/c4/86/05ed2056f43b27aaf12ebc592ebd9037a26bed315958cf882f43425fd469/onnxruntime-1.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:73e0165d58ece068c2a8a1c477c90b38e5a8adbbd399fdfdfd4bd79cbc28ff8d", size = 14888306, upload-time = "2026-10-09T04:18:43.722Z" },
{ url = "https://files.pythonhosted.org/packages/11/2f/ef00b45b911e7f2115273a24ed1b43e73c8c6c9ec17f9bfa13f8492581f8/onnxruntime-1.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:c389b6887fc95e0fcb80e89b156bc2cb18e662c29df55e9326fe64140e7d7b4f", size = 14175134, upload-time = "2026-09-10T16:31:50.799Z" }, { url = "https://files.pythonhosted.org/packages/c9/93/d33bae7b1a78780c4946ce03989c59a67d42d7015ad62d2098975fc5a580/onnxruntime-1.31.0-cp313-cp313-win_arm64.whl", hash = "sha256:e51d10d2e2e1e5bbf9b126a0cd9853d3e6c4e21424518dd50160b91471be33dc", size = 14740892, upload-time = "2026-10-09T04:18:46.338Z" },
{ url = "https://files.pythonhosted.org/packages/92/0a/284fd6fe701c9a8aff39dbc119c37582ca40e9cff1ea05425a7cc8606a02/onnxruntime-1.30.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5224ba2b00284cb1c48b3edcd303c109245d66df9ec1b861858fd6de672a38e", size = 21349453, upload-time = "2026-09-10T16:31:53.898Z" }, { url = "https://files.pythonhosted.org/packages/12/05/cf44f7642269b285aada4b662c4662b14ac63f6e03e129d939c4a956a0f5/onnxruntime-1.31.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e0e050bf9ec754950a6ba9830e4032f4004d972c6f38c5642fef26d44d894965", size = 21432644, upload-time = "2026-10-09T04:18:48.925Z" },
{ url = "https://files.pythonhosted.org/packages/d6/72/4f4466f8fa1ec267a9ef5e2f3bd175c203d3da0facbdf4049dc93abbe91c/onnxruntime-1.30.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:3f9e002417f1e3bbb31ed43dafa4f22ca1b2b68832244fdee192fcaa1ae19bca", size = 23579837, upload-time = "2026-09-10T16:31:56.82Z" }, { url = "https://files.pythonhosted.org/packages/b5/8e/673315b2dd2eb99b2f4774d7a5986fe00d933ebed17ee72c441f579226e6/onnxruntime-1.31.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:e93d7c5fad20afa697ac16f376fd0306ed180f9a376e86106cc0b7d84f53ef87", size = 23773868, upload-time = "2026-10-09T04:18:51.776Z" },
{ url = "https://files.pythonhosted.org/packages/6a/03/05c9a9234688757d2876ddecf80bba908561ee11debf125bc1a427ae6f48/onnxruntime-1.30.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:8b6169c16a48429890d2f4a0c774ebf54dfe9066a998514aad0518a16d398547", size = 21545313, upload-time = "2026-09-10T16:31:59.654Z" }, { url = "https://files.pythonhosted.org/packages/9d/fb/b4c52e500c6f3d00dfc22fad4d7513524f3ea2100a24a077ee3b0daf552d/onnxruntime-1.31.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:278e0dc922ec69b05a28f59110d5421e2ec8b1d0dd46c6b10c063069a4051e72", size = 20883462, upload-time = "2026-10-09T04:18:54.978Z" },
{ url = "https://files.pythonhosted.org/packages/c6/bc/1069e58b24779ba9d2fd479db5ecb3a15a6f49b585107c898819c0789558/onnxruntime-1.30.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:d2184fddb6798136e7c478244391ca82443f5c757f59f15bb9e5ad2da5e03175", size = 21346830, upload-time = "2026-09-10T16:32:02.275Z" }, { url = "https://files.pythonhosted.org/packages/37/fb/8be04665b700cb6e874d944e9932bb3c3969d3f53e820f5c42bfd26565d0/onnxruntime-1.31.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:984c0a2c1ad6a41fbc101dc3949abe4a72254892d01a5e70d9b792711e0bfa54", size = 21421618, upload-time = "2026-10-09T04:18:58.1Z" },
{ url = "https://files.pythonhosted.org/packages/f1/38/8138eed225c5bc6ddfc05879ecac7dacc63c34b9b6f99be72839c1f6dc49/onnxruntime-1.30.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:8b611d24db2954545ce6bd9acd4670183cb368e4642450de7a9ab6474eb374ec", size = 23586333, upload-time = "2026-09-10T16:32:04.996Z" }, { url = "https://files.pythonhosted.org/packages/30/2e/5c6ec7e26a097e97ee70f2dee68b8ca4d9d26701f2f33c3f8ab585cb89fe/onnxruntime-1.31.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e4efa4a1a0bb0b5173c6a3292c181d518b8323f9d56e978635d0c09d38c94d1a", size = 23762993, upload-time = "2026-10-09T04:19:01.236Z" },
{ url = "https://files.pythonhosted.org/packages/4d/0a/748b86000fbf9518b5dfc5bdc1b924eeaca976203740fd9d30649e568ee6/onnxruntime-1.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bdd1752a8502ac1a7ccc6e878d16db6943df574d54ed7f01058a6806ae05be4", size = 14675319, upload-time = "2026-09-10T16:32:07.5Z" }, { url = "https://files.pythonhosted.org/packages/6a/66/0bf4fdb9f58efa69cf4eddde24c72aebcc628d6ff1d67c9546145c6b9922/onnxruntime-1.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:83e3dbcf6abc6189c4bdf7d329c07ba1133c88172134c266d84b4409aa3b9dbf", size = 15268709, upload-time = "2026-10-09T04:19:04.2Z" },
{ url = "https://files.pythonhosted.org/packages/2f/db/db8e4c0cf6f70f1311060560630fd21763648e01dfbba5195a2592c052ef/onnxruntime-1.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:83d543843cbd352cfa9996a6c7b92f8a480f114a18f95ff2a1acaf6921e85b6d", size = 14569360, upload-time = "2026-09-10T16:32:09.896Z" }, { url = "https://files.pythonhosted.org/packages/af/99/75a36172c1ed1d74ac0e91c11d642548081e2c9c63f15ee796564619556f/onnxruntime-1.31.0-cp314-cp314-win_arm64.whl", hash = "sha256:d2d5ac22f896c810be2b2b171392bb908f80b6c9a7e2d592ddb7435c928044e1", size = 15153795, upload-time = "2026-10-09T04:19:06.609Z" },
{ url = "https://files.pythonhosted.org/packages/24/0a/ec0a9d656e39b43887c378a5388b20c3b1e1ee43bded84580b0936466694/onnxruntime-1.30.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:265de607ba6f9814264e1d5d413fa7069d70f48e3049b5e460b3b04bdfcef294", size = 21348433, upload-time = "2026-09-10T16:32:12.772Z" }, { url = "https://files.pythonhosted.org/packages/9c/ec/23b7749edc7aad53bf4632de190399fda69a9195499426637ef1b02f06c6/onnxruntime-1.31.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d25cd65874b75fdf16149120a04d0cd4551f860a3c8e2ecec785a1903e41d8aa", size = 21432344, upload-time = "2026-10-09T04:19:09.646Z" },
{ url = "https://files.pythonhosted.org/packages/91/f0/40f74b7c00077e1e25627067ed98a70df1fef5c0e21b82849190d312554e/onnxruntime-1.30.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:67ad7f03433b6462c627d0f555dece80e6a26bc71e8542ced35cebd32142d1b7", size = 23579340, upload-time = "2026-09-10T16:32:15.532Z" }, { url = "https://files.pythonhosted.org/packages/f2/76/155ab0b265e9ceade28a8dd3858fdfa509b039f78010042c875940e32e58/onnxruntime-1.31.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1ecc1450af28d2cf362990e188ccc81b51388f317f641ad973ab4301473200f2", size = 23772576, upload-time = "2026-10-09T04:19:12.731Z" },
] ]
[[package]] [[package]]
@@ -1802,6 +1802,54 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c0/37/66b12dc476041fe523bca29c9d58fbf1ea92a4eaf73acfb9e993948f1191/pillow-13.0.0-pp312-pypy312_pp80-win_amd64.whl", hash = "sha256:567f20f1c75897d7663830da6ea8af719e03d712b1d9ee7f813319f9ece42e5a", size = 7366030, upload-time = "2026-10-16T22:18:06.638Z" }, { url = "https://files.pythonhosted.org/packages/c0/37/66b12dc476041fe523bca29c9d58fbf1ea92a4eaf73acfb9e993948f1191/pillow-13.0.0-pp312-pypy312_pp80-win_amd64.whl", hash = "sha256:567f20f1c75897d7663830da6ea8af719e03d712b1d9ee7f813319f9ece42e5a", size = 7366030, upload-time = "2026-10-16T22:18:06.638Z" },
] ]
[[package]]
name = "pillow-jxl-plugin"
version = "1.3.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
{ name = "pillow" },
]
sdist = { url = "https://files.pythonhosted.org/packages/02/75/810608d489c2d453e16a0866de5edea2bedd728e8830da22a1ec633f3b80/pillow_jxl_plugin-1.3.8.tar.gz", hash = "sha256:4430fd7757899742079c548a7d2537d6d63cf0f1d323180095bc0f6f03d9d4fa", size = 1602233, upload-time = "2026-07-17T03:23:50.387Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/bb/093aced52ebaafcd050eb21ab07ddcab7efffaa687e264f64d3f0423a4ba/pillow_jxl_plugin-1.3.8-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7f6fd0fa12dd0d98bb55df6bbfff7ad913e4ea27306fbdf5e8543fe1fa40772c", size = 2361426, upload-time = "2026-07-17T03:22:56.906Z" },
{ url = "https://files.pythonhosted.org/packages/8e/9f/f272fd205ab511475b135772e85bcaa71e5f3d081e6604b08e6e1d3982a6/pillow_jxl_plugin-1.3.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01b632dbca07b59f7d6e106dad3ac8391e9ffdab1f94f2068a3e8edff1bbd180", size = 2660368, upload-time = "2026-07-17T03:22:48.663Z" },
{ url = "https://files.pythonhosted.org/packages/74/1a/3445d2b4f6d6e4e26f10e9ccd75b60a14295b2d8eb280f0b1bd0bd7caac3/pillow_jxl_plugin-1.3.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6459d131af2cbccab20742f6fe43a3022b021dcd1ee20b05f99b1fb54d0ec525", size = 2788244, upload-time = "2026-07-17T03:22:21.809Z" },
{ url = "https://files.pythonhosted.org/packages/26/f2/3855e83fd1e4043bd7ab286d3f0ca434fbf935598fae57f1af2777620891/pillow_jxl_plugin-1.3.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b9a546ac71757f53f0ca2b14ea8f99c33b70e298a7873477f4548e2d83f817", size = 2500550, upload-time = "2026-07-17T03:22:30.993Z" },
{ url = "https://files.pythonhosted.org/packages/f0/13/d9befae0e705474e550b16c90bb6d33c3f07837af63a0e8ce4c84fb546fb/pillow_jxl_plugin-1.3.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd2afd5153e2a7ef230a3e48a679a9e1cb3e59032f79c78bb2f78010a4e9bfc1", size = 2504336, upload-time = "2026-07-17T03:22:40.143Z" },
{ url = "https://files.pythonhosted.org/packages/52/2c/3e671c8dd84f6ed4e47d588df1dc15aae61b0df0302f4bfff7b5c743045b/pillow_jxl_plugin-1.3.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b987fb202dd1c71d7f6e103363afd1128d347ffcf540d8416c257c8f1041ff3", size = 9727025, upload-time = "2026-07-17T03:23:06.554Z" },
{ url = "https://files.pythonhosted.org/packages/27/89/a63b75d9c05deb0017ff51ddf3674cef19dbfe9c2fb74117d7e4a66b9a93/pillow_jxl_plugin-1.3.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c05cf62a992f48434a6b876fe010e5cae12bd54039854ce9ae0f05c13a58c7ad", size = 8386443, upload-time = "2026-07-17T03:23:19.195Z" },
{ url = "https://files.pythonhosted.org/packages/c7/4f/b6ead8f5a50103e2c6b264a6d4778b8fcd08006c957b2ad0bccbbb306e97/pillow_jxl_plugin-1.3.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0392e5349b67522130ecff9d8dca4713e4c84662a310d2170291b229965e8fd1", size = 9032022, upload-time = "2026-07-17T03:23:30.993Z" },
{ url = "https://files.pythonhosted.org/packages/ea/53/b9aa7c156427ad4d94d8c90bd87cb502e7d6326d2689c309aa574baf1ab7/pillow_jxl_plugin-1.3.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:88b06a4e5fc0d882b188824990b6571e201ad775d22a09487c8d5ab8aed9c3b7", size = 9233286, upload-time = "2026-07-17T03:23:42.022Z" },
{ url = "https://files.pythonhosted.org/packages/32/e3/b25194909cfef9081f6a1f0e40e699e95ea96b211cae30bbf0f965d6b450/pillow_jxl_plugin-1.3.8-cp312-cp312-win32.whl", hash = "sha256:3345a45a09ba3149001c3e9a1b6991be8aab72c33097c0633e414a254cd283fa", size = 4552597, upload-time = "2026-07-17T03:24:08.129Z" },
{ url = "https://files.pythonhosted.org/packages/c3/c9/01efd23cb4827f8a1731e1a7652023a06375e933f85e3169e0761d916d6f/pillow_jxl_plugin-1.3.8-cp312-cp312-win_amd64.whl", hash = "sha256:5b1c0d7a0c7e451014b288f0504545bff0f543f2ee1581af628e01e885b75f50", size = 4937252, upload-time = "2026-07-17T03:24:00.294Z" },
{ url = "https://files.pythonhosted.org/packages/db/ef/b3aee5725df66399f529c41d97f71893e2b38f665c9278a99519112f03f3/pillow_jxl_plugin-1.3.8-cp312-cp312-win_arm64.whl", hash = "sha256:230a1d086ef5b66055e0dbc9837914becf1d1ca58d45d1404660de0a11fb63c3", size = 2330507, upload-time = "2026-07-17T03:23:53.22Z" },
{ url = "https://files.pythonhosted.org/packages/b9/10/18bb2d6ed82184e2427f7501a17c1b413eb50bdcd4ebb42bbfb187bb4290/pillow_jxl_plugin-1.3.8-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:fe24e957016dcb0c2ec35f72ff3bb8ae78a9468b282f0d423485556de492975c", size = 2361184, upload-time = "2026-07-17T03:22:58.376Z" },
{ url = "https://files.pythonhosted.org/packages/89/3e/5e1156e5c5b402d84ef924153e046ba937ac46e8b14decb3ab6e01a6f40e/pillow_jxl_plugin-1.3.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d648ef965002bee0c98dbf144b9b3c994146856fa4d3c54008b75dda10f69915", size = 2659163, upload-time = "2026-07-17T03:22:50.003Z" },
{ url = "https://files.pythonhosted.org/packages/e9/fe/e610196142343f80c98502fd204f28e901a1101db6988be6ccc40c0e8ca0/pillow_jxl_plugin-1.3.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34685b27d01e55aabb09d9436d4ddb1f446e8805da82ffd6448c07bbf563354c", size = 2787568, upload-time = "2026-07-17T03:22:23.469Z" },
{ url = "https://files.pythonhosted.org/packages/6e/18/a862b140f82be55ce62c681b63c38bf61c962f0c27e32797ebebc0d47548/pillow_jxl_plugin-1.3.8-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:288eb07090c91891ee0dc782786d937af8e53a117835606d7982c29e31072b90", size = 2499957, upload-time = "2026-07-17T03:22:32.414Z" },
{ url = "https://files.pythonhosted.org/packages/59/29/d430cd5f82c08d2950e292d195081e9dd4e2a038a7e1722d79ba7c6e7ed9/pillow_jxl_plugin-1.3.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b39ac44cbe5a98b8a364ed98a16c8c1e197ee997e32731be4be727c55e629e4", size = 2504367, upload-time = "2026-07-17T03:22:41.501Z" },
{ url = "https://files.pythonhosted.org/packages/f4/68/6806df96fdd625943c795beca932ac033c255a323b63b051903df96935b6/pillow_jxl_plugin-1.3.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0e1550f30768a0aa3f756da10b7807f6fac7fd2e5a18991eb8e00ec5908089d8", size = 9726010, upload-time = "2026-07-17T03:23:08.579Z" },
{ url = "https://files.pythonhosted.org/packages/12/df/d12f42d6e31bb47e2002fcba2db9d92aad19dc3db9ae7d4b2b76152e268a/pillow_jxl_plugin-1.3.8-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b13f860c280d9d5770f49a1909926f15c6e26e50935da52ecb78700f7c377857", size = 8386305, upload-time = "2026-07-17T03:23:20.983Z" },
{ url = "https://files.pythonhosted.org/packages/92/ee/4dc11ad5a90aa6d2eeae6eb0be2376cde0c365423036a078746b3476eca2/pillow_jxl_plugin-1.3.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:273908ffb85f68a2374b0934700939c572f32b355c9d128d4e3b6924e72ef274", size = 9031514, upload-time = "2026-07-17T03:23:32.948Z" },
{ url = "https://files.pythonhosted.org/packages/ae/76/b3125f13c9551862ad3ae7137d86959cde159b759083f3e7b5e720525fd0/pillow_jxl_plugin-1.3.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2be721646718af722f2ca5c1d6b06bf056a61a0db355cf944df53c3723119d4b", size = 9233009, upload-time = "2026-07-17T03:23:44.276Z" },
{ url = "https://files.pythonhosted.org/packages/13/16/1aa669ae22a6c36810a925bbbfb1f4521df1d0402180052331b39ef8dee0/pillow_jxl_plugin-1.3.8-cp313-cp313-win32.whl", hash = "sha256:a2716f1367ed9647da36ce15882a12b4d424892cb5a03ab4b0783039d701c7c2", size = 4551381, upload-time = "2026-07-17T03:24:09.552Z" },
{ url = "https://files.pythonhosted.org/packages/0e/e7/823f58aaea0d969f876f593a39c85c39fefe1b8ee9ca10a6ffbafe1a254a/pillow_jxl_plugin-1.3.8-cp313-cp313-win_amd64.whl", hash = "sha256:2388374b3493df9add4ef3d97cc70d6a5f51ed188dfe10d2b83ebc55e82e48d4", size = 4936720, upload-time = "2026-07-17T03:24:01.9Z" },
{ url = "https://files.pythonhosted.org/packages/3e/12/e04b73832d1b8af3c5c4646127949a362f9ca3d30f42b05156ca5ffc61ea/pillow_jxl_plugin-1.3.8-cp313-cp313-win_arm64.whl", hash = "sha256:48d13d81d1c4933e5d42dd09366a9827583808be0a2fe842abd7358653c7d92f", size = 2330087, upload-time = "2026-07-17T03:23:54.604Z" },
{ url = "https://files.pythonhosted.org/packages/66/30/c37d161345302ebda07f29d8230b15ba83b43c586f14fec98d088b18e982/pillow_jxl_plugin-1.3.8-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:30b72accb8505a46b05f709871da40f09975aea3a6b6f3cb1d646ff0a9aaba5e", size = 2362140, upload-time = "2026-07-17T03:22:59.694Z" },
{ url = "https://files.pythonhosted.org/packages/a7/be/01613af0307c31e77aced40bdf9cf3d25ce1333b3e99bb85575bdcbfeca5/pillow_jxl_plugin-1.3.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b5bbc933487207e634802f21990c365b6e61473cea34f6621420f4eda8ffd1df", size = 2660381, upload-time = "2026-07-17T03:22:51.351Z" },
{ url = "https://files.pythonhosted.org/packages/c5/8b/89636d315a31eb805681e60e569fe38d5eef3d09f13b1b6f576574387942/pillow_jxl_plugin-1.3.8-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1d06fd4eb038f431db27f1ab2e483fd6967bafb21a7757e1bd5f76b6fd93649", size = 2787875, upload-time = "2026-07-17T03:22:24.736Z" },
{ url = "https://files.pythonhosted.org/packages/2b/e0/7b9bdc388d89773a5f9ae74ff0be3b06080ef1a60be57b5897d929012622/pillow_jxl_plugin-1.3.8-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f65361ce18db59e8949c60599bfa46b7de187e28917fc39b5d816ab4b81cb16d", size = 2500834, upload-time = "2026-07-17T03:22:33.928Z" },
{ url = "https://files.pythonhosted.org/packages/24/25/0903ba5a4c8d948df9217e48182ff7d872098ddf86fe512402569f4041cc/pillow_jxl_plugin-1.3.8-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b881a40737c647a24270b73cd3f506dd16d634202e5214cb492f9270bd1b02", size = 2504805, upload-time = "2026-07-17T03:22:42.911Z" },
{ url = "https://files.pythonhosted.org/packages/4b/ef/77a15b2f3227854d4ef2563250ea57b03538ed962f406f1248c9caf045aa/pillow_jxl_plugin-1.3.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4049e48f09947a38047fc5d70817c6dffa90ffb5f14585c8c00e8b534d17c9b", size = 9725768, upload-time = "2026-07-17T03:23:10.845Z" },
{ url = "https://files.pythonhosted.org/packages/c6/95/486fc1fabd1e6cb8b520dce9898b51f2e29e991ba54e632381fe7f287300/pillow_jxl_plugin-1.3.8-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6ce20ef0887efb6dace5245f714f94b4f070ef39a1ec45161b35879c48faf30a", size = 8386447, upload-time = "2026-07-17T03:23:22.87Z" },
{ url = "https://files.pythonhosted.org/packages/05/28/7979927eb5dc79e40b31e4e07d074057722656743d324182f4a68056c2a9/pillow_jxl_plugin-1.3.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:845e61add39272ca8545f4a0e6ead977ebb7e89fa017014bb8e17273a7a09c6c", size = 9032664, upload-time = "2026-07-17T03:23:34.78Z" },
{ url = "https://files.pythonhosted.org/packages/92/b7/f606ed6fcb5fd9e177ce2b15a613a405ac22ceafc2c03bb3f58cd500ca04/pillow_jxl_plugin-1.3.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:915b8fee6a97aaf5486d0d625f26faae2e9f0d9b5892f1076f5a29b8103125a4", size = 9233868, upload-time = "2026-07-17T03:23:46.277Z" },
{ url = "https://files.pythonhosted.org/packages/a4/6d/d10a4426fc3b394d2222cb694774451e67ab052df2e15a488dfac22be561/pillow_jxl_plugin-1.3.8-cp314-cp314-win32.whl", hash = "sha256:d63d1c3566a8c9ebdd1ec6b75da816b9475d95c4f69656827d6d3957d2594ecb", size = 4554691, upload-time = "2026-07-17T03:24:11.39Z" },
{ url = "https://files.pythonhosted.org/packages/47/77/0f0297c265f1743784b6d1df719e62e49dabd1d9ede8efb0a9ba93174293/pillow_jxl_plugin-1.3.8-cp314-cp314-win_amd64.whl", hash = "sha256:6b23c83aabf4615b92ae609293d8597ca1a8d14d01ab16c703bb3a3df359796b", size = 4939714, upload-time = "2026-07-17T03:24:03.288Z" },
{ url = "https://files.pythonhosted.org/packages/ca/cf/2db398fb1a41c46fd382bad96ee90283589eaed0ec2c50cfe4afda3df72c/pillow_jxl_plugin-1.3.8-cp314-cp314-win_arm64.whl", hash = "sha256:60c36bec3d8d4ef23cf2aed0f792213f999312dd3d8cd5225ae1494c430e16f6", size = 2330667, upload-time = "2026-07-17T03:23:55.913Z" },
]
[[package]] [[package]]
name = "pluggy" name = "pluggy"
version = "1.5.0" version = "1.5.0"
@@ -2253,57 +2301,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
] ]
[[package]]
name = "rknn-toolkit-lite2"
version = "2.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "psutil" },
{ name = "ruamel-yaml" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/ff/db/64c756f3f06b219e92ff4f0fd4e000870ee49f214d505ff01c8b0275e26d/rknn_toolkit_lite2-2.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1e4ec691fed900c0e6fde5e7d8eeba17f806aa45092b63b361ee775e2c1b50e", size = 527458, upload-time = "2025-04-09T09:39:58.881Z" },
]
[[package]]
name = "ruamel-yaml"
version = "0.18.10"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ruamel-yaml-clib", marker = "python_full_version < '3.13' and platform_python_implementation == 'CPython'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ea/46/f44d8be06b85bc7c4d8c95d658be2b68f27711f279bf9dd0612a5e4794f5/ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58", size = 143447, upload-time = "2025-01-06T14:08:51.334Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/36/dfc1ebc0081e6d39924a2cc53654497f967a084a436bb64402dfce4254d9/ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1", size = 117729, upload-time = "2025-01-06T14:08:47.471Z" },
]
[[package]]
name = "ruamel-yaml-clib"
version = "0.2.12"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/20/84/80203abff8ea4993a87d823a5f632e4d92831ef75d404c9fc78d0176d2b5/ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f", size = 225315, upload-time = "2024-10-20T10:10:56.22Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/41/e7a405afbdc26af961678474a55373e1b323605a4f5e2ddd4a80ea80f628/ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632", size = 133433, upload-time = "2024-10-20T10:12:55.657Z" },
{ url = "https://files.pythonhosted.org/packages/ec/b0/b850385604334c2ce90e3ee1013bd911aedf058a934905863a6ea95e9eb4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d", size = 647362, upload-time = "2024-10-20T10:12:57.155Z" },
{ url = "https://files.pythonhosted.org/packages/44/d0/3f68a86e006448fb6c005aee66565b9eb89014a70c491d70c08de597f8e4/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c", size = 754118, upload-time = "2024-10-20T10:12:58.501Z" },
{ url = "https://files.pythonhosted.org/packages/52/a9/d39f3c5ada0a3bb2870d7db41901125dbe2434fa4f12ca8c5b83a42d7c53/ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd", size = 706497, upload-time = "2024-10-20T10:13:00.211Z" },
{ url = "https://files.pythonhosted.org/packages/b0/fa/097e38135dadd9ac25aecf2a54be17ddf6e4c23e43d538492a90ab3d71c6/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31", size = 698042, upload-time = "2024-10-21T11:26:46.038Z" },
{ url = "https://files.pythonhosted.org/packages/ec/d5/a659ca6f503b9379b930f13bc6b130c9f176469b73b9834296822a83a132/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680", size = 745831, upload-time = "2024-10-21T11:26:47.487Z" },
{ url = "https://files.pythonhosted.org/packages/db/5d/36619b61ffa2429eeaefaab4f3374666adf36ad8ac6330d855848d7d36fd/ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d", size = 715692, upload-time = "2024-12-11T19:58:17.252Z" },
{ url = "https://files.pythonhosted.org/packages/b1/82/85cb92f15a4231c89b95dfe08b09eb6adca929ef7df7e17ab59902b6f589/ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5", size = 98777, upload-time = "2024-10-20T10:13:01.395Z" },
{ url = "https://files.pythonhosted.org/packages/d7/8f/c3654f6f1ddb75daf3922c3d8fc6005b1ab56671ad56ffb874d908bfa668/ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4", size = 115523, upload-time = "2024-10-20T10:13:02.768Z" },
{ url = "https://files.pythonhosted.org/packages/29/00/4864119668d71a5fa45678f380b5923ff410701565821925c69780356ffa/ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a", size = 132011, upload-time = "2024-10-20T10:13:04.377Z" },
{ url = "https://files.pythonhosted.org/packages/7f/5e/212f473a93ae78c669ffa0cb051e3fee1139cb2d385d2ae1653d64281507/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475", size = 642488, upload-time = "2024-10-20T10:13:05.906Z" },
{ url = "https://files.pythonhosted.org/packages/1f/8f/ecfbe2123ade605c49ef769788f79c38ddb1c8fa81e01f4dbf5cf1a44b16/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef", size = 745066, upload-time = "2024-10-20T10:13:07.26Z" },
{ url = "https://files.pythonhosted.org/packages/e2/a9/28f60726d29dfc01b8decdb385de4ced2ced9faeb37a847bd5cf26836815/ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6", size = 701785, upload-time = "2024-10-20T10:13:08.504Z" },
{ url = "https://files.pythonhosted.org/packages/84/7e/8e7ec45920daa7f76046578e4f677a3215fe8f18ee30a9cb7627a19d9b4c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf", size = 693017, upload-time = "2024-10-21T11:26:48.866Z" },
{ url = "https://files.pythonhosted.org/packages/c5/b3/d650eaade4ca225f02a648321e1ab835b9d361c60d51150bac49063b83fa/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1", size = 741270, upload-time = "2024-10-21T11:26:50.213Z" },
{ url = "https://files.pythonhosted.org/packages/87/b8/01c29b924dcbbed75cc45b30c30d565d763b9c4d540545a0eeecffb8f09c/ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01", size = 709059, upload-time = "2024-12-11T19:58:18.846Z" },
{ url = "https://files.pythonhosted.org/packages/30/8c/ed73f047a73638257aa9377ad356bea4d96125b305c34a28766f4445cc0f/ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6", size = 98583, upload-time = "2024-10-20T10:13:09.658Z" },
{ url = "https://files.pythonhosted.org/packages/b0/85/e8e751d8791564dd333d5d9a4eab0a7a115f7e349595417fd50ecae3395c/ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3", size = 115190, upload-time = "2024-10-20T10:13:10.66Z" },
]
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.17.1" version = "0.17.1"
+100 -134
View File
@@ -1,29 +1,22 @@
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html # @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
lockfile_version = 3
[[tools."github:extism/cli"]] [[tools."github:extism/cli"]]
version = "1.6.3" version = "1.6.3"
backend = "github:extism/cli" backend = "github:extism/cli"
specifiers = ["1.6.3"]
[tools."github:extism/cli"."platforms.linux-arm64"] [tools."github:extism/cli"."platforms.linux-arm64"]
checksum = "sha256:d92f830c9be39637569feacb04e9750c28848df6d9a219db94152a9b4eb9452b" checksum = "sha256:d92f830c9be39637569feacb04e9750c28848df6d9a219db94152a9b4eb9452b"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-arm64.tar.gz" url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-arm64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694030" url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694030"
[tools."github:extism/cli"."platforms.linux-arm64-musl"]
checksum = "sha256:d92f830c9be39637569feacb04e9750c28848df6d9a219db94152a9b4eb9452b"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-arm64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694030"
[tools."github:extism/cli"."platforms.linux-x64"] [tools."github:extism/cli"."platforms.linux-x64"]
checksum = "sha256:34e7ae9bfded6e2c32dee83f70a4e50d34f9d3e80d1762b09625fe82e214d02d" checksum = "sha256:34e7ae9bfded6e2c32dee83f70a4e50d34f9d3e80d1762b09625fe82e214d02d"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-amd64.tar.gz" url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-amd64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694025" url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694025"
[tools."github:extism/cli"."platforms.linux-x64-musl"]
checksum = "sha256:34e7ae9bfded6e2c32dee83f70a4e50d34f9d3e80d1762b09625fe82e214d02d"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-amd64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694025"
[tools."github:extism/cli"."platforms.macos-arm64"] [tools."github:extism/cli"."platforms.macos-arm64"]
checksum = "sha256:b4ddbc575b5ac000115247f781723f9b9f284ed87b29c600539d72161b5b29fc" checksum = "sha256:b4ddbc575b5ac000115247f781723f9b9f284ed87b29c600539d72161b5b29fc"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-darwin-arm64.tar.gz" url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-darwin-arm64.tar.gz"
@@ -42,27 +35,18 @@ url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694035"
[[tools."github:extism/js-pdk"]] [[tools."github:extism/js-pdk"]]
version = "1.7.0" version = "1.7.0"
backend = "github:extism/js-pdk" backend = "github:extism/js-pdk"
specifiers = ["v1.7.0"]
[tools."github:extism/js-pdk"."platforms.linux-arm64"] [tools."github:extism/js-pdk"."platforms.linux-arm64"]
checksum = "sha256:025f4050b199d68413c159bde1187271ae270021a9f7171e7beb509922821f2a" checksum = "sha256:025f4050b199d68413c159bde1187271ae270021a9f7171e7beb509922821f2a"
url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-aarch64-linux-v1.7.0.gz" url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-aarch64-linux-v1.7.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518632503" url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518632503"
[tools."github:extism/js-pdk"."platforms.linux-arm64-musl"]
checksum = "sha256:025f4050b199d68413c159bde1187271ae270021a9f7171e7beb509922821f2a"
url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-aarch64-linux-v1.7.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518632503"
[tools."github:extism/js-pdk"."platforms.linux-x64"] [tools."github:extism/js-pdk"."platforms.linux-x64"]
checksum = "sha256:63b72da2f5e88655522dc21477de549f238a2f40546a69ce4e0fce7e78654035" checksum = "sha256:63b72da2f5e88655522dc21477de549f238a2f40546a69ce4e0fce7e78654035"
url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-x86_64-linux-v1.7.0.gz" url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-x86_64-linux-v1.7.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518632991" url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518632991"
[tools."github:extism/js-pdk"."platforms.linux-x64-musl"]
checksum = "sha256:63b72da2f5e88655522dc21477de549f238a2f40546a69ce4e0fce7e78654035"
url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-x86_64-linux-v1.7.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518632991"
[tools."github:extism/js-pdk"."platforms.macos-arm64"] [tools."github:extism/js-pdk"."platforms.macos-arm64"]
checksum = "sha256:12c01c2bb2240a6a05a4f8babe680c793c397d84e6a1497a7ca1312e78a475c3" checksum = "sha256:12c01c2bb2240a6a05a4f8babe680c793c397d84e6a1497a7ca1312e78a475c3"
url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-aarch64-macos-v1.7.0.gz" url = "https://github.com/extism/js-pdk/releases/download/v1.7.0/extism-js-aarch64-macos-v1.7.0.gz"
@@ -81,6 +65,7 @@ url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/518636347"
[[tools."github:jellyfin/jellyfin-ffmpeg"]] [[tools."github:jellyfin/jellyfin-ffmpeg"]]
version = "7.1.3-6" version = "7.1.3-6"
backend = "github:jellyfin/jellyfin-ffmpeg" backend = "github:jellyfin/jellyfin-ffmpeg"
specifiers = ["7.1.3-6"]
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.windows-x64"] [tools."github:jellyfin/jellyfin-ffmpeg"."platforms.windows-x64"]
checksum = "sha256:7b7168149689610296f3a187c717056ce0786cc125a31caf28056737e9ba1cc1" checksum = "sha256:7b7168149689610296f3a187c717056ce0786cc125a31caf28056737e9ba1cc1"
@@ -90,18 +75,33 @@ url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets
[[tools."github:jellyfin/jellyfin-ffmpeg"]] [[tools."github:jellyfin/jellyfin-ffmpeg"]]
version = "7.1.3-6" version = "7.1.3-6"
backend = "github:jellyfin/jellyfin-ffmpeg" backend = "github:jellyfin/jellyfin-ffmpeg"
specifiers = ["7.1.3-6"]
[tools."github:jellyfin/jellyfin-ffmpeg".options] [tools."github:jellyfin/jellyfin-ffmpeg".options]
asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz" asset_pattern = "jellyfin-ffmpeg_*_portable_linux64-gpl.tar.xz"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-arm64"] [tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64"]
checksum = "sha256:e024d5e78d5414e75f0181036cd21373fafb9270c72894dfd7dbda2572439820" checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_macarm64-gpl.tar.xz" url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995838" url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
[[tools."github:jellyfin/jellyfin-ffmpeg"]] [[tools."github:jellyfin/jellyfin-ffmpeg"]]
version = "7.1.3-6" version = "7.1.3-6"
backend = "github:jellyfin/jellyfin-ffmpeg" backend = "github:jellyfin/jellyfin-ffmpeg"
specifiers = ["7.1.3-6"]
[tools."github:jellyfin/jellyfin-ffmpeg".options]
asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"]
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
version = "7.1.3-6"
backend = "github:jellyfin/jellyfin-ffmpeg"
specifiers = ["7.1.3-6"]
[tools."github:jellyfin/jellyfin-ffmpeg".options] [tools."github:jellyfin/jellyfin-ffmpeg".options]
asset_pattern = "jellyfin-ffmpeg_*_portable_mac64-gpl.tar.xz" asset_pattern = "jellyfin-ffmpeg_*_portable_mac64-gpl.tar.xz"
@@ -114,61 +114,31 @@ url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets
[[tools."github:jellyfin/jellyfin-ffmpeg"]] [[tools."github:jellyfin/jellyfin-ffmpeg"]]
version = "7.1.3-6" version = "7.1.3-6"
backend = "github:jellyfin/jellyfin-ffmpeg" backend = "github:jellyfin/jellyfin-ffmpeg"
specifiers = ["7.1.3-6"]
[tools."github:jellyfin/jellyfin-ffmpeg".options] [tools."github:jellyfin/jellyfin-ffmpeg".options]
asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz" asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64"] [tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-arm64"]
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886" checksum = "sha256:e024d5e78d5414e75f0181036cd21373fafb9270c72894dfd7dbda2572439820"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz" url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_macarm64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876" url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995838"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64-musl"]
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
version = "7.1.3-6"
backend = "github:jellyfin/jellyfin-ffmpeg"
[tools."github:jellyfin/jellyfin-ffmpeg".options]
asset_pattern = "jellyfin-ffmpeg_*_portable_linux64-gpl.tar.xz"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64"]
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64-musl"]
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
[[tools."github:webassembly/binaryen"]] [[tools."github:webassembly/binaryen"]]
version = "version_124" version = "version_124"
backend = "github:webassembly/binaryen" backend = "github:webassembly/binaryen"
specifiers = ["version_124"]
[tools."github:webassembly/binaryen"."platforms.linux-arm64"] [tools."github:webassembly/binaryen"."platforms.linux-arm64"]
checksum = "sha256:6291bd9a57d8e046f3bc099a4db386c147433a87f71c783a901c5b1792e38de3" checksum = "sha256:6291bd9a57d8e046f3bc099a4db386c147433a87f71c783a901c5b1792e38de3"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-aarch64-linux.tar.gz" url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-aarch64-linux.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288927659" url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288927659"
[tools."github:webassembly/binaryen"."platforms.linux-arm64-musl"]
checksum = "sha256:6291bd9a57d8e046f3bc099a4db386c147433a87f71c783a901c5b1792e38de3"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-aarch64-linux.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288927659"
[tools."github:webassembly/binaryen"."platforms.linux-x64"] [tools."github:webassembly/binaryen"."platforms.linux-x64"]
checksum = "sha256:0290c3779fedf592b8da0ded3032ff55c41a2b7bfa2d6bf7b7bac6f0e6e28963" checksum = "sha256:0290c3779fedf592b8da0ded3032ff55c41a2b7bfa2d6bf7b7bac6f0e6e28963"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-linux.tar.gz" url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-linux.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926769" url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926769"
[tools."github:webassembly/binaryen"."platforms.linux-x64-musl"]
checksum = "sha256:0290c3779fedf592b8da0ded3032ff55c41a2b7bfa2d6bf7b7bac6f0e6e28963"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-linux.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926769"
[tools."github:webassembly/binaryen"."platforms.macos-arm64"] [tools."github:webassembly/binaryen"."platforms.macos-arm64"]
checksum = "sha256:86a2c960ff62c6d2ea6009d1f89745c22c70100d394a095eab45eb941bdaa24c" checksum = "sha256:86a2c960ff62c6d2ea6009d1f89745c22c70100d394a095eab45eb941bdaa24c"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-arm64-macos.tar.gz" url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-arm64-macos.tar.gz"
@@ -187,9 +157,10 @@ url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288
[[tools.java]] [[tools.java]]
version = "21.0.2" version = "21.0.2"
backend = "core:java" backend = "core:java"
specifiers = ["21.0.2"]
[tools.java.options] [tools.java.options]
shorthand_vendor = "openjdk" shorthand_vendor = "temurin"
[tools.java."platforms.linux-arm64"] [tools.java."platforms.linux-arm64"]
checksum = "sha256:08db1392a48d4eb5ea5315cf8f18b89dbaf36cda663ba882cf03c704c9257ec2" checksum = "sha256:08db1392a48d4eb5ea5315cf8f18b89dbaf36cda663ba882cf03c704c9257ec2"
@@ -245,123 +216,118 @@ checksum = "sha256:d2046d28c2c3dcd211ac5145c0bf30e2047ef9dfb306693c8c6e2eccd8b95
url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-win-x64.zip" url = "https://nodejs.org/dist/v24.22.1/node-v24.22.1-win-x64.zip"
[[tools."npm:@openapitools/openapi-generator-cli"]] [[tools."npm:@openapitools/openapi-generator-cli"]]
version = "2.41.0" version = "2.42.0"
aube = { path = ".mise/locks/npm-openapitools-openapi-generator-cli/2.42.0", digest = "sha256:ef9f5cf15f958d31297bb706ee83d1347633603f3079431938665c7bc4136856" }
backend = "npm:@openapitools/openapi-generator-cli" backend = "npm:@openapitools/openapi-generator-cli"
specifiers = ["2.42.0"]
[[tools."npm:oazapfts"]] [[tools."npm:oazapfts"]]
version = "7.5.0" version = "7.5.0"
aube = { path = ".mise/locks/npm-oazapfts/7.5.0", digest = "sha256:74214614b0338eaa2baee026d615afde514311c5c76e274f420f8de8b1f0e017" }
backend = "npm:oazapfts" backend = "npm:oazapfts"
specifiers = ["7.5.0"]
[[tools.opentofu]] [[tools.opentofu]]
version = "1.13.1" version = "1.13.2"
backend = "aqua:opentofu/opentofu" backend = "aqua:opentofu/opentofu"
specifiers = ["1.13.1"] specifiers = ["1.13.2"]
[tools.opentofu."platforms.linux-arm64"] [tools.opentofu."platforms.linux-arm64"]
checksum = "sha256:9c1ef375aa1852db0b2888aa921b640c71f8140d4682aa4fec99378a64fa7dc3" checksum = "sha256:88e2d9e3f94f97b76ab2faf55ade87578f56aed19c5e45fa139d70e994366d1f"
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_linux_arm64.tar.gz" checksum_source = "mise-versions"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708055" url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_linux_arm64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742799"
[tools.opentofu."platforms.linux-arm64-musl"]
checksum = "sha256:9bd0228a81bcd0c88f7045c74378f45a815779f19897191dff7d9efba9976b9e"
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_linux_arm64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762688"
[tools.opentofu."platforms.linux-x64"] [tools.opentofu."platforms.linux-x64"]
checksum = "sha256:378ada19d4bc70c43732004e8159be771b23b9a5afdf059e5f8a2b3fa2c70a69" checksum = "sha256:ac05d9952662c7db28dd57e93f0a68018837bfdf9342384ec0987977c16a9645"
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_linux_amd64.tar.gz" checksum_source = "mise-versions"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708017" url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_linux_amd64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742636"
[tools.opentofu."platforms.linux-x64-musl"]
checksum = "sha256:50a6106fa4de523d09c87af85f3db1dd47535fc005727fdca6852146476b88ec"
url = "https://github.com/opentofu/opentofu/releases/download/v1.12.6/tofu_1.12.6_linux_amd64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/520762723"
[tools.opentofu."platforms.macos-arm64"] [tools.opentofu."platforms.macos-arm64"]
checksum = "sha256:be78f659f04ef06a9dbd9b3934d46af95d787a3aa38396d459dea395261816a9" checksum = "sha256:0d1cdc85f38992055b48e7fb161304a2ddfe2e5ee0ff68ffec9d0349c8bd1b59"
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_darwin_arm64.tar.gz" checksum_source = "mise-versions"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708059" url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_darwin_arm64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742637"
[tools.opentofu."platforms.macos-x64"] [tools.opentofu."platforms.macos-x64"]
checksum = "sha256:a73720443ba38712d7d96dc1e857add02c15a790919c653ad07492e9952f8c27" checksum = "sha256:dc6e4eb1d3ed6550c2ef052596f933dbcd8c806957665ea83ae7537147cb3074"
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_darwin_amd64.tar.gz" checksum_source = "mise-versions"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708142" url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_darwin_amd64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742982"
[tools.opentofu."platforms.windows-x64"] [tools.opentofu."platforms.windows-x64"]
checksum = "sha256:6825ae311e9a15747ce2ede7b7e97c8bb744a4d271fa8113259eeeee3d09438c" checksum = "sha256:e74fc2bda0c81ce696b6b7016a4d5c1f557638161239e4101e9ee7d8e0faba59"
url = "https://github.com/opentofu/opentofu/releases/download/v1.13.1/tofu_1.13.1_windows_amd64.tar.gz" checksum_source = "mise-versions"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/603708102" url = "https://github.com/opentofu/opentofu/releases/download/v1.13.2/tofu_1.13.2_windows_amd64.tar.gz"
url_api = "https://api.github.com/repos/opentofu/opentofu/releases/assets/662742635"
[[tools.pnpm]] [[tools.pnpm]]
version = "12.14.0" version = "12.15.0"
backend = "aqua:pnpm/pnpm" backend = "aqua:pnpm/pnpm"
specifiers = ["12.14.0"] specifiers = ["12.15.0"]
[tools.pnpm."platforms.linux-arm64"] [tools.pnpm."platforms.linux-arm64"]
checksum = "sha256:cfc780b9e690549e6633e1045fb1fb6ef84e390711e2099e6ccbae91c32456ea" checksum = "sha256:d7ce93a2c7d847d3a34bd3f559b60c14f578b1320c17cc352a2c5c5fdf956117"
url = "https://github.com/pnpm/pnpm/releases/download/v12.14.0/pnpm-linux-arm64.tar.gz" url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-linux-arm64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/643957409" url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173352"
provenance = "github-attestations" provenance = "github-attestations"
[tools.pnpm."platforms.linux-x64"] [tools.pnpm."platforms.linux-x64"]
checksum = "sha256:3910c3852136bed5bc1511b9137a45fd4357ed6b3970d9d08893641e9005867e" checksum = "sha256:55b5ed0092bd11176c1669598e231ae52e7cedac59de6efd0d415730f37d0598"
url = "https://github.com/pnpm/pnpm/releases/download/v12.14.0/pnpm-linux-x64.tar.gz" url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-linux-x64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/643957410" url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173360"
provenance = "github-attestations" provenance = "github-attestations"
[tools.pnpm."platforms.macos-arm64"] [tools.pnpm."platforms.macos-arm64"]
checksum = "sha256:50f48e0e17508952790a3fbf58fcf14f3b82efa5f594ca04ad904e29aaf854b0" checksum = "sha256:6a41a2877773e97f19832067187cc6fc82e186d8a2cf91fec4b4c1a5fbd6ef27"
url = "https://github.com/pnpm/pnpm/releases/download/v12.14.0/pnpm-darwin-arm64.tar.gz" url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-darwin-arm64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/643957413" url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173343"
provenance = "github-attestations" provenance = "github-attestations"
[tools.pnpm."platforms.macos-x64"] [tools.pnpm."platforms.macos-x64"]
checksum = "sha256:2c412e07382049b4edb04fa6da78f3d4ef7810b75853c80544dcfb7c0ed79880" checksum = "sha256:2d7b336884574aee8a765bee48591212e9caf4fcf985fb3279588c1fb73fb47f"
url = "https://github.com/pnpm/pnpm/releases/download/v12.14.0/pnpm-darwin-x64.tar.gz" url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-darwin-x64.tar.gz"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/643957411" url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173347"
provenance = "github-attestations" provenance = "github-attestations"
[tools.pnpm."platforms.windows-x64"] [tools.pnpm."platforms.windows-x64"]
checksum = "sha256:6f0f38e917c8ce76ef31db528c357e2bfa354a1bcb16464e3cdec39e6530d784" checksum = "sha256:84cf69db1be19e95bb4e5bef329291a046f2821b390d87e00f5690809d8a2ed7"
url = "https://github.com/pnpm/pnpm/releases/download/v12.14.0/pnpm-win32-x64.zip" url = "https://github.com/pnpm/pnpm/releases/download/v12.15.0/pnpm-win32-x64.zip"
url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/643957404" url_api = "https://api.github.com/repos/pnpm/pnpm/releases/assets/662173351"
provenance = "github-attestations" provenance = "github-attestations"
[[tools.terragrunt]] [[tools.terragrunt]]
version = "1.1.1" version = "1.2.0"
backend = "aqua:gruntwork-io/terragrunt" backend = "aqua:gruntwork-io/terragrunt"
specifiers = ["1.2.0"]
[tools.terragrunt."platforms.linux-arm64"] [tools.terragrunt."platforms.linux-arm64"]
checksum = "sha256:a374a7993ff3d99665a7e014007d3647ec7f0465c9d55c85e9f94c932e73cea2" checksum = "sha256:1e13b7f0646fa7962bc6c5bfba14a30d6a589325b82561e21b66be5981d86395"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_linux_arm64.tar.gz" checksum_source = "mise-versions"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476786148" url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_linux_arm64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662437526"
[tools.terragrunt."platforms.linux-arm64-musl"]
checksum = "sha256:a374a7993ff3d99665a7e014007d3647ec7f0465c9d55c85e9f94c932e73cea2"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_linux_arm64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476786148"
[tools.terragrunt."platforms.linux-x64"] [tools.terragrunt."platforms.linux-x64"]
checksum = "sha256:ce90077ac31ef17a2ba10d11d45f36c6501997a8f4f79d703bb7daba37032f53" checksum = "sha256:23b5f2f9203d34919f7fd270a4b635b721b4903beca9762a1e4853b159bc9be8"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_linux_amd64.tar.gz" checksum_source = "mise-versions"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476785968" url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_linux_amd64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662437263"
[tools.terragrunt."platforms.linux-x64-musl"]
checksum = "sha256:ce90077ac31ef17a2ba10d11d45f36c6501997a8f4f79d703bb7daba37032f53"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_linux_amd64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476785968"
[tools.terragrunt."platforms.macos-arm64"] [tools.terragrunt."platforms.macos-arm64"]
checksum = "sha256:9ec8f678b9ae6c81d5d9d77b94cbf6349ce639d5938694b4adc9dea73e416794" checksum = "sha256:79790d841ce71fe94c689fb6f5034afd31ad53d5904937c7a10934cca9d9734b"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_darwin_arm64.tar.gz" checksum_source = "mise-versions"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476785628" url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_darwin_arm64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662436516"
[tools.terragrunt."platforms.macos-x64"] [tools.terragrunt."platforms.macos-x64"]
checksum = "sha256:73e768a69fa44a60f9d9174f54aaf179327e08706f633c9c79d5f5c9622c91c2" checksum = "sha256:0b0e63e80509eb1ffb4f89b892bc6fbf7728f27f0a73974400f1bd82a80b8627"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_darwin_amd64.tar.gz" checksum_source = "mise-versions"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476785520" url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_darwin_amd64.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662436142"
[tools.terragrunt."platforms.windows-x64"] [tools.terragrunt."platforms.windows-x64"]
checksum = "sha256:dd50a324691e072a3ac879e9ec43d6310e2936c929fda658eb2811fefcc75115" checksum = "sha256:c9a146b9aad456a289614023ad5778123d819af3cf9efd1c6130f196cfcb1999"
url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.1.1/terragrunt_windows_amd64.exe.tar.gz" checksum_source = "mise-versions"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/476786421" url = "https://github.com/gruntwork-io/terragrunt/releases/download/v1.2.0/terragrunt_windows_amd64.exe.tar.gz"
url_api = "https://api.github.com/repos/gruntwork-io/terragrunt/releases/assets/662438255"
+4 -4
View File
@@ -16,10 +16,10 @@ config_roots = [
[tools] [tools]
node = "24.22.1" node = "24.22.1"
pnpm = "12.14.0" pnpm = "12.15.0"
terragrunt = "1.1.1" terragrunt = "1.2.0"
opentofu = "1.13.1" opentofu = "1.13.2"
"npm:@openapitools/openapi-generator-cli" = "2.41.0" "npm:@openapitools/openapi-generator-cli" = "2.42.0"
"npm:oazapfts" = "7.5.0" "npm:oazapfts" = "7.5.0"
"github:extism/cli" = "1.6.3" "github:extism/cli" = "1.6.3"
"github:webassembly/binaryen" = "version_124" "github:webassembly/binaryen" = "version_124"
+1 -1
View File
@@ -1,6 +1,6 @@
# Immich Mobile Application - Flutter # Immich Mobile Application - Flutter
The Immich mobile app is a Flutter-based solution leveraging the Isar Database for local storage and Riverpod for state management. This structure optimizes functionality and maintainability, allowing for efficient development and robust performance. The Immich mobile app is a Flutter-based solution leveraging the Drift (SQLite) database for local storage and Riverpod for state management. This structure optimizes functionality and maintainability, allowing for efficient development and robust performance.
## Setup ## Setup
@@ -28,12 +28,12 @@ object NativeImage {
external fun convert1010102(bitmap: Bitmap, outInfo: IntArray): Long external fun convert1010102(bitmap: Bitmap, outInfo: IntArray): Long
/** /**
* Like [convert1010102], but decodes the JPEG XL image of [length] bytes at [address], stopped by * Like [convert1010102], but decodes the image of [length] bytes at [address], stopped by
* [cancel], downscaled to cover [targetWidth] x [targetHeight] unless either is 0 and to fit * [cancel], downscaled to cover [targetWidth] x [targetHeight] unless either is 0 and to fit
* within [maxPixelSize]. * within [maxPixelSize].
*/ */
@JvmStatic @JvmStatic
external fun decodeJxl( external fun decode(
address: Long, address: Long,
length: Int, length: Int,
targetWidth: Int, targetWidth: Int,
@@ -43,7 +43,7 @@ object NativeImage {
outInfo: IntArray outInfo: IntArray
): Long ): Long
/** Stops the [decodeJxl] given this one-byte direct buffer. */ /** Stops the [decode] given this one-byte direct buffer. */
@JvmStatic @JvmStatic
external fun cancel(cancelled: ByteBuffer) external fun cancel(cancelled: ByteBuffer)
} }
@@ -106,7 +106,7 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
// The fetch is done, so cancelling now stops the decode instead. // The fetch is done, so cancelling now stops the decode instead.
val cancelled = ByteBuffer.allocateDirect(1) val cancelled = ByteBuffer.allocateDirect(1)
signal.setOnCancelListener { NativeImage.cancel(cancelled) } signal.setOnCancelListener { NativeImage.cancel(cancelled) }
val pointer = if (signal.isCanceled) 0L else NativeImage.decodeJxl( val pointer = if (signal.isCanceled) 0L else NativeImage.decode(
buffer.pointer, buffer.pointer,
buffer.offset, buffer.offset,
width?.toInt() ?: 0, width?.toInt() ?: 0,
+4 -4
View File
@@ -9,7 +9,7 @@ enum NativeCore {
} }
private static let coreThumbhash = symbol("native_core_thumbhash", as: NativeCoreThumbhashFn.self) private static let coreThumbhash = symbol("native_core_thumbhash", as: NativeCoreThumbhashFn.self)
private static let coreJxlDecode = symbol("native_core_jxl_decode", as: NativeCoreJxlDecodeFn.self) private static let coreDecodeImage = symbol("native_core_decode_image", as: NativeCoreDecodeImageFn.self)
private static let coreCancel = symbol("native_core_cancel", as: NativeCoreCancelFn.self) private static let coreCancel = symbol("native_core_cancel", as: NativeCoreCancelFn.self)
static func thumbhash(_ hash: Data) -> (width: Int, height: Int, pointer: UnsafeMutableRawPointer)? { static func thumbhash(_ hash: Data) -> (width: Int, height: Int, pointer: UnsafeMutableRawPointer)? {
@@ -23,11 +23,11 @@ enum NativeCore {
} }
} }
static func decodeJxl(_ data: Data, targetWidth: Int64?, targetHeight: Int64?, maxPixelSize: Int, cancelled: UnsafeMutablePointer<Bool>) -> (width: Int, height: Int, pointer: UnsafeMutableRawPointer)? { static func decodeImage(_ data: Data, targetWidth: Int64?, targetHeight: Int64?, maxPixelSize: Int, cancelled: UnsafeMutablePointer<Bool>) -> (width: Int, height: Int, pointer: UnsafeMutableRawPointer)? {
guard let coreJxlDecode else { return nil } guard let coreDecodeImage else { return nil }
var info = NativeCoreImageInfo() var info = NativeCoreImageInfo()
return data.withUnsafeBytes { bytes in return data.withUnsafeBytes { bytes in
guard let pointer = coreJxlDecode(bytes.bindMemory(to: UInt8.self).baseAddress, bytes.count, false, UInt32(clamping: targetWidth ?? 0), UInt32(clamping: targetHeight ?? 0), UInt32(clamping: maxPixelSize), cancelled, &info) guard let pointer = coreDecodeImage(bytes.bindMemory(to: UInt8.self).baseAddress, bytes.count, false, UInt32(clamping: targetWidth ?? 0), UInt32(clamping: targetHeight ?? 0), UInt32(clamping: maxPixelSize), cancelled, &info)
else { return nil } else { return nil }
return (Int(info.width), Int(info.height), UnsafeMutableRawPointer(pointer)) return (Int(info.width), Int(info.height), UnsafeMutableRawPointer(pointer))
} }
@@ -80,7 +80,7 @@ class RemoteImageApiImpl: NSObject, RemoteImageApi {
return request.completion(ImageProcessing.cancelledResult) return request.completion(ImageProcessing.cancelledResult)
} }
guard let (width, height, pointer) = NativeCore.decodeJxl(data, targetWidth: width, targetHeight: height, maxPixelSize: ImageProcessing.maxPixelSize, cancelled: request.decodeCancelled) else { guard let (width, height, pointer) = NativeCore.decodeImage(data, targetWidth: width, targetHeight: height, maxPixelSize: ImageProcessing.maxPixelSize, cancelled: request.decodeCancelled) else {
registry.remove(requestId: request.id) registry.remove(requestId: request.id)
if request.isCancelled { if request.isCancelled {
return request.completion(ImageProcessing.cancelledResult) return request.completion(ImageProcessing.cancelledResult)
+1 -1
View File
@@ -3,5 +3,5 @@
// The function pointer type comes from the declaration, so nothing is retyped here. // The function pointer type comes from the declaration, so nothing is retyped here.
typedef __typeof__(&native_core_thumbhash) NativeCoreThumbhashFn; typedef __typeof__(&native_core_thumbhash) NativeCoreThumbhashFn;
typedef __typeof__(&native_core_jxl_decode) NativeCoreJxlDecodeFn; typedef __typeof__(&native_core_decode_image) NativeCoreDecodeImageFn;
typedef __typeof__(&native_core_cancel) NativeCoreCancelFn; typedef __typeof__(&native_core_cancel) NativeCoreCancelFn;
@@ -60,7 +60,6 @@ class AssetService {
} }
final stack = await _remoteRepository.getStackChildren(asset); final stack = await _remoteRepository.getStackChildren(asset);
// Include the primary asset in the stack as the first item
return [asset, ...stack]; return [asset, ...stack];
} }
@@ -74,8 +74,8 @@ class TimelineFactory {
TimelineService place(String place) => TimelineService(_timelineRepository.place(place, groupBy)); TimelineService place(String place) => TimelineService(_timelineRepository.place(place, groupBy));
TimelineService person(List<String> userIds, String personId) => TimelineService person(String userId, String personId) =>
TimelineService(_timelineRepository.person(userIds, personId, groupBy)); TimelineService(_timelineRepository.person(userId, personId, groupBy));
TimelineService fromAssets(List<BaseAsset> assets, TimelineOrigin type) => TimelineService fromAssets(List<BaseAsset> assets, TimelineOrigin type) =>
TimelineService(_timelineRepository.fromAssets(assets, type)); TimelineService(_timelineRepository.fromAssets(assets, type));
@@ -76,7 +76,7 @@ class RemoteAssetRepository extends DatabaseAccessor<Drift> with $RemoteAssetRep
final query = _db.remoteAssetEntity.select() final query = _db.remoteAssetEntity.select()
..where((row) => row.stackId.equals(stackId) & row.id.equals(asset.id).not()) ..where((row) => row.stackId.equals(stackId) & row.id.equals(asset.id).not())
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]); ..orderBy([(row) => OrderingTerm.asc(row.createdAt)]);
return query.map((row) => row.toDto()).get(); return query.map((row) => row.toDto()).get();
} }
@@ -923,7 +923,7 @@ class SyncStreamRepository extends DatabaseAccessor<Drift> with $SyncStreamRepos
try { try {
await _db.batch((batch) { await _db.batch((batch) {
for (final assetOcr in data) { for (final assetOcr in data) {
batch.deleteWhere(_db.assetOcrEntity, (row) => row.id.equals(assetOcr.id)); batch.deleteWhere(_db.assetOcrEntity, (row) => row.assetId.equals(assetOcr.assetId));
} }
}); });
} catch (error, stack) { } catch (error, stack) {
@@ -366,10 +366,10 @@ class TimelineRepository extends DatabaseAccessor<Drift> with $TimelineRepositor
origin: TimelineOrigin.place, origin: TimelineOrigin.place,
); );
TimelineQuery person(List<String> userIds, String personId, GroupAssetsBy groupBy) => ( TimelineQuery person(String userId, String personId, GroupAssetsBy groupBy) => (
bucketSource: () => _watchPersonBucket(userIds, personId, groupBy: groupBy), bucketSource: () => _watchPersonBucket(userId, personId, groupBy: groupBy),
assetSource: (offset, count) => assetSource: (offset, count) =>
_getPersonBucketAssets(userIds, personId, groupBy: groupBy, offset: offset, count: count), _getPersonBucketAssets(userId, personId, groupBy: groupBy, offset: offset, count: count),
origin: TimelineOrigin.person, origin: TimelineOrigin.person,
); );
@@ -430,7 +430,7 @@ class TimelineRepository extends DatabaseAccessor<Drift> with $TimelineRepositor
return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get(); return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get();
} }
Expression<bool> _personAssetFilter(List<String> userIds, String personId) { Expression<bool> _personAssetFilter(String userId, String personId) {
final faceAssetIds = _db.assetFaceEntity.selectOnly() final faceAssetIds = _db.assetFaceEntity.selectOnly()
..addColumns([_db.assetFaceEntity.assetId]) ..addColumns([_db.assetFaceEntity.assetId])
..where( ..where(
@@ -438,22 +438,23 @@ class TimelineRepository extends DatabaseAccessor<Drift> with $TimelineRepositor
_db.assetFaceEntity.isVisible.equals(true) & _db.assetFaceEntity.isVisible.equals(true) &
_db.assetFaceEntity.deletedAt.isNull(), _db.assetFaceEntity.deletedAt.isNull(),
); );
final partnerIds = _db.partnerEntity.selectOnly()
..addColumns([_db.partnerEntity.sharedById])
..where(_db.partnerEntity.sharedWithId.equals(userId));
final albumAssetIds = _db.remoteAlbumAssetEntity.selectOnly()..addColumns([_db.remoteAlbumAssetEntity.assetId]); final albumAssetIds = _db.remoteAlbumAssetEntity.selectOnly()..addColumns([_db.remoteAlbumAssetEntity.assetId]);
return _db.remoteAssetEntity.id.isInQuery(faceAssetIds) & return _db.remoteAssetEntity.id.isInQuery(faceAssetIds) &
(_db.remoteAssetEntity.ownerId.isIn(userIds) | _db.remoteAssetEntity.id.isInQuery(albumAssetIds)) & (_db.remoteAssetEntity.ownerId.equals(userId) |
_db.remoteAssetEntity.ownerId.isInQuery(partnerIds) |
_db.remoteAssetEntity.id.isInQuery(albumAssetIds)) &
_db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAssetEntity.deletedAt.isNull() &
_db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline); _db.remoteAssetEntity.visibility.equalsValue(AssetVisibility.timeline);
} }
Stream<List<Bucket>> _watchPersonBucket( Stream<List<Bucket>> _watchPersonBucket(String userId, String personId, {GroupAssetsBy groupBy = GroupAssetsBy.day}) {
List<String> userIds,
String personId, {
GroupAssetsBy groupBy = GroupAssetsBy.day,
}) {
if (groupBy == GroupAssetsBy.none) { if (groupBy == GroupAssetsBy.none) {
final query = _db.remoteAssetEntity.selectOnly() final query = _db.remoteAssetEntity.selectOnly()
..addColumns([_db.remoteAssetEntity.id.count()]) ..addColumns([_db.remoteAssetEntity.id.count()])
..where(_personAssetFilter(userIds, personId)); ..where(_personAssetFilter(userId, personId));
return query.map((row) { return query.map((row) {
final count = row.read(_db.remoteAssetEntity.id.count())!; final count = row.read(_db.remoteAssetEntity.id.count())!;
@@ -466,7 +467,7 @@ class TimelineRepository extends DatabaseAccessor<Drift> with $TimelineRepositor
final query = _db.remoteAssetEntity.selectOnly() final query = _db.remoteAssetEntity.selectOnly()
..addColumns([assetCountExp, dateExp]) ..addColumns([assetCountExp, dateExp])
..where(_personAssetFilter(userIds, personId)) ..where(_personAssetFilter(userId, personId))
..groupBy([dateExp]) ..groupBy([dateExp])
..orderBy([OrderingTerm.desc(dateExp)]); ..orderBy([OrderingTerm.desc(dateExp)]);
@@ -478,14 +479,14 @@ class TimelineRepository extends DatabaseAccessor<Drift> with $TimelineRepositor
} }
Future<List<BaseAsset>> _getPersonBucketAssets( Future<List<BaseAsset>> _getPersonBucketAssets(
List<String> userIds, String userId,
String personId, { String personId, {
required int offset, required int offset,
required int count, required int count,
GroupAssetsBy groupBy = GroupAssetsBy.day, GroupAssetsBy groupBy = GroupAssetsBy.day,
}) { }) {
final query = _db.remoteAssetEntity.select() final query = _db.remoteAssetEntity.select()
..where((_) => _personAssetFilter(userIds, personId)) ..where((_) => _personAssetFilter(userId, personId))
..orderBy(_assetDateOrder(groupBy)) ..orderBy(_assetDateOrder(groupBy))
..limit(count, offset: offset); ..limit(count, offset: offset);
@@ -82,8 +82,7 @@ class _PersonPageState extends ConsumerState<PersonPage> {
throw Exception('User must be logged in to view person timeline'); throw Exception('User must be logged in to view person timeline');
} }
final users = ref.watch(timelineUsersProvider).valueOrNull ?? [user.id]; final timelineService = ref.watch(timelineFactoryProvider).person(user.id, _person.id);
final timelineService = ref.watch(timelineFactoryProvider).person(users, _person.id);
ref.onDispose(timelineService.dispose); ref.onDispose(timelineService.dispose);
return timelineService; return timelineService;
}), }),
@@ -42,12 +42,16 @@ class SearchPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final serverFeatures = ref.watch(serverInfoProvider.select((v) => v.serverFeatures)); final serverFeatures = ref.watch(serverInfoProvider.select((v) => v.serverFeatures));
final textSearchType = useState<TextSearchType>( // Stays null until the user picks a type, so the default follows the server features once they are loaded
serverFeatures.smartSearch ? TextSearchType.context : TextSearchType.filename, final selectedTextSearchType = useState<TextSearchType?>(null);
); final textSearchType =
final searchHintText = useState<String>( selectedTextSearchType.value ?? (serverFeatures.smartSearch ? TextSearchType.context : TextSearchType.filename);
serverFeatures.smartSearch ? context.t.sunrise_on_the_beach : context.t.file_name_or_extension, final searchHintText = switch (textSearchType) {
); TextSearchType.context => context.t.sunrise_on_the_beach,
TextSearchType.filename => context.t.file_name_or_extension,
TextSearchType.description => context.t.search_by_description_example,
TextSearchType.ocr => context.t.search_by_ocr_example,
};
final textSearchController = useTextEditingController(); final textSearchController = useTextEditingController();
final filter = useState<SearchFilter>( final filter = useState<SearchFilter>(
SearchFilter( SearchFilter(
@@ -491,14 +495,14 @@ class SearchPage extends HookConsumerWidget {
); );
} }
void handleTextSubmitted(String value) => search(switch (textSearchType.value) { void handleTextSubmitted(String value) => search(switch (textSearchType) {
TextSearchType.context => filter.value.copyWith(filename: '', context: value, description: '', ocr: ''), TextSearchType.context => filter.value.copyWith(filename: '', context: value, description: '', ocr: ''),
TextSearchType.filename => filter.value.copyWith(filename: value, context: '', description: '', ocr: ''), TextSearchType.filename => filter.value.copyWith(filename: value, context: '', description: '', ocr: ''),
TextSearchType.description => filter.value.copyWith(filename: '', context: '', description: value, ocr: ''), TextSearchType.description => filter.value.copyWith(filename: '', context: '', description: value, ocr: ''),
TextSearchType.ocr => filter.value.copyWith(filename: '', context: '', description: '', ocr: value), TextSearchType.ocr => filter.value.copyWith(filename: '', context: '', description: '', ocr: value),
}); });
IconData getSearchPrefixIcon() => switch (textSearchType.value) { IconData getSearchPrefixIcon() => switch (textSearchType) {
TextSearchType.context => Icons.image_search_rounded, TextSearchType.context => Icons.image_search_rounded,
TextSearchType.filename => Icons.abc_rounded, TextSearchType.filename => Icons.abc_rounded,
TextSearchType.description => Icons.text_snippet_outlined, TextSearchType.description => Icons.text_snippet_outlined,
@@ -543,15 +547,14 @@ class SearchPage extends HookConsumerWidget {
context.t.search_by_context, context.t.search_by_context,
style: context.textTheme.bodyLarge?.copyWith( style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: textSearchType.value == TextSearchType.context ? context.colorScheme.primary : null, color: textSearchType == TextSearchType.context ? context.colorScheme.primary : null,
), ),
), ),
selectedColor: context.colorScheme.primary, selectedColor: context.colorScheme.primary,
selected: textSearchType.value == TextSearchType.context, selected: textSearchType == TextSearchType.context,
), ),
onPressed: () { onPressed: () {
textSearchType.value = TextSearchType.context; selectedTextSearchType.value = TextSearchType.context;
searchHintText.value = context.t.sunrise_on_the_beach;
}, },
), ),
), ),
@@ -562,15 +565,14 @@ class SearchPage extends HookConsumerWidget {
context.t.search_filter_filename, context.t.search_filter_filename,
style: context.textTheme.bodyLarge?.copyWith( style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: textSearchType.value == TextSearchType.filename ? context.colorScheme.primary : null, color: textSearchType == TextSearchType.filename ? context.colorScheme.primary : null,
), ),
), ),
selectedColor: context.colorScheme.primary, selectedColor: context.colorScheme.primary,
selected: textSearchType.value == TextSearchType.filename, selected: textSearchType == TextSearchType.filename,
), ),
onPressed: () { onPressed: () {
textSearchType.value = TextSearchType.filename; selectedTextSearchType.value = TextSearchType.filename;
searchHintText.value = context.t.file_name_or_extension;
}, },
), ),
MenuItemButton( MenuItemButton(
@@ -580,15 +582,14 @@ class SearchPage extends HookConsumerWidget {
context.t.search_by_description, context.t.search_by_description,
style: context.textTheme.bodyLarge?.copyWith( style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: textSearchType.value == TextSearchType.description ? context.colorScheme.primary : null, color: textSearchType == TextSearchType.description ? context.colorScheme.primary : null,
), ),
), ),
selectedColor: context.colorScheme.primary, selectedColor: context.colorScheme.primary,
selected: textSearchType.value == TextSearchType.description, selected: textSearchType == TextSearchType.description,
), ),
onPressed: () { onPressed: () {
textSearchType.value = TextSearchType.description; selectedTextSearchType.value = TextSearchType.description;
searchHintText.value = context.t.search_by_description_example;
}, },
), ),
FeatureCheck( FeatureCheck(
@@ -600,15 +601,14 @@ class SearchPage extends HookConsumerWidget {
context.t.search_by_ocr, context.t.search_by_ocr,
style: context.textTheme.bodyLarge?.copyWith( style: context.textTheme.bodyLarge?.copyWith(
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: textSearchType.value == TextSearchType.ocr ? context.colorScheme.primary : null, color: textSearchType == TextSearchType.ocr ? context.colorScheme.primary : null,
), ),
), ),
selectedColor: context.colorScheme.primary, selectedColor: context.colorScheme.primary,
selected: textSearchType.value == TextSearchType.ocr, selected: textSearchType == TextSearchType.ocr,
), ),
onPressed: () { onPressed: () {
textSearchType.value = TextSearchType.ocr; selectedTextSearchType.value = TextSearchType.ocr;
searchHintText.value = context.t.search_by_ocr_example;
}, },
), ),
), ),
@@ -631,7 +631,7 @@ class SearchPage extends HookConsumerWidget {
), ),
), ),
child: SearchField( child: SearchField(
hintText: searchHintText.value, hintText: searchHintText,
key: const Key('search_text_field'), key: const Key('search_text_field'),
controller: textSearchController, controller: textSearchController,
contentPadding: filter.value.assetId != null ? const EdgeInsets.only(left: 24) : const EdgeInsets.all(8), contentPadding: filter.value.assetId != null ? const EdgeInsets.only(left: 24) : const EdgeInsets.all(8),
@@ -8,6 +8,7 @@ import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/utils/bytes_units.dart'; import 'package:immich_mobile/utils/bytes_units.dart';
import 'package:immich_mobile/utils/camera.utils.dart';
const _kSeparator = ' • '; const _kSeparator = ' • ';
@@ -114,12 +115,7 @@ class TechnicalDetails extends ConsumerWidget {
if (exifInfo == null) { if (exifInfo == null) {
return null; return null;
} }
return switch ((exifInfo.make, exifInfo.model)) { return getCameraName(exifInfo.make, exifInfo.model);
(null, null) => null,
(final String make, null) => make,
(null, final String model) => model,
(final String make, final String model) => '$make $model',
};
} }
static String? _getCameraInfoSubtitle(ExifInfo? exifInfo) { static String? _getCameraInfoSubtitle(ExifInfo? exifInfo) {
+38
View File
@@ -0,0 +1,38 @@
final _whitespace = RegExp(r'\s+');
final _wordChar = RegExp(r'\w');
/// Builds the display name of a camera from its EXIF make and model.
///
/// Many cameras (e.g. Canon) already include the manufacturer in the model
/// (`Canon EOS R6m2`), so prefixing the make again would produce
/// "Canon Canon EOS R6m2". The make is only prepended when the model does not
/// already start with it. Makes made up of several words are matched word by
/// word, since the model usually repeats only part of them:
/// `NIKON CORPORATION` / `NIKON D850` and
/// `EASTMAN KODAK COMPANY` / `KODAK EASYSHARE C875`.
///
/// Returns `null` when neither a make nor a model is available.
String? getCameraName(String? make, String? model) {
final trimmedMake = make?.trim() ?? '';
final trimmedModel = model?.trim() ?? '';
if (trimmedMake.isEmpty && trimmedModel.isEmpty) {
return null;
}
if (trimmedMake.isEmpty) {
return trimmedModel;
}
if (trimmedModel.isEmpty) {
return trimmedMake;
}
final lowerModel = trimmedModel.toLowerCase();
final modelIncludesMake = trimmedMake.toLowerCase().split(_whitespace).any((word) {
final charAfterWord = lowerModel.length > word.length ? lowerModel[word.length] : '';
return lowerModel.startsWith(word) && !_wordChar.hasMatch(charAfterWord);
});
return modelIncludesMake ? trimmedModel : '$trimmedMake $trimmedModel';
}
+1 -1
View File
@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none' publish_to: 'none'
version: 3.3.5-local.11+3030503 version: 3.3.5-local.14+3030506
environment: environment:
sdk: '>=3.12.0 <4.0.0' sdk: '>=3.12.0 <4.0.0'
@@ -363,4 +363,65 @@ void main() {
expect(rows.map((r) => r.id).toSet(), {last.id, lastLib.id}); expect(rows.map((r) => r.id).toSet(), {last.id, lastLib.id});
}); });
}); });
group('SyncStreamRepository - deleteAssetOcrV1 (#32337)', () {
test('removes OCR rows for deleted assets', () async {
await sut.updateUsersV1([_createUser()]);
final known1 = _createAsset(
id: 'known1',
checksum: 'AAA',
fileName: 'a.jpg',
);
final known2 = _createAsset(
id: 'known2',
checksum: 'BBB',
fileName: 'b.jpg',
);
await sut.updateAssetsV1([known1, known2]);
await sut.updateAssetOcrV1([
SyncAssetOcrV1(
assetId: known1.id,
boxScore: 0.9,
id: 'ocr-1',
isVisible: true,
text: 'this text',
textScore: 0.9,
x1: 1,
x2: 2,
x3: 3,
x4: 4,
y1: 1,
y2: 2,
y3: 3,
y4: 4,
),
SyncAssetOcrV1(
assetId: known2.id,
boxScore: 0.8,
id: 'ocr-2',
isVisible: false,
text: 'this text again',
textScore: 0.8,
x1: 1,
x2: 2,
x3: 3,
x4: 4,
y1: 1,
y2: 2,
y3: 3,
y4: 4,
),
]);
await sut.deleteAssetOcrV1([
SyncAssetOcrDeleteV1(id: 'log-id', assetId: known1.id, deletedAt: DateTime(2024, 1, 5)),
]);
final removedRows = await (db.assetOcrEntity.select()..where((t) => t.assetId.equals(known1.id))).get();
expect(removedRows, isEmpty);
final keepRows = await (db.assetOcrEntity.select()..where((t) => t.assetId.equals(known2.id))).get();
expect(keepRows, hasLength(1));
});
});
} }
@@ -18,6 +18,37 @@ void main() {
await ctx.dispose(); await ctx.dispose();
}); });
group('getStackChildren', () {
test('returns stack children from oldest to newest', () async {
final user = await ctx.newUser();
const stackId = 'stack';
final primary = await ctx.newRemoteAsset(
id: 'primary',
ownerId: user.id,
stackId: stackId,
createdAt: DateTime(2026, 1, 2),
);
final beforePrimary = await ctx.newRemoteAsset(
id: 'before-primary',
ownerId: user.id,
stackId: stackId,
createdAt: DateTime(2026, 1, 1),
);
final afterPrimary = await ctx.newRemoteAsset(
id: 'after-primary',
ownerId: user.id,
stackId: stackId,
createdAt: DateTime(2026, 1, 3),
);
await ctx.newRemoteAsset(ownerId: user.id, stackId: 'other-stack');
final primaryAsset = await sut.get(primary.id);
final stack = await sut.getStackChildren(primaryAsset!);
expect(stack.map((asset) => asset.id), [beforePrimary.id, afterPrimary.id]);
});
});
group('getByChecksum', () { group('getByChecksum', () {
late String userId; late String userId;
@@ -103,6 +103,22 @@ void main() {
}); });
group('person assets', () { group('person assets', () {
test('includes partner assets even when the partner is not shown in the timeline', () async {
final user = await ctx.newUser();
final partner = await ctx.newUser();
final stranger = await ctx.newUser();
await ctx.newPartner(sharedById: partner.id, sharedWithId: user.id, inTimeline: false);
final person = await ctx.newPerson(ownerId: user.id);
final partnerAsset = await ctx.newRemoteAsset(ownerId: partner.id);
final strangerAsset = await ctx.newRemoteAsset(ownerId: stranger.id);
await ctx.newFace(assetId: partnerAsset.id, personId: person.id);
await ctx.newFace(assetId: strangerAsset.id, personId: person.id);
final assets = await sut.person(user.id, person.id, .day).assetSource(0, 10);
expect(assets.map((a) => (a as RemoteAsset).id), [partnerAsset.id]);
});
test('does not duplicate an asset that has multiple face records for the same person', () async { test('does not duplicate an asset that has multiple face records for the same person', () async {
// Regression check for #26723: an INNER JOIN between remote_asset_entity and asset_face_entity // Regression check for #26723: an INNER JOIN between remote_asset_entity and asset_face_entity
// fanned out one asset into N rows when N face records pointed at the same (asset, person) pair // fanned out one asset into N rows when N face records pointed at the same (asset, person) pair
@@ -113,7 +129,7 @@ void main() {
await ctx.newFace(assetId: asset.id, personId: person.id); await ctx.newFace(assetId: asset.id, personId: person.id);
await ctx.newFace(assetId: asset.id, personId: person.id); await ctx.newFace(assetId: asset.id, personId: person.id);
final query = sut.person([user.id], person.id, .day); final query = sut.person(user.id, person.id, .day);
final buckets = await query.bucketSource().first; final buckets = await query.bucketSource().first;
expect(buckets, hasLength(1)); expect(buckets, hasLength(1));
@@ -140,7 +156,7 @@ void main() {
await ctx.newFace(assetId: shiftedLater.id, personId: person.id); await ctx.newFace(assetId: shiftedLater.id, personId: person.id);
await ctx.newFace(assetId: shiftedEarlier.id, personId: person.id); await ctx.newFace(assetId: shiftedEarlier.id, personId: person.id);
final query = sut.person([user.id], person.id, .day); final query = sut.person(user.id, person.id, .day);
final buckets = await query.bucketSource().first; final buckets = await query.bucketSource().first;
expect(buckets, hasLength(2)); expect(buckets, hasLength(2));
@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/presentation/pages/search/search.page.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import '../../unit/presentation/presentation_context.dart';
/// Server info whose features arrive after the page is built, like on a slow or late connection
class DelayedServerInfoNotifier extends ServerInfoNotifier {
DelayedServerInfoNotifier(super.serverInfoService);
void enableSmartSearch() => state = state.copyWith(serverFeatures: state.serverFeatures.copyWith(smartSearch: true));
}
void main() {
late PresentationContext context;
setUp(() async => context = await PresentationContext.create());
tearDown(() async => context.dispose());
final contextSearchIcon = find.byIcon(Icons.image_search_rounded);
final filenameSearchIcon = find.byIcon(Icons.abc_rounded);
Future<DelayedServerInfoNotifier> pumpSearchPage(WidgetTester tester) async {
final serverInfo = DelayedServerInfoNotifier(context.service.serverInfo);
await tester.pumpTestWidget(
context,
const SearchPage(),
overrides: [serverInfoProvider.overrideWith((ref) => serverInfo)],
);
return serverInfo;
}
testWidgets('uses context search once smart search is reported by the server', (tester) async {
final serverInfo = await pumpSearchPage(tester);
expect(filenameSearchIcon, findsOneWidget);
serverInfo.enableSmartSearch();
await tester.pumpAndSettle();
expect(contextSearchIcon, findsOneWidget);
expect(filenameSearchIcon, findsNothing);
});
testWidgets('keeps the search type selected by the user', (tester) async {
final serverInfo = await pumpSearchPage(tester);
await tester.tap(find.byIcon(Icons.more_vert_rounded));
await tester.pumpAndSettle();
await tester.tap(find.byIcon(Icons.text_snippet_outlined));
await tester.pumpAndSettle();
serverInfo.enableSmartSearch();
await tester.pumpAndSettle();
expect(find.byIcon(Icons.text_snippet_outlined), findsOneWidget);
expect(contextSearchIcon, findsNothing);
});
}
+47
View File
@@ -0,0 +1,47 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/utils/camera.utils.dart';
void main() {
group('getCameraName', () {
test('does not repeat the make when the model already starts with it', () {
expect(getCameraName('Canon', 'Canon EOS R6m2'), 'Canon EOS R6m2');
expect(getCameraName('Canon', 'Canon EOS 5D Mark IV'), 'Canon EOS 5D Mark IV');
});
test('compares the make and model case-insensitively', () {
expect(getCameraName('NIKON', 'Nikon Z 6'), 'Nikon Z 6');
expect(getCameraName('canon', 'CANON EOS R5'), 'CANON EOS R5');
});
test('compares against the first word of a multi-word make', () {
expect(getCameraName('NIKON CORPORATION', 'NIKON D850'), 'NIKON D850');
expect(getCameraName('OLYMPUS IMAGING CORP.', 'OLYMPUS E-M1'), 'OLYMPUS E-M1');
});
test('matches any word of a multi-word make', () {
expect(getCameraName('EASTMAN KODAK COMPANY', 'KODAK EASYSHARE C875'), 'KODAK EASYSHARE C875');
expect(getCameraName('EASTMAN KODAK COMPANY', 'EASTMAN KODAK 1'), 'EASTMAN KODAK 1');
});
test('prepends the make when the model does not include it', () {
expect(getCameraName('SONY', 'ILCE-7M4'), 'SONY ILCE-7M4');
expect(getCameraName('FUJIFILM', 'X-T5'), 'FUJIFILM X-T5');
expect(getCameraName('Apple', 'iPhone 15 Pro'), 'Apple iPhone 15 Pro');
});
test('only treats the make as a prefix on a word boundary', () {
expect(getCameraName('Canon', 'Canonical X1'), 'Canon Canonical X1');
});
test('handles a missing make or model', () {
expect(getCameraName('Canon', null), 'Canon');
expect(getCameraName(null, 'EOS R6m2'), 'EOS R6m2');
expect(getCameraName(null, null), isNull);
expect(getCameraName('', ''), isNull);
});
test('ignores surrounding whitespace', () {
expect(getCameraName(' Canon ', ' Canon EOS R6m2 '), 'Canon EOS R6m2');
});
});
}
+15 -8
View File
@@ -56,6 +56,18 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "codecs"
version = "0.1.0"
dependencies = [
"jxl",
"libc",
"moxcms",
"pic-scale",
"quick_cache",
"rayon",
]
[[package]] [[package]]
name = "combine" name = "combine"
version = "4.6.8" version = "4.6.8"
@@ -251,9 +263,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.190" version = "0.2.191"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
@@ -287,12 +299,7 @@ dependencies = [
name = "native_core" name = "native_core"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"jxl", "codecs",
"libc",
"moxcms",
"pic-scale",
"quick_cache",
"rayon",
] ]
[[package]] [[package]]
+1 -1
View File
@@ -1,6 +1,6 @@
[workspace] [workspace]
resolver = "3" resolver = "3"
members = ["crates/native_core", "crates/native_core_ffi"] members = ["crates/codecs", "crates/native_core", "crates/native_core_ffi"]
[workspace.package] [workspace.package]
version = "0.1.0" version = "0.1.0"
+3 -2
View File
@@ -5,8 +5,9 @@ Rust code that ships inside the mobile app. Flutter compiles it from source thro
in `mobile/` sets one up, otherwise install [rustup](https://rustup.rs) once and the build in `mobile/` sets one up, otherwise install [rustup](https://rustup.rs) once and the build
fetches the pinned toolchain itself. fetches the pinned toolchain itself.
- `crates/native_core`: the shared logic - `crates/codecs`: the image decoders, a feature each, behind one `decode`
- `crates/native_core_ffi`: the C ABI, cbindgen writes `include/native_core.h` - `crates/native_core`: the shared logic, a feature per part and a group per target
- `crates/native_core_ffi`: the C ABI on the `flutter` group, cbindgen writes `include/native_core.h`
- `native_core`: Flutter package with the build hook and the ffigen bindings - `native_core`: Flutter package with the build hook and the ffigen bindings
(`NOTICES` carries the licenses of the crates compiled into the core, update it with the dependencies) (`NOTICES` carries the licenses of the crates compiled into the core, update it with the dependencies)
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "codecs"
version.workspace = true
edition.workspace = true
license.workspace = true
[features]
default = ["jxl"]
jxl = ["dep:jxl", "dep:rayon"]
[dependencies]
jxl = { workspace = true, optional = true }
libc = { workspace = true }
moxcms = { workspace = true }
pic-scale = { workspace = true }
quick_cache = { workspace = true }
rayon = { workspace = true, optional = true }
[lints]
workspace = true
@@ -70,7 +70,7 @@ impl JxlParallelRunner for Cancellable<'_> {
} }
impl<'a> Jxl<'a> { impl<'a> Jxl<'a> {
pub fn open(mut input: &'a [u8], target: Target) -> Option<Self> { pub fn open(mut input: &'a [u8], color_space: Target) -> Option<Self> {
let mut options = JxlDecoderOptions::default(); let mut options = JxlDecoderOptions::default();
// Colour transforms need straight alpha; premultiplying happens last. // Colour transforms need straight alpha; premultiplying happens last.
options.alpha_mode = AlphaMode::Unpremultiplied; options.alpha_mode = AlphaMode::Unpremultiplied;
@@ -85,7 +85,7 @@ impl<'a> Jxl<'a> {
.ok()?; .ok()?;
let profile = decoder.output_color_profile()?; let profile = decoder.output_color_profile()?;
if let Some(cicp) = cicp(profile) if let Some(cicp) = cicp(profile)
&& (target == Target::Source || color::is_srgb(cicp)) && (color_space == Target::Source || color::is_srgb(cicp))
{ {
if color::is_hdr(cicp) { if color::is_hdr(cicp) {
decoder decoder
@@ -121,11 +121,11 @@ impl<'a> Jxl<'a> {
}) })
} }
/// Decodes the first frame unless cancelled, downscaled to cover `target` pixels and to fit /// Decodes the first frame unless cancelled, downscaled to cover `target_size` pixels and to
/// within `max_pixel_size`. /// fit within `max_pixel_size`.
pub fn decode( pub fn decode(
mut self, mut self,
target: Option<(usize, usize)>, target_size: Option<(usize, usize)>,
max_pixel_size: usize, max_pixel_size: usize,
cancelled: &AtomicBool, cancelled: &AtomicBool,
) -> Option<Image> { ) -> Option<Image> {
@@ -139,7 +139,7 @@ impl<'a> Jxl<'a> {
let pixel_bytes = let pixel_bytes =
format.color_type.samples_per_pixel() * format.color_data_format?.bytes_per_sample(); format.color_type.samples_per_pixel() * format.color_data_format?.bytes_per_sample();
let stride = w.checked_mul(pixel_bytes)?; let stride = w.checked_mul(pixel_bytes)?;
let size = resize::cover((w, h), target, max_pixel_size); let size = resize::cover((w, h), target_size, max_pixel_size);
let samples = if color::is_hdr(self.cicp) { let samples = if color::is_hdr(self.cicp) {
let mut rgba = Pixels::zeroed(w, h, pixel_bytes)?; let mut rgba = Pixels::zeroed(w, h, pixel_bytes)?;
self.decode_frame(&mut rgba, stride, cancelled)?; self.decode_frame(&mut rgba, stride, cancelled)?;
+49
View File
@@ -0,0 +1,49 @@
use std::sync::atomic::AtomicBool;
use color::Target;
use image::Image;
pub mod color;
pub mod image;
#[cfg(feature = "jxl")]
pub mod jxl;
mod resize;
enum Format {
#[cfg(feature = "jxl")]
Jxl,
}
pub struct Options {
pub color_space: Target,
/// Downscales to the smallest size that still fills this.
pub target_size: Option<(usize, usize)>,
/// Downscales to fit within this many pixels per side.
pub max_pixel_size: usize,
}
fn probe(data: &[u8]) -> Option<Format> {
#[cfg(feature = "jxl")]
if matches!(
::jxl::api::check_signature(data),
::jxl::api::JxlSignature::Codestream | ::jxl::api::JxlSignature::Container
) {
return Some(Format::Jxl);
}
None
}
/// Decodes the first frame of `data` unless cancelled.
pub fn decode(data: &[u8], options: &Options, cancelled: &AtomicBool) -> Option<Image> {
match probe(data)? {
#[cfg(feature = "jxl")]
Format::Jxl => jxl::Jxl::open(data, options.color_space)?.decode(
options.target_size,
options.max_pixel_size,
cancelled,
),
}
}
@@ -47,16 +47,16 @@ impl Lanczos for u16 {
} }
} }
/// The size of `size` scaled down to cover `target` and to fit within `max_pixel_size`, if that /// The size of `size` scaled down to cover `target_size` and to fit within `max_pixel_size`, if
/// is smaller. /// that is smaller.
pub(crate) fn cover( pub(crate) fn cover(
(w, h): (usize, usize), (w, h): (usize, usize),
target: Option<(usize, usize)>, target_size: Option<(usize, usize)>,
max_pixel_size: usize, max_pixel_size: usize,
) -> Option<(usize, usize)> { ) -> Option<(usize, usize)> {
// Scales as side ratios, so the side that sets the scale lands exactly on its limit. // Scales as side ratios, so the side that sets the scale lands exactly on its limit.
let fit = (max_pixel_size, w.max(h)); let fit = (max_pixel_size, w.max(h));
let (num, den) = match target { let (num, den) = match target_size {
Some((tw, th)) => { Some((tw, th)) => {
let cover = if tw * h >= th * w { (tw, w) } else { (th, h) }; let cover = if tw * h >= th * w { (tw, w) } else { (th, h) };
if cover.0 * fit.1 < fit.0 * cover.1 { if cover.0 * fit.1 < fit.0 * cover.1 {
@@ -1,11 +1,19 @@
#![cfg(feature = "jxl")]
use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicBool;
use native_core::color::{CicpColorPrimaries, CicpProfile, Target, TransferCharacteristics}; use codecs::Options;
use native_core::image::{Image, Pixels, Samples}; use codecs::color::{CicpColorPrimaries, CicpProfile, Target, TransferCharacteristics};
use native_core::jxl::Jxl; use codecs::image::{Image, Pixels, Samples};
use codecs::jxl::Jxl;
fn decode(data: &[u8], target: Target) -> Option<Image> { fn decode(data: &[u8], color_space: Target) -> Option<Image> {
Jxl::open(data, target)?.decode(None, 16384, &AtomicBool::new(false)) let options = Options {
color_space,
target_size: None,
max_pixel_size: 16384,
};
codecs::decode(data, &options, &AtomicBool::new(false))
} }
fn decode_srgb(data: &[u8]) -> Option<Pixels<u8>> { fn decode_srgb(data: &[u8]) -> Option<Pixels<u8>> {
+9 -6
View File
@@ -4,13 +4,16 @@ version.workspace = true
edition.workspace = true edition.workspace = true
license.workspace = true license.workspace = true
[features]
convert = []
rotate = []
thumbhash = []
jxl = ["codecs/jxl"]
# One group per target.
flutter = ["convert", "rotate", "thumbhash", "jxl"]
[dependencies] [dependencies]
jxl = { workspace = true } codecs = { path = "../codecs", default-features = false, optional = true }
libc = { workspace = true }
moxcms = { workspace = true }
pic-scale = { workspace = true }
quick_cache = { workspace = true }
rayon = { workspace = true }
[lints] [lints]
workspace = true workspace = true
+5 -4
View File
@@ -1,9 +1,10 @@
pub mod color; #[cfg(feature = "codecs")]
pub use codecs;
#[cfg(feature = "convert")]
pub mod convert; pub mod convert;
pub mod image; #[cfg(feature = "rotate")]
pub mod jxl;
mod resize;
pub mod rotate; pub mod rotate;
#[cfg(feature = "thumbhash")]
pub mod thumbhash; pub mod thumbhash;
pub fn version() -> &'static str { pub fn version() -> &'static str {
@@ -1,3 +1,5 @@
#![cfg(feature = "convert")]
use native_core::convert::convert_1010102; use native_core::convert::convert_1010102;
#[test] #[test]
@@ -1,3 +1,5 @@
#![cfg(feature = "rotate")]
use native_core::rotate::{Orientation, rotate}; use native_core::rotate::{Orientation, rotate};
#[test] #[test]
@@ -1,3 +1,5 @@
#![cfg(feature = "thumbhash")]
use native_core::thumbhash::decode; use native_core::thumbhash::decode;
// Expected pixels come from the old Swift decoder patched to round like the Java did (it truncated), // Expected pixels come from the old Swift decoder patched to round like the Java did (it truncated),
+1 -1
View File
@@ -10,7 +10,7 @@ name = "native_core_ffi"
crate-type = ["cdylib", "staticlib"] crate-type = ["cdylib", "staticlib"]
[dependencies] [dependencies]
native_core = { path = "../native_core" } native_core = { path = "../native_core", features = ["flutter"] }
libc = { workspace = true } libc = { workspace = true }
[target.'cfg(target_os = "android")'.dependencies] [target.'cfg(target_os = "android")'.dependencies]
+1 -1
View File
@@ -3,7 +3,7 @@ use std::path::Path;
fn main() { fn main() {
// Files that define exported items. A directory here would make Flutter rerun the hook every build. // Files that define exported items. A directory here would make Flutter rerun the hook every build.
println!("cargo:rerun-if-changed=src/lib.rs"); println!("cargo:rerun-if-changed=src/lib.rs");
println!("cargo:rerun-if-changed=src/jxl.rs"); println!("cargo:rerun-if-changed=src/decode.rs");
println!("cargo:rerun-if-changed=src/thumbhash.rs"); println!("cargo:rerun-if-changed=src/thumbhash.rs");
println!("cargo:rerun-if-changed=cbindgen.toml"); println!("cargo:rerun-if-changed=cbindgen.toml");
let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
@@ -10,9 +10,9 @@ use jni::{
objects::{JByteBuffer, JClass, JIntArray}, objects::{JByteBuffer, JClass, JIntArray},
sys::{jint, jlong}, sys::{jint, jlong},
}; };
use native_core::color::Target; use native_core::codecs::color::Target;
use native_core::image::{Image, Samples}; use native_core::codecs::image::{Image, Samples};
use native_core::jxl::Jxl; use native_core::codecs::{self, Options};
use super::guard; use super::guard;
@@ -30,7 +30,7 @@ pub struct NativeCoreImageInfo {
pub transfer: u8, pub transfer: u8,
} }
/// Decodes a JPEG XL image to premultiplied RGBA in sRGB, or with `keep_space`, in the image's own /// Decodes an image to premultiplied RGBA in sRGB, or with `keep_space`, in the image's own
/// space when it has BT.709, BT.2020 or Display P3 primaries with the sRGB, PQ or HLG transfer. /// space when it has BT.709, BT.2020 or Display P3 primaries with the sRGB, PQ or HLG transfer.
/// Unless either is 0, it is downscaled to cover `target_width` x `target_height`, and it never /// Unless either is 0, it is downscaled to cover `target_width` x `target_height`, and it never
/// exceeds `max_pixel_size` pixels per side. `info` gets the size and space. The caller frees the /// exceeds `max_pixel_size` pixels per side. `info` gets the size and space. The caller frees the
@@ -42,7 +42,7 @@ pub struct NativeCoreImageInfo {
/// `cancelled` must be null or valid for the call and only changed through `native_core_cancel`. /// `cancelled` must be null or valid for the call and only changed through `native_core_cancel`.
/// `info` must be null or a valid writable pointer, outside `data`. /// `info` must be null or a valid writable pointer, outside `data`.
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub unsafe extern "C" fn native_core_jxl_decode( pub unsafe extern "C" fn native_core_decode_image(
data: *const u8, data: *const u8,
len: size_t, len: size_t,
keep_space: bool, keep_space: bool,
@@ -60,16 +60,19 @@ pub unsafe extern "C" fn native_core_jxl_decode(
let cancelled = unsafe { AtomicBool::from_ptr(cancelled) }; let cancelled = unsafe { AtomicBool::from_ptr(cancelled) };
// SAFETY: the caller provides a buffer readable for len bytes. // SAFETY: the caller provides a buffer readable for len bytes.
let input = unsafe { slice::from_raw_parts(data, len) }; let input = unsafe { slice::from_raw_parts(data, len) };
let target = if keep_space { let color_space = if keep_space {
Target::Source Target::Source
} else { } else {
Target::Srgb Target::Srgb
}; };
let cover = (target_width > 0 && target_height > 0) let target_size = (target_width > 0 && target_height > 0)
.then_some((target_width as usize, target_height as usize)); .then_some((target_width as usize, target_height as usize));
let Some(Image { cicp, samples }) = Jxl::open(input, target) let options = Options {
.and_then(|image| image.decode(cover, max_pixel_size as usize, cancelled)) color_space,
else { target_size,
max_pixel_size: max_pixel_size as usize,
};
let Some(Image { cicp, samples }) = codecs::decode(input, &options, cancelled) else {
return ptr::null_mut(); return ptr::null_mut();
}; };
let (width, height, bit_depth, pixels) = match samples { let (width, height, bit_depth, pixels) = match samples {
@@ -93,7 +96,7 @@ pub unsafe extern "C" fn native_core_jxl_decode(
/// Asks the decode given `flag` to stop early. /// Asks the decode given `flag` to stop early.
/// ///
/// # Safety /// # Safety
/// `flag` must be null or valid and otherwise only read by `native_core_jxl_decode`. /// `flag` must be null or valid and otherwise only read by `native_core_decode_image`.
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub unsafe extern "C" fn native_core_cancel(flag: *mut bool) { pub unsafe extern "C" fn native_core_cancel(flag: *mut bool) {
if !flag.is_null() { if !flag.is_null() {
@@ -104,7 +107,7 @@ pub unsafe extern "C" fn native_core_cancel(flag: *mut bool) {
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
#[unsafe(no_mangle)] #[unsafe(no_mangle)]
pub extern "system" fn Java_app_alextran_immich_NativeImage_decodeJxl<'caller>( pub extern "system" fn Java_app_alextran_immich_NativeImage_decode<'caller>(
mut env: EnvUnowned<'caller>, mut env: EnvUnowned<'caller>,
_class: JClass<'caller>, _class: JClass<'caller>,
data: jlong, data: jlong,
@@ -121,7 +124,7 @@ pub extern "system" fn Java_app_alextran_immich_NativeImage_decodeJxl<'caller>(
// SAFETY: Kotlin passes a live native buffer of `length` bytes and a direct buffer it only // SAFETY: Kotlin passes a live native buffer of `length` bytes and a direct buffer it only
// changes through `cancel`, and the output is valid. // changes through `cancel`, and the output is valid.
let rgba = unsafe { let rgba = unsafe {
native_core_jxl_decode( native_core_decode_image(
data as *const u8, data as *const u8,
length as size_t, length as size_t,
false, false,
@@ -154,7 +157,7 @@ pub extern "system" fn Java_app_alextran_immich_NativeImage_cancel<'caller>(
) { ) {
env.with_env(|env| -> jni::errors::Result<()> { env.with_env(|env| -> jni::errors::Result<()> {
let flag = env.get_direct_buffer_address(&flag)?.cast::<bool>(); let flag = env.get_direct_buffer_address(&flag)?.cast::<bool>();
// SAFETY: the buffer is the live one-byte flag Kotlin passed to `decodeJxl`. // SAFETY: the buffer is the live one-byte flag Kotlin passed to `decode`.
unsafe { native_core_cancel(flag) }; unsafe { native_core_cancel(flag) };
Ok(()) Ok(())
}) })
+1 -1
View File
@@ -9,7 +9,7 @@ mod android;
mod bitmap; mod bitmap;
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
mod convert; mod convert;
mod jxl; mod decode;
#[cfg(target_os = "android")] #[cfg(target_os = "android")]
mod rotate; mod rotate;
mod thumbhash; mod thumbhash;
+1
View File
@@ -11,6 +11,7 @@ const _manifests = [
'../.cargo/config.toml', '../.cargo/config.toml',
'../Cargo.toml', '../Cargo.toml',
'../Cargo.lock', '../Cargo.lock',
'../crates/codecs/Cargo.toml',
'../crates/native_core/Cargo.toml', '../crates/native_core/Cargo.toml',
'$_crate/Cargo.toml', '$_crate/Cargo.toml',
'$_crate/rust-toolchain.toml', '$_crate/rust-toolchain.toml',
+11 -3
View File
@@ -1237,6 +1237,7 @@
"state": "Stable" "state": "Stable"
} }
], ],
"x-immich-permission": "notification.create",
"x-immich-state": "Stable" "x-immich-state": "Stable"
} }
}, },
@@ -1306,6 +1307,7 @@
"state": "Stable" "state": "Stable"
} }
], ],
"x-immich-permission": "notification.create",
"x-immich-state": "Stable" "x-immich-state": "Stable"
} }
}, },
@@ -1366,6 +1368,7 @@
"state": "Stable" "state": "Stable"
} }
], ],
"x-immich-permission": "notification.create",
"x-immich-state": "Stable" "x-immich-state": "Stable"
} }
}, },
@@ -6340,7 +6343,7 @@
"state": "Added" "state": "Added"
} }
], ],
"x-immich-permission": "clusterGroup.read" "x-immich-permission": "clusterGroup.admin"
} }
}, },
"/cluster-groups/{id}/requests": { "/cluster-groups/{id}/requests": {
@@ -23130,6 +23133,7 @@
"description": "Image format", "description": "Image format",
"enum": [ "enum": [
"jpeg", "jpeg",
"jxl",
"webp" "webp"
], ],
"type": "string" "type": "string"
@@ -25807,6 +25811,7 @@
"backup.delete", "backup.delete",
"clusterGroup.read", "clusterGroup.read",
"clusterGroup.leave", "clusterGroup.leave",
"clusterGroup.admin",
"clusterGroupRequest.create", "clusterGroupRequest.create",
"clusterGroupRequest.read", "clusterGroupRequest.read",
"clusterGroupRequest.delete", "clusterGroupRequest.delete",
@@ -28462,11 +28467,13 @@
"ServerFeatureLevel": { "ServerFeatureLevel": {
"description": "Server feature level", "description": "Server feature level",
"enum": [ "enum": [
1 1,
2
], ],
"type": "integer", "type": "integer",
"x-enum-varnames": [ "x-enum-varnames": [
"FeatureLevel" "FeatureLevel",
"Passkeys"
] ]
}, },
"ServerFeaturesDto": { "ServerFeaturesDto": {
@@ -34666,6 +34673,7 @@
"enum": [ "enum": [
"AssetCreate", "AssetCreate",
"AssetMetadataExtraction", "AssetMetadataExtraction",
"AssetOcrExtraction",
"AssetTagged" "AssetTagged"
], ],
"type": "string" "type": "string"
+1 -1
View File
@@ -2,6 +2,6 @@
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2, "spaces": 2,
"generator-cli": { "generator-cli": {
"version": "7.25.0" "version": "7.26.0"
} }
} }
+1 -1
View File
@@ -8,7 +8,7 @@
"format": "prettier --cache --check i18n/", "format": "prettier --cache --check i18n/",
"format:fix": "prettier --cache --write --list-different i18n" "format:fix": "prettier --cache --write --list-different i18n"
}, },
"packageManager": "pnpm@12.14.0", "packageManager": "pnpm@12.15.0",
"engines": { "engines": {
"pnpm": ">=10.0.0" "pnpm": ">=10.0.0"
}, },
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:24.22.1-trixie-slim@sha256:fdc9220097e0ae3bd947c44b10cd3af5867a67a6d3a23b30b30b70929219034e AS core FROM node:24.22.1-trixie-slim@sha256:43b9ba1142b8dcf33fa8f922ca1be85714adc444e0586c7cf7726daaeabc1bea AS core
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY package* pnpm* .pnpmfile.cjs ./ COPY package* pnpm* .pnpmfile.cjs ./
+2 -2
View File
@@ -33,7 +33,7 @@
"eslint": "^10.0.0", "eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^75.0.0", "eslint-plugin-unicorn": "^78.0.0",
"globals": "^17.0.0", "globals": "^17.0.0",
"mock-fs": "^5.2.0", "mock-fs": "^5.2.0",
"prettier": "^3.7.4", "prettier": "^3.7.4",
@@ -62,7 +62,7 @@
"node": ">=22.0.0" "node": ">=22.0.0"
}, },
"dependencies": { "dependencies": {
"chokidar": "^5.0.0", "chokidar": "^6.0.0",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"fastq": "^1.17.1", "fastq": "^1.17.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
+19 -19
View File
@@ -28,11 +28,11 @@ export const defaultQueueOptions = {
}; };
/** /**
* An in-memory queue that processes tasks in parallel with a given concurrency. An in-memory queue that processes tasks in parallel with a given concurrency.
* @see {@link https://www.npmjs.com/package/fastq} @see {@link https://www.npmjs.com/package/fastq}
* @template T - The type of the worker task data. @template T - The type of the worker task data.
* @template R - The type of the worker output data. @template R - The type of the worker output data.
*/ */
export class Queue<T, R> { export class Queue<T, R> {
private readonly queue: fastq.queueAsPromised<string, Task<T, R>>; private readonly queue: fastq.queueAsPromised<string, Task<T, R>>;
private readonly store = new Map<string, Task<T, R>>(); private readonly store = new Map<string, Task<T, R>>();
@@ -40,10 +40,10 @@ export class Queue<T, R> {
readonly worker: (data: T) => Promise<R>; readonly worker: (data: T) => Promise<R>;
/** /**
* Create a new queue. Create a new queue.
* @param worker - The worker function that processes the task. @param worker - The worker function that processes the task.
* @param options - The queue options. @param options - The queue options.
*/ */
constructor(worker: (data: T) => Promise<R>, options?: QueueOptions) { constructor(worker: (data: T) => Promise<R>, options?: QueueOptions) {
this.options = { ...defaultQueueOptions, ...options }; this.options = { ...defaultQueueOptions, ...options };
this.worker = worker; this.worker = worker;
@@ -65,21 +65,21 @@ export class Queue<T, R> {
} }
/** /**
* Wait for the queue to be empty. Wait for the queue to be empty.
* @returns Promise<void> - The returned Promise will be resolved when all tasks in the queue have been processed by a worker. @returns Promise<void> - The returned Promise will be resolved when all tasks in the queue have been processed by a worker.
* This promise could be ignored as it will not lead to a `unhandledRejection`. This promise could be ignored as it will not lead to a `unhandledRejection`.
*/ */
drained(): Promise<void> { drained(): Promise<void> {
return this.queue.drained(); return this.queue.drained();
} }
/** /**
* Add a task at the end of the queue. Add a task at the end of the queue.
* @see {@link https://www.npmjs.com/package/fastq} @see {@link https://www.npmjs.com/package/fastq}
* @param data @param data
* @returns Promise<void> - A Promise that will be fulfilled (rejected) when the task is completed successfully (unsuccessfully). @returns Promise<void> - A Promise that will be fulfilled (rejected) when the task is completed successfully (unsuccessfully).
* This promise could be ignored as it will not lead to a `unhandledRejection`. This promise could be ignored as it will not lead to a `unhandledRejection`.
*/ */
async push(data: T): Promise<Task<T, R>> { async push(data: T): Promise<Task<T, R>> {
const id = uniqueId(); const id = uniqueId();
const task: Task<T, R> = { id, status: 'idle', error: undefined, count: 0, data, result: undefined }; const task: Task<T, R> = { id, status: 'idle', error: undefined, count: 0, data, result: undefined };
+3 -3
View File
@@ -218,9 +218,9 @@ export const sha1 = (filepath: string) => {
}; };
/** /**
* Batches items and calls onBatch to process them Batches items and calls onBatch to process them
* when the batch size is reached or the debounce time has passed. when the batch size is reached or the debounce time has passed.
*/ */
export class Batcher<T = unknown> { export class Batcher<T = unknown> {
private items: T[] = []; private items: T[] = [];
private readonly batchSize: number; private readonly batchSize: number;

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