Compare commits

...

1315 Commits

Author SHA1 Message Date
Lincoln Stein
36c72c76c1 fix: show Prune button whenever terminal install jobs exist
Previously the Prune button was only shown prominently when ALL jobs
were terminal (no active/cancelable jobs). Now it shows whenever at
least one terminal job exists, even alongside in-progress jobs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 16:27:55 -04:00
joshistoast
a96c4d851b style(model manager queue): fix prettier lint 2026-03-30 14:49:25 -06:00
joshistoast
106ae5ada8 feat(model manager queue): optimistic updated and code qulity
- Treated downloads_done as an active install phase for row UI and bulk cancel.
- Stopped stale error text from overriding the badge after resume/restart by only showing the error label when
  the displayed status is actually error.
- Added row-level action locking to block duplicate pause/resume/cancel/restart submissions.
- Added optimistic row status handling so the UI does not briefly fall back to stale error/restart state
  before RTK Query/socket updates arrive.
- Fixed local-path basename parsing for both the main row title and restart-required file rows.
- Added an accessible aria-label to the overflow menu button.
2026-03-30 14:45:19 -06:00
joshistoast
44efbc0b88 feat(model manager queue): disable actions dropdown if items disabled 2026-03-30 13:58:58 -06:00
Josh Corbett
9d4bf2d995 Merge branch 'main' into feat/model-manager-queue-redesign 2026-03-30 13:46:33 -06:00
Jonathan
6963cd97ba Fix SIGINT shutdown during active inference (#8993) 2026-03-28 18:35:18 -04:00
Lincoln Stein
1c6870d2ec Merge branch 'main' into feat/model-manager-queue-redesign 2026-03-28 18:10:14 -04:00
Lincoln Stein
ab6f186f8c chore: bump version to 6.12.0.post1 (#8990)
* (chore) bump version to 6.12.0.post1
2026-03-25 22:00:13 -04:00
Lincoln Stein
7f2878f691 Fix(frontend): Make ordering of multiple FLUX.2 reference images deterministic (#8989) 2026-03-24 09:52:50 -04:00
Weblate (bot)
d32f6b5a56 ui: translations update from weblate (#8985)
* translationBot(ui): update translation (Italian)

Currently translated at 98.0% (2205 of 2250 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 97.8% (2210 of 2259 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.8% (2224 of 2272 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 98.1% (2252 of 2295 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 98.0% (2264 of 2309 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Russian)

Currently translated at 60.7% (1419 of 2334 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/

* translationBot(ui): update translation (Italian)

Currently translated at 98.1% (2290 of 2334 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.7% (2319 of 2372 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.7% (2327 of 2380 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.7% (2328 of 2382 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.5% (2370 of 2429 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Finnish)

Currently translated at 1.5% (37 of 2429 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/fi/

* translationBot(ui): update translation (Italian)

Currently translated at 97.5% (2373 of 2433 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Japanese)

Currently translated at 87.1% (2120 of 2433 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ja/

* translationBot(ui): update translation (Italian)

Currently translated at 97.5% (2374 of 2433 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Co-authored-by: DustyShoe <warukeichi@gmail.com>
Co-authored-by: Ilmari Laakkonen <ilmarille@gmail.com>
Co-authored-by: 嶋田豪介 <shimada_gosuke@cyberagent.co.jp>
2026-03-23 23:09:57 -04:00
Jonathan
f7aa5fcbbf Add chaining to Collect node (#8933)
* Add chained collect node

* test(frontend): align parseSchema fixtures with collect v1.1 and normalize undefined fields in assertions

* fix(nodes): block collect-to-collect links when inferred item types differ

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-03-24 01:39:52 +00:00
Lincoln Stein
438515bf9a Chore: Bump version to 6.12.0 (#8981)
* chore: bump version to 6.12.0

* chore: update What's New text
2026-03-23 20:20:01 -04:00
Josh Corbett
faebc90845 Merge branch 'main' into feat/model-manager-queue-redesign 2026-03-23 13:06:29 -06:00
dunkeroni
17da6bb9c3 Fix(UI): Replace boolean submenu icon with PiIntersectSquareBold (#8962)
* change submenu icon to phosphor

* Use PiIntersectSquareBold
2026-03-15 11:14:35 -04:00
Weblate (bot)
b120ef5183 ui: translations update from weblate (#8956)
* translationBot(ui): update translation (Italian)

Currently translated at 98.0% (2205 of 2250 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 97.8% (2210 of 2259 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.8% (2224 of 2272 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 98.1% (2252 of 2295 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 98.0% (2264 of 2309 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Russian)

Currently translated at 60.7% (1419 of 2334 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/

* translationBot(ui): update translation (Italian)

Currently translated at 98.1% (2290 of 2334 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.7% (2319 of 2372 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.7% (2327 of 2380 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.7% (2328 of 2382 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.5% (2370 of 2429 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Finnish)

Currently translated at 1.5% (37 of 2429 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/fi/

* translationBot(ui): update translation (Italian)

Currently translated at 97.5% (2373 of 2433 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Co-authored-by: DustyShoe <warukeichi@gmail.com>
Co-authored-by: Ilmari Laakkonen <ilmarille@gmail.com>
2026-03-15 11:01:09 -04:00
Jonathan
dc5007fe95 Fix/model cache Qwen/CogView4 cancel repair (#8959)
* Repair partially loaded Qwen models after cancel to avoid device mismatches

* ruff

* Repair CogView4 text encoder after canceled partial loads

* Avoid MPS CI crash in repair regression test

* Fix MPS device assertion in repair test
2026-03-15 10:04:15 -04:00
Alexander Eichhorn
bba207a856 fix(ui): IP adapter / control adapter model recall for reinstalled models (#8960)
* fix(ui): resolve models by name+base+type when recalling metadata for reinstalled models

When a model (IP Adapter, ControlNet, etc.) is deleted and reinstalled,
it gets a new UUID key. Previously, metadata recall would fail because
it only looked up models by their stored UUID key. Now the recall falls
back to searching by name+base+type, allowing reinstalled models with
the same name to be correctly resolved.

https://claude.ai/code/session_01XYubzMK363BXGTvfJJqFnX

* Add hash-based model recall fallback for reinstalled models

When a model is deleted and reinstalled, it gets a new UUID key but
retains the same BLAKE3 content hash. This adds hash as a middle
fallback stage in model resolution (key → hash → name+base+type),
making recall more robust.

Changes:
- Add /api/v2/models/get_by_hash backend endpoint (uses existing
  search_by_hash from model records store)
- Add getModelConfigByHash RTK Query endpoint in frontend
- Add hash fallback to both resolveModel and parseModelIdentifier

https://claude.ai/code/session_01XYubzMK363BXGTvfJJqFnX

* Chore pnpm fix

* Chore typegen

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-11 17:59:47 +00:00
Alexander Eichhorn
a7b367fda2 fix: only delete individual LoRA file instead of entire parent directory (#8954)
When deleting a file-based model (e.g. LoRA), the previous logic used
rmtree on the parent directory, which would delete all files in that
folder — even unrelated ones. Now only the specific model file is
removed, and the parent directory is cleaned up only if empty afterward.
2026-03-10 22:33:08 +00:00
Lincoln Stein
cd47b3baf7 Feature: Make strict password checking optional (#8957)
* feat: add strict_password_checking config option to relax password requirements

- Add `strict_password_checking: bool = Field(default=False)` to InvokeAIAppConfig
- Add `get_password_strength()` function to password_utils.py (returns weak/moderate/strong)
- Add `strict_password_checking` field to SetupStatusResponse API endpoint
- Update users_base.py and users_default.py to accept `strict_password_checking` param
- Update auth.py router to pass config.strict_password_checking to all user service calls
- Create shared frontend utility passwordUtils.ts for password strength validation
- Update AdministratorSetup, UserProfile, UserManagement components to:
  - Fetch strict_password_checking from setup status endpoint
  - Show colored strength indicators (red/yellow/blue) in non-strict mode
  - Allow any non-empty password in non-strict mode
  - Maintain strict validation behavior when strict_password_checking=True
- Update SetupStatusResponse type in auth.ts endpoint
- Add passwordStrength and passwordHelperRelaxed translation keys to en.json
- Add tests for new get_password_strength() function

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Changes before error encountered

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore(backend): docstrings

* chore(frontend): typegen

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com>
2026-03-10 18:22:47 -04:00
Josh Corbett
afcdacb29d Merge branch 'main' into feat/model-manager-queue-redesign 2026-03-09 18:17:28 -06:00
Weblate (bot)
c8ac303ad2 ui: translations update from weblate (#8947)
* translationBot(ui): update translation (Italian)

Currently translated at 98.0% (2205 of 2250 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 97.8% (2210 of 2259 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.8% (2224 of 2272 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 98.1% (2252 of 2295 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 98.0% (2264 of 2309 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Russian)

Currently translated at 60.7% (1419 of 2334 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/

* translationBot(ui): update translation (Italian)

Currently translated at 98.1% (2290 of 2334 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.7% (2319 of 2372 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.7% (2327 of 2380 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.7% (2328 of 2382 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.5% (2370 of 2429 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Co-authored-by: DustyShoe <warukeichi@gmail.com>
2026-03-09 16:16:39 -04:00
Sense_wang
f01cbd35a8 docs: Fix typo in contributing guide - remove extra 'the' (#8949)
Co-authored-by: Contributor <contributor@example.com>
2026-03-09 18:03:32 +00:00
Sense_wang
2179d93ce0 docs: Fix typo in README.md - 'easy' should be 'ease' (#8948)
Co-authored-by: Contributor <contributor@example.com>
2026-03-09 18:01:54 +00:00
Lincoln Stein
863fa50551 Doc: update multiuser mode documentation (#8953)
* docs(multiuser): update multiuser mode documentation

* Update docs/multiuser/user_guide.md

Co-authored-by: dunkeroni <dunkeroni@gmail.com>

* Update docs/multiuser/user_guide.md

Co-authored-by: dunkeroni <dunkeroni@gmail.com>

* Update docs/multiuser/user_guide.md

Co-authored-by: dunkeroni <dunkeroni@gmail.com>

* slight wording change

* add info about the host interface binding option

---------

Co-authored-by: dunkeroni <dunkeroni@gmail.com>
2026-03-09 17:56:56 +00:00
DustyShoe
e74d8ab2bb Fix(gallery): Re-add image browsing with arrow keys (#8874)
* fix(gallery): restore arrow-key browsing and extract shared prev/next navigation

* Added same behavior to Upscale mode and autofocus to gallery after using hotkeys Ctrl+Enter and Ctrl+Shift+Enter

* restore arrow navigation focus flow across viewer states

* fix(gallery): stabilize arrow-key browsing, remove viewer UI flicker, and optimize code

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-03-09 12:23:00 +00:00
Lincoln Stein
2d1dbceae5 Add user management UI for admin and regular users (#106) (#8937)
* Add user management UI for admin and regular users (#106)

* Add user management UI and backend API endpoints

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

Fix user management feedback: cancel/back navigation, system user filter, tooltip fix

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

Make Back button on User Management page more prominent

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore(frontend): typegen

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>

* Add Confirm Password field to My Profile password change form (#110)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Alexander Eichhorn <alex@eichhorn.dev>
2026-03-08 16:49:15 -04:00
Jonathan
62b7c7a6e8 Added SQL injection tests (#8873)
* Added SQL injection tests

* Updated tests after multi-user merge

* ruff:format

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-03-07 02:07:14 +00:00
Weblate (bot)
b8b6798167 ui: translations update from weblate (#8946)
* translationBot(ui): update translation (Italian)

Currently translated at 98.0% (2205 of 2250 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 97.8% (2210 of 2259 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.8% (2224 of 2272 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 98.1% (2252 of 2295 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 98.0% (2264 of 2309 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Russian)

Currently translated at 60.7% (1419 of 2334 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/

* translationBot(ui): update translation (Italian)

Currently translated at 98.1% (2290 of 2334 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.7% (2319 of 2372 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.7% (2327 of 2380 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Co-authored-by: DustyShoe <warukeichi@gmail.com>
2026-03-06 20:56:30 -05:00
Alexander Eichhorn
274d9b3a74 fix(model_manager): detect Flux 2 Klein LoRAs in Kohya format with transformer-only keys (#8938)
LoRAs trained with musubi-tuner (and potentially other trainers) that
only target transformer blocks (double_blocks/single_blocks) without
embedding layers (txt_in/vector_in/context_embedder) were incorrectly
classified as Flux 1. Add fallback detection using attention projection
hidden_size and MLP ratio from transformer block tensors

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-03-07 01:52:25 +00:00
girlyoulookthebest
3d81edac61 perf(flux2): optimize cache locking in Klein encoder to fix #7513 (#8863)
* perf(flux2): optimize model loading order to prevent cache eviction (fixes #7513)

* Update flux2_klein_text_encoder.py

* Update flux2_klein_text_encoder.py version

---------

Co-authored-by: Alexander Eichhorn <alex@eichhorn.dev>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-03-07 01:43:12 +00:00
Alexander Eichhorn
df225d3751 Fix model reidentify losing path and failing on IP Adapters (#8941)
The reidentify endpoint overwrote the model's relative path with an
absolute path from the prober, and unconditionally accessed
trigger_phrases which doesn't exist on all config types (e.g. IP
Adapters), causing an AttributeError.

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-03-07 01:24:02 +00:00
Josh Corbett
fcdcd7f46b Prompt Attention Fixes (#8860)
* fix(prompt): add more punctuations, fixes attention hotkeys removing them from prompt.

* fix(prompt): improve numeric weighting calculation

* feat(prompts): add numeric attention preference toggle to settings

* feat(prompts): use attention style preference, rewrite to accomodate prompt functions

* fix(prompts): account for weirdness with quotes

account for mismatching quotes, missing quotes and other quote entities

* fix(prompts): add tests, qol improvements, code cleanup

* fix(prompts): test lint

* fix(prompts): remove unused exports

* fix(prompts): separator whitespace serialization

---------

Co-authored-by: joshistoast <me@joshcorbett.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-03-07 01:13:30 +00:00
Lincoln Stein
94e04b1e1e Fix race condition in download queue when concurrent jobs share destination directory (#104) (#8931)
* Initial plan

* Fix race condition in _do_download when scanning for .downloading files



* chore(backend): update copyright

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
2026-03-06 03:35:44 +00:00
Lincoln Stein
67669b7fbe QoL: Persist selected board and most recent image across browser sessions (#8920)
* Persist selected board and auto-select most recent image across browser sessions (#92)

* Persist selectedBoardId across browser sessions

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* fix(frontend): make appStarted listener async so image auto-selection works on startup

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore(frontend): remove unwanted package-lock.json

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
2026-03-06 03:08:09 +00:00
Lincoln Stein
c7bdaf93b2 Fix: Shut down the server with one keyboard interrupt (#94) (#8936)
* Fix: Kill the server with one keyboard interrupt (#94)

* Initial plan

* Handle KeyboardInterrupt in run_app to allow single Ctrl+C shutdown

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Force os._exit(0) on KeyboardInterrupt to avoid hanging on background threads

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

Fix graceful shutdown to wait for download/install worker threads (#102)

* Initial plan

* Replace os._exit(0) with ApiDependencies.shutdown() on KeyboardInterrupt

Instead of immediately force-exiting the process on CTRL+C, call
ApiDependencies.shutdown() to gracefully stop the download and install
manager services, allowing active work to complete or cancel cleanly
before the process exits.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Make stop() idempotent in download and model install services

When CTRL+C is pressed, uvicorn's graceful shutdown triggers the FastAPI
lifespan which calls ApiDependencies.shutdown(), then a KeyboardInterrupt
propagates from run_until_complete() hitting the except block which tries
to call ApiDependencies.shutdown() a second time.

Change both stop() methods to return silently (instead of raising) when
the service is not running. This handles:
- Double-shutdown: lifespan already stopped the services
- Early interrupt: services were never fully started

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

Fix shutdown hang on session processor thread lock (#108)

* Initial plan

* Fix shutdown hang: wake session processor thread on stop() and mark daemon

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix: shut down asyncio executor on KeyboardInterrupt to prevent post-generation hang (#112)

Fix: cancel pending asyncio tasks before loop.close() to suppress destroyed-task warnings
Fix: suppress stack trace when dispatching events after event loop is closed on shutdown
Fix: cancel in-progress generation on stop() to prevent core dump during mid-flight Ctrl+C

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
2026-03-05 22:01:40 -05:00
joshistoast
8d44474b81 feat(model manager queue): tweak column widths 2026-03-04 19:33:27 -07:00
joshistoast
89bf302efd feat(model manager queue): reorganize bulk actions 2026-03-04 15:00:39 -07:00
joshistoast
eb54499427 fix(model manager queue): qol list item ui tweaks 2026-03-04 14:22:56 -07:00
joshistoast
2e014dca03 feat(model meneger queue): backend disconnected visual feedback 2026-03-04 01:18:57 -07:00
joshistoast
db1c17a84d fix(model manager queue): prettier lint 2026-03-03 21:34:29 -07:00
joshistoast
04b064a994 fix(model manager queue): remove unused useStore import 2026-03-03 21:30:28 -07:00
Alexander Eichhorn
6b57b004a4 feat(MM):model settings export import (#8872)
* feat(model_manager): add export/import for model settings

Add the ability to export model settings (default_settings, trigger_phrases,
cpu_only) as JSON and import them back. The model name is used as the
filename for exports.

https://claude.ai/code/session_01LXKjbRjfzcG3d3vzk3xRCh

* fix(ui): reset settings forms after import so updated values display immediately

The useForm defaultValues only apply on mount, so importing model settings
updated the backend but the forms kept showing stale values. Added useEffect
to reset forms when the underlying model config changes. Also fixed lint
errors (strict equality, missing React import).

* fix(ui): harden model settings export/import

Prevent cross-model-type import errors by filtering imported fields
against the target model's supported fields, showing clear warnings
for incompatible or partially compatible settings instead of raw
pydantic validation errors. Also fix falsy checks for empty arrays
and objects in export, disable export button when nothing to export,
add client-side validation and FileReader error handling on import.

* Chore pnpm fix

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-03-02 03:03:10 +00:00
DustyShoe
6fe7910a90 fix(model-install): persist remote access_token for resume after restart (#8932)
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-03-02 02:44:21 +00:00
DustyShoe
445c6a3c36 Fix(MM): Fixed incorrect advertised model size for Z-Image Turbo (#8934) 2026-03-01 21:31:53 -05:00
Alexander Eichhorn
54c1609687 Filter non-transformer keys from Z-Image checkpoint state dicts (#8918)
Merged Z-Image checkpoints (e.g. models with LoRAs baked in) may bundle
text encoder weights (text_encoders.*) or other non-transformer keys
alongside the transformer weights. These cause load_state_dict() to fail
with strict=True. Instead of disabling strict mode, explicitly whitelist
valid ZImageTransformer2DModel key prefixes and discard everything else.

Also moves RAM allocation after filtering so it doesn't over-allocate
for discarded keys.

Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com>
2026-02-28 16:22:29 +00:00
Alexander Eichhorn
ec46b5cb9e Fix: Replace deprecated huggingface_hub.get_token_permission() with whoami() (#8913)
`get_token_permission` is deprecated and will be removed in huggingface_hub 1.0.
Use `whoami()` to validate the token instead, as recommended by the deprecation warning.
2026-02-28 15:59:45 +00:00
Weblate (bot)
4fd5cd26a0 ui: translations update from weblate (#8924)
* translationBot(ui): update translation (Italian)

Currently translated at 98.0% (2205 of 2250 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 97.8% (2210 of 2259 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.8% (2224 of 2272 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 98.1% (2252 of 2295 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 98.0% (2264 of 2309 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Russian)

Currently translated at 60.7% (1419 of 2334 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/

* translationBot(ui): update translation (Italian)

Currently translated at 98.1% (2290 of 2334 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.7% (2319 of 2372 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Co-authored-by: DustyShoe <warukeichi@gmail.com>
2026-02-28 15:09:46 +00:00
DustyShoe
c83c4af1ea Fix(UI): Fixes broken "Cancel Current Item" button in left panel. (#8925) 2026-02-28 10:06:41 -05:00
Lincoln Stein
146b936844 feat(multiuser mode): Support multiple isolated users on same backend (#8822)
* Add comprehensive multi-user support specification and implementation plan

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Clarify Python tooling transition state

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Add executive summary for multi-user support specification

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Reorganize multiuser docs into subfolder and update with approved design decisions

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* docs: fix mkdocs syntax issues

* Fix Z-Image VAE operations not reserving working memory for OOM prevention (#8)

* Initial plan

* Fix Z-Image VAE encode/decode to request working memory

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Add test for Z-Image working memory estimation

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix unit test: only set config attribute for AutoencoderKL, not FluxAutoEncoder

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* style: fix ruff errors

* Fix test: use model_construct to bypass Pydantic validation for mock fields

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore(ruff): fix ruff errors

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>

* Phase 1: Add multi-user database schema and authentication foundation (#6)

* Initial plan

* Add Phase 1: Database schema, authentication utilities, and user service

- Added dependencies: passlib[bcrypt], python-jose[cryptography], email-validator
- Created migration_25 for multi-user support database schema
  - users table with authentication fields
  - user_sessions, user_invitations, shared_boards tables
  - Added user_id columns to boards, images, workflows, session_queue, style_presets
  - Created system user for backward compatibility
- Implemented authentication utilities
  - Password hashing and validation (password_utils.py)
  - JWT token generation and verification (token_service.py)
- Implemented user service
  - Abstract base class (users_base.py)
  - SQLite implementation (users_default.py)
  - Full CRUD operations for users
  - Authentication and admin management
- Added comprehensive tests for all components
- All code passes ruff linting and mypy type checking

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Refactor user service to use transaction pattern and improve security documentation

- Changed all database operations to use self._db.transaction() context manager
- Follows the same pattern as other services like board_records
- Improves thread safety and transaction management
- Added detailed security warnings for the hardcoded JWT secret key
- Documented that it must be moved to config before production
- Fixed whitespace linting issues
- All tests still pass with the refactored code

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix system user email to use valid domain

- Changed system user email from system@invokeai.local to system@system.invokeai
- .local domains are rejected by email-validator as special-use/reserved
- All end-to-end tests now pass successfully

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix test failures: password length, migration chain, and table existence checks

- Fixed bcrypt 72-byte password limit by truncating long passwords in hash_password and verify_password
- Fixed test_migration_25 to use callback directly instead of migrator to avoid fragmented chain error
- Updated system user email assertion in test from @invokeai.local to @system.invokeai
- Added table existence checks for boards, images, workflows, and session_queue before altering them
- This prevents "no such table" errors when migration runs in environments without all tables

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* (chore) fix ruff errors

* (chore) fix ruff errors (2)

* Fix test failures: use _conn instead of conn and improve password truncation

- Fixed test_user_service.py to use db._conn instead of db.conn (SqliteDatabase uses private _conn)
- Simplified password truncation logic in password_utils.py for better clarity
- Ensured UTF-8 decoding handles truncated bytes properly with errors="ignore"

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore(uv): updated uv lockfile

* Fix bcrypt password length errors by configuring passlib properly

- Added bcrypt__relax_truncate_checks=True to CryptContext to prevent errors on long passwords
- Removed min_length=8 constraint from pydantic models to allow service-level validation
- Service-level validation provides better error messages and more control
- Manual truncation code kept as safety net for passwords >72 bytes

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix bcrypt parameter name: use truncate_error instead of relax_truncate_checks

- Changed bcrypt__relax_truncate_checks=True to bcrypt__truncate_error=False
- The correct passlib parameter is truncate_error (not relax_truncate_checks)
- Setting it to False allows passwords >72 bytes without raising an error

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* fix(passwords) downgrade bcrypt to work with current passlib

* chore(uv): update locks file

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>

* Implement Phase 2: Authentication Service with JWT and FastAPI Integration (#11)

* Initial plan

* Add Phase 2 authentication service - auth dependencies, router, and service integration

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Address code review feedback - add token expiration constants and improve documentation

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix email validation to allow special-use domains like .local for testing

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix system user to not be admin - allows /auth/setup to work on fresh database

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore: typegen

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>

* Add Phase 3 integration tests and documentation for authentication middleware (#15)

* Initial plan

* Add Phase 3 integration tests for auth router endpoints

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Add Phase 3 completion documentation and testing guide

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore: ruff

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>

* chore: fix unresolved merged conflict markers

* Phase 4: Add multi-tenancy to boards and session queue services (#17)

* Initial plan

* Update boards service for multi-user support

- Add user_id parameter to board creation and filtering
- Update board_records base and SQLite implementation to filter by user
- Include shared boards and public boards in user queries
- Update boards router to require authentication
- Update invocation context to pass user_id from queue item
- Add user_id field to SessionQueueItem model

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Update session_queue service for multi-user support

- Add user_id parameter to enqueue_batch method
- Update ValueToInsertTuple to include user_id
- Update prepare_values_to_insert to accept and use user_id
- Update SQL INSERT statements to include user_id column
- Update session_queue router to require authentication
- Update retry_items_by_id to preserve user_id when retrying

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Add authentication imports to images, workflows, and style_presets routers

- Import CurrentUser dependency in all three routers
- Add CurrentUser parameter to upload_image endpoint
- Prepare for further authentication requirements in these services

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Add multiuser tests and Phase 4 documentation

- Create test_boards_multiuser.py with authentication tests
- Verify board operations require authentication
- Test board creation and listing with auth tokens
- Add comprehensive Phase 4 implementation summary
- Document all changes, data flows, and security considerations

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Add authentication to remaining board endpoints

- Add CurrentUser to get_board endpoint
- Add CurrentUser to update_board endpoint
- Add CurrentUser to delete_board endpoint
- Ensures all board operations require authentication
- Addresses code review feedback

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Feature(image boards): Implement per-user board isolation

- Complete verification report with all checks passed
- Document code quality, security, and testing results
- List all achievements and sign-off criteria
- Mark phase as READY FOR MERGE

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore: ruff

* chore: resolve conflicts in z_image_working_memory test

* chore: ruff

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>

* feat: Implement Phase 5 of multiuser plan - Frontend authentication (#19)

* Phase 5: Implement frontend authentication infrastructure

- Created auth slice with Redux state management for authentication
- Created auth API endpoints (login, logout, setup, me)
- Created LoginPage component for user authentication
- Created AdministratorSetup component for initial admin setup
- Created ProtectedRoute wrapper for route authentication checking
- Updated API configuration to include Authorization headers
- Installed and configured react-router-dom for routing
- Updated App component with authentication routes
- All TypeScript checks passing

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore(style): prettier, typegen and add convenience targets to makefile

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>

* feat: Implement Phase 6 frontend UI updates - UserMenu and admin restrictions

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

docs: Add comprehensive testing and verification documentation for Phase 6

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

docs: Add Phase 6 summary document

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* feat: Add user management script for testing multiuser features

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* feat: Implement read-only model manager access for non-admin users

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

feat: Add admin authorization to model management API endpoints

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

docs: Update specification and implementation plan for read-only model manager

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Phase 7: Comprehensive testing and security validation for multiuser authentication (#23)

* Initial plan

* Phase 7: Complete test suite with 88 comprehensive tests

- Add password utils tests (31 tests): hashing, verification, validation
- Add token service tests (20 tests): JWT creation, verification, security
- Add security tests (13 tests): SQL injection, XSS, auth bypass prevention
- Add data isolation tests (11 tests): multi-user data separation
- Add performance tests (13 tests): benchmarks and scalability
- Add comprehensive testing documentation
- Add phase 7 verification report

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* bugfix(backend): Fix issues with authentication token expiration handling

- Remove time.sleep from token uniqueness test (use different expiration instead)
- Increase token expiration test time from 1 microsecond to 10 milliseconds
- More reliable test timing to prevent flakiness

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

Add Phase 7 summary documentation

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

Fix test_performance.py missing logger fixture

Add missing logger fixture to test_performance.py that was causing test failures.
The fixture creates a Logger instance needed by the user_service fixture.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

Add board isolation issue specification document

Document the three board isolation issues that need to be addressed:
1. Board list not updating when switching users
2. "Uncategorized" board shared among users
3. Admin cannot access all users' boards

Includes technical details, implementation plan, and acceptance criteria.
This document will be used to create a separate GitHub issue and PR.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

Fix test failures in Phase 7 tests

- Fix board service API calls to use enum values (BoardRecordOrderBy, SQLiteDirection) instead of strings
- Fix board deletion test to use get_dto() instead of non-existent get() method
- Add exception handling to verify_password() for invalid hash formats
- Update SQL injection test to accept both 401 and 422 status codes (Pydantic validation)

All fixes ensure tests match actual API signatures and handle edge cases properly.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

Fix token forgery test to properly decode and modify JWT payload

The test was attempting to modify the JWT payload by string replacement on the
base64-encoded data, which doesn't work since "false" doesn't appear literally
in the base64 encoding. Fixed to:
- Properly decode the base64 payload
- Parse the JSON
- Modify the is_admin field
- Re-encode the payload
- Create a forged token with the modified payload and original signature
- Verify it's rejected with 401 status

This properly tests that JWT signature verification prevents token forgery.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

Fix test_verify_expired_token timing issue

Increased token expiration from 10ms to 100ms and wait time from 20ms to 200ms
to make the test more reliable. The previous timing was too tight and could
cause race conditions where the token hadn't actually expired yet when verified.

This ensures the test reliably validates that expired tokens are properly rejected.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

Fix token verification to properly validate signatures and expiration

The verify_token() function was not explicitly enabling JWT verification options,
causing it to accept expired tokens and tokens with modified signatures/payloads.

Fixed by explicitly setting verification options:
- verify_signature: True (reject modified tokens)
- verify_exp: True (reject expired tokens)
- verify_nbf: True (verify "not before" claim)
- verify_iat: True (verify "issued at" claim)
- verify_aud: False (we don't use audience claims)

Also added catch-all exception handler for other validation errors.

This ensures all token security tests pass properly.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

Revert to default python-jose verification behavior

Removed explicit options parameter from jwt.decode() call since python-jose
verifies signatures and expiration by default. The explicit options may have
been causing issues or conflicts with the library's default behavior.

Added comments clarifying that python-jose defaults are secure and handle
verification properly. Need user to test and provide debugging output if
issues persist.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

Fix JWT expiration verification for python-jose 3.5.0

python-jose 3.5.0 has a bug where expiration verification doesn't work properly
by default. The jwt.decode() function is not rejecting expired tokens even when
they should be rejected.

Workaround implemented:
1. First, get unverified claims to extract the 'exp' timestamp
2. Manually check if current time >= exp time (token is expired)
3. Return None immediately if expired
4. Then verify signature with jwt.decode() for tokens that aren't expired

This ensures:
- Expired tokens are properly rejected
- Signature verification still happens for non-expired tokens
- Modified tokens are rejected due to signature mismatch

All three failing tests should now pass:
- test_verify_expired_token
- test_verify_token_with_modified_payload
- test_token_signature_verification

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix race condition in token verification - verify signature before expiration

Changed the order of verification in verify_token():
1. First verify signature with jwt.decode() - rejects modified/forged tokens
2. Then manually check expiration timestamp

Previous implementation checked expiration first using get_unverified_claims(),
which could cause a race condition where:
- Token with valid payload but INVALID signature would pass expiration check
- If expiration check happened to return None due to timing, signature was never verified
- Modified tokens could be accepted intermittently

New implementation ensures signature is ALWAYS verified first, preventing any
modified tokens from being accepted, while still working around the python-jose
3.5.0 expiration bug by manually checking expiration after signature verification.

This eliminates the non-deterministic test failures in test_verify_token_with_modified_payload.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore(app): ruff

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>

* Backend: Add admin board filtering and uncategorized board isolation

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix intermittent token service test failures caused by Base64 padding (#32)

* Initial plan

* Fix intermittent token service test failures due to Base64 padding

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Address code review: add constants for magic numbers in tests

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore(tests): ruff

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>

* Implement user isolation for session queue and socket events (WIP - debugging queue visibility) (#30)

* Add user isolation for queue events and field values filtering

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Add user column to queue list UI

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Add field values privacy indicator and implementation documentation

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Allow all users to see queue item status events while keeping invocation events private

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore(backend): ruff

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>

* Fix Queue tab not updating for other users in real-time (#34)

* Initial plan

* Add SessionQueueItemIdList invalidation to queue socket events

This ensures the queue item list updates in real-time for all users when
queue events occur (status changes, batch enqueued, queue cleared).

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Add SessionQueueItemIdList invalidation to queue_items_retried event

Ensures queue list updates when items are retried.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Improve queue_items_retried event and mutation invalidation

- Add individual item invalidation to queue_items_retried event handler
- Add SessionQueueStatus and BatchStatus tags to retryItemsById mutation
- Ensure consistency between event handler and mutation invalidation patterns

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Add privacy check for batch field values in Queue tab

Displays "Hidden for privacy" message for non-admin users viewing
queue items they don't own, instead of showing the actual field values.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* i18n(frontend): change wording of queue values suppressed message

* Add SessionQueueItemIdList cache invalidation to queue events

Ensures real-time queue updates for all users by invalidating the
SessionQueueItemIdList cache tag when queue events occur.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>

* Fix multiuser information leakage in Queue panel detail view (#38)

* Initial plan

* Implement multiuser queue information leakage fix

- Backend: Update sanitize_queue_item_for_user to clear session graph and workflow
- Frontend: Add permission check to disable detail view for unauthorized users
- Add test for sanitization logic
- Add translation key for permission denied message

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix prettier formatting for QueueItemComponent

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Address code review feedback

- Move Graph and GraphExecutionState imports to top of file
- Remove dependency on test_nodes in sanitization test
- Create minimal test invocation directly in test file

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Address additional code review feedback

- Create shallow copy to avoid mutating original queue_item
- Extract 'system' user_id to constant (SYSTEM_USER_ID)
- Add constant to both backend and frontend for consistency

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix pydantic validation error in test fixture

Add required timestamp fields (created_at, updated_at, started_at, completed_at) to SessionQueueItem in test fixture

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* fix(queue): Enforce user permissions for queue operations in multiuser mode (#36)

* Initial plan

* Add backend authorization checks for queue operations

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix linting issues in authorization changes

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Add frontend authorization checks for queue operations

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Add access denied messages for cancel and clear operations

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix access denied messages for all cancel/delete operations

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix merge conflict duplicates in QueueItemComponent

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore(frontend): typegen

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>

* fix(multiuser): Isolate client state per user to prevent data leakage (#40)

* Implement per-user client state storage to fix multiuser leakage

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix: Make authentication optional for client_state endpoints to support single-user mode

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Clear params state on logout/login to prevent user data leakage

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* feat(queue): show user/total pending jobs in multiuser mode badge (#43)

* Initial plan

* Add multiuser queue badge support - show X/Y format in multiuser mode

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Format openapi.json with Prettier

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Address code review feedback - optimize DB queries and improve code clarity

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* translationBot(ui): update translation files (#8767)

Updated by "Cleanup translation files" hook in Weblate.


Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* Limit automated issue closure to bug issues only (#8776)

* Initial plan

* Add only-labels parameter to limit automated issue closure to bugs only

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* fix(multiuser): Isolate client state per user to prevent data leakage (#40)

* Implement per-user client state storage to fix multiuser leakage

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix: Make authentication optional for client_state endpoints to support single-user mode

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Clear params state on logout/login to prevent user data leakage

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Initial plan

* chore(backend) ruff & typegen

* Fix real-time badge updates by invalidating SessionQueueStatus on queue events

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Weblate (bot) <hosted@weblate.org>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>

* Convert session queue isolation logs from info to debug level

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Add JWT secret storage in database and app_settings service

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Add multiuser configuration option with default false

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Update token service tests to initialize JWT secret

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix app_settings_service to use proper database transaction pattern

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore(backend): typegen and ruff

* chore(docs): update docstrings

* Fix frontend to bypass authentication in single-user mode

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix auth tests to enable multiuser mode

Auth tests were failing because the login and setup endpoints now return 403 when multiuser mode is disabled (the default). Updated test fixtures to enable multiuser mode for all auth-related tests.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix model manager UI visibility in single-user mode

Model manager UI for adding, deleting and modifying models is now:
- Visible in single-user mode (multiuser: false, the default)
- Hidden in multiuser mode for non-admin users
- Visible in multiuser mode for admin users

Created useIsModelManagerEnabled hook that checks multiuser_enabled status
and returns true when multiuser is disabled OR when user is admin.

Updated all model manager components to use this hook instead of direct
is_admin checks.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore(backend): ruff

* chore(frontend): typegen

* Fix TypeScript lint errors

- Added multiuser_enabled field to SetupStatusResponse type in auth.ts
- Removed unused user variable reference in MainModelDefaultSettings.tsx

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix test_data_isolation to enable multiuser mode

Added fixture to enable multiuser mode for data isolation tests, similar to other auth tests.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Redirect login and setup pages to app in single-user mode

When multiuser mode is disabled, the LoginPage and AdministratorSetup components now redirect to /app instead of showing the login/setup forms. This prevents users from being stuck on the login page after browser refresh in single-user mode.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix test_auth.py to initialize JWT secret

Added setup_jwt_secret fixture to test_auth.py to initialize the JWT secret before running auth tests. This fixture was missing, causing token creation/verification to fail in auth router tests.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Prevent login form flash in single-user mode

Show loading spinner instead of login/setup forms when multiuser mode is disabled or when redirecting is about to happen. This prevents the unattractive flash of the login dialog when refreshing the page in single-user mode.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix board and queue operations in single-user mode

Changed boards, session_queue, and images routers to use CurrentUserOrDefault instead of CurrentUser. This allows these endpoints to work without authentication when multiuser mode is disabled (default), fixing the issue where users couldn't create boards or add jobs to the queue in single-user mode.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Add user management utilities and rename add_user.py

Created three user management scripts in the scripts/ directory:
- useradd.py (renamed from add_user.py) - add users with admin privileges
- userdel.py - delete users by email address with confirmation
- usermod.py - modify user details (name, password, admin status)

All scripts support both CLI and interactive modes for flexibility.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix ESLint errors in frontend code

- Fixed brace-style issue in App.tsx (else-if on same line)
- Removed unused useAppSelector imports from model manager components
- Fixed import sorting in ControlAdapterModelDefaultSettings.tsx

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Add userlist.py script for viewing database users

Created scripts/userlist.py to display all users in the database. Supports:
- Table format (default): Shows ID, email, display name, admin status, and active status
- JSON format (--json flag): Outputs user data as JSON for scripting/automation

Example usage:
  python scripts/userlist.py          # Table view
  python scripts/userlist.py --json   # JSON output

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix test_boards_multiuser.py test failures

Fixed test failures caused by ApiDependencies.invoker not being set properly:
- Added setup_jwt_secret fixture to initialize JWT secret for token generation
- Added enable_multiuser_for_tests fixture that sets ApiDependencies.invoker as a class attribute
- Updated tests to use enable_multiuser_for_tests fixture to ensure ApiDependencies is properly configured
- Removed MockApiDependencies class approach in favor of directly setting the class attribute

This fixes the AttributeError and ensures all tests have the proper setup.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore(backend): ruff

* Fix userlist.py SqliteDatabase initialization

Fixed AttributeError in userlist.py where SqliteDatabase was being passed the config object instead of config.db_path. The constructor expects a Path object (db_path) as the first argument, not the entire config object.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix test_boards_multiuser.py by adding app_settings service to mock

Added AppSettingsService initialization to the mock_services fixture in tests/conftest.py. The test was failing because setup_jwt_secret fixture expected mock_invoker.services.app_settings to exist, but it wasn't being initialized in the mock services.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* bugfix(scripts): fix crash in userlist.py script

* Fix test_boards_multiuser.py JWT secret initialization

Fixed setup_jwt_secret fixture to call set_jwt_secret() directly instead of trying to access non-existent app_settings service. Removed incorrect app_settings parameter from InvocationServices initialization in tests/conftest.py since app_settings is not an attribute of InvocationServices.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix CurrentUserOrDefault to require auth in multiuser mode

Changed get_current_user_or_default to raise HTTP 401 when multiuser mode is enabled and credentials are missing, invalid, or the user is inactive. This ensures that board/queue/image operations require authentication in multiuser mode while still working without authentication in single-user mode (default).

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore(front & backend): ruff and lint

* Add AdminUserOrDefault and fix model settings in single-user mode

Created AdminUserOrDefault dependency that allows admin operations to work without authentication in single-user mode while requiring admin privileges in multiuser mode. Updated model_manager router to use AdminUserOrDefault for update_model_record, update_model_image, and reidentify_model endpoints. This fixes the "Missing authentication credentials" error when saving model default settings in single-user mode.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix model manager operations in single-user mode

Changed all model manager endpoints from AdminUser to AdminUserOrDefault to allow model installation, deletion, conversion, and cache management operations to work without authentication in single-user mode. This fixes the issue where users couldn't add or delete models in single-user mode.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix syntax error in model_manager.py

Added Depends(AdminUserOrDefault) to all AdminUserOrDefault dependency parameters to fix Python syntax error where parameters without defaults were following parameters with defaults. Imported Depends from fastapi.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix FastAPI dependency injection syntax error

Removed type annotations from AdminUserOrDefault dependency parameters. FastAPI doesn't allow both Annotated type hints and = Depends() default values together. Changed from `_: AdminUserOrDefault = Depends(AdminUserOrDefault)` to `_ = Depends(AdminUserOrDefault)` throughout model_manager.py.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix delete_model endpoint parameter annotation

Changed delete_model endpoint to use Annotated[str, Path(...)] instead of str = Path(...) to match FastAPI's preferred syntax and fix the 422 Unprocessable Entity error when deleting models in single-user mode.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix parameter annotations for all AdminUserOrDefault endpoints

Changed all endpoints using AdminUserOrDefault from old syntax (key: str = Path(...)) to FastAPI's preferred Annotated syntax (key: Annotated[str, Path(...)]). This fixes 422 Unprocessable Entity errors when updating model settings and deleting models in single-user mode. Updated endpoints: delete_model_image, install_model, install_hugging_face_model, and convert_model.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Revert to correct AdminUserOrDefault usage pattern

Reverted model_manager.py to state before commit c47af8f and reapplied AdminUserOrDefault correctly. Changed from `_: AdminUser` to `current_admin: AdminUserOrDefault` using the same pattern as boards.py (`current_user: CurrentUserOrDefault`). This fixes all 422 errors in single-user mode while maintaining proper admin authentication in multiuser mode.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix parameter order for AdminUserOrDefault in model manager

Moved current_admin: AdminUserOrDefault parameter before all parameters with default values in model_manager.py endpoints. Python requires parameters without defaults to come before parameters with defaults. Fixed 8 endpoints: delete_model, bulk_delete_models, delete_model_image, install_model, install_hugging_face_model, cancel_model_install_job, convert_model, and do_hf_login.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore(frontend): typegen

* chore(frontend): typegen again

* Docs(app): Comprehensive Documentation of Multiuser Features (#50)

* Implement Phase 8: Complete multiuser documentation (user, admin, and API guides)

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Update multiuser documentation for single-user mode and CLI scripts

- Document multiuser config option (true/false/absent)
- Explain single-user mode behavior (no login required)
- Document mode switching and legacy "system" user
- Update user management to reference CLI scripts (useradd, userdel, usermod, userlist)
- Note that web UI for user management is coming in future release
- Add adaptive API client example for both modes

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* docs(multiuser): bring user guide documentation up to date

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* docs(app): update multiuser documentation

* bugfix(app): fix misaligned database migration calls

* chore(tests): update migration test to accommodate resequencing of migrations

* fix(frontend): prevent caching of static pages

* chore(backend): ruff

* fix(backend): fix incorrect migration import

* Fix: Admin users can see image previews from other users' generations (#61)

* Initial plan

* Fix: strip image preview from InvocationProgressEvent sent to admin room

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore: ruff

* fix(backend): add migration_29 file

* chore(tests): fix migration_29 test

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>

* fix(queue): System user queue items show blank instead of `<hidden>` for non-admin users (#63)

* Initial plan

* fix(queue): System user queue items show blank instead of `<hidden>` for non-admin users

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore(backend): ruff

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>

* Hide "Use Cache" checkbox in node editor for non-admin users in multiuser mode (#65)

* Initial plan

* Hide use cache checkbox for non-admin users in multiuser mode

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix node loading hang when invoke URL ends with /app (#67)

* Initial plan

* Fix node loading hang when URL ends with /app

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Move user management scripts to installable module with CLI entry points (#69)

* Initial plan

* Add user management module with invoke-useradd/userdel/userlist/usermod entry points

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore(util): remove superceded user administration scripts

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>

* chore(backend): reorganized migrations, but something still broken

* Fix migration 28 crash when `client_state.data` column is absent (#70)

* Initial plan

* Fix migration 28 to handle missing data column in client_state table

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Consolidate multiuser DB migrations 27–29 into a single migration step (#71)

* Initial plan

* Consolidate migrations 27, 28, and 29 into a single migration step

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Add `--root` option to user management CLI utilities (#81)

* Initial plan

* Add --root option to user management CLI utilities

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix queue clear() endpoint to respect user_id for multi-tenancy (#75)

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

Add tests for session queue clear() user_id scoping

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

chore(frontend): rebuild typegen

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>

* fix: use AdminUserOrDefault for pause and resume queue endpoints (#77)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* fix: queue pause/resume buttons disabled in single-user mode (#83)

In single-user mode, currentUser is never populated (no auth), so
`currentUser?.is_admin ?? false` always returns false, disabling the buttons.

Follow the same pattern as useIsModelManagerEnabled: treat as admin
when multiuser mode is disabled, and check is_admin flag when enabled.

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* fix: enforce board ownership checks in multiuser mode (#84)

- get_board: verify current user owns the board (or is admin), return 403 otherwise
- update_board: verify ownership before updating, 404 if not found, 403 if unauthorized
- delete_board: verify ownership before deleting, 404 if not found, 403 if unauthorized
- list_all_board_image_names: add CurrentUserOrDefault auth and ownership check for non-'none' board IDs



test: add ownership enforcement tests for board endpoints in multiuser mode

- Auth requirement tests for get, update, delete, and list_image_names
- Cross-user 403 forbidden tests (non-owner cannot access/modify/delete)
- Admin bypass tests (admin can access/update/delete any user's board)
- Board listing isolation test (users only see their own boards)
- Refactored fixtures to use monkeypatch (consistent with other test files)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix: Clear auth state when switching from multiuser to single-user mode (#86)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix race conditions in download queue and model install service (#98)

* Initial plan

* Fix race conditions in download queue and model install service

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Weblate (bot) <hosted@weblate.org>
Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com>
2026-02-26 23:47:25 -05:00
DustyShoe
b90969ee88 Fix(Text-tool): Remove redundant Font tooltip on fonts selection dropdown. (#8906) 2026-02-27 03:01:08 +00:00
Lincoln Stein
1bbcfa1ac2 Merge branch 'main' into feat/model-manager-queue-redesign 2026-02-26 21:23:19 -05:00
Lincoln Stein
dfc66b7142 Feature: Add FLUX.2 LOKR model support (detection and loading) (#8909)
* Add FLUX.2 LOKR model support (detection and loading) (#88)

Fix BFL LOKR models being misidentified as AIToolkit format



Fix alpha key warning in LOKR QKV split layers

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix BFL→diffusers key mapping for non-block layers in FLUX.2 LoRA/LoKR

BFL's FLUX.2 model uses different names than diffusers' Flux2Transformer2DModel
for top-level modules (embedders, modulations, output layers). The existing
conversion only handled block-level renames (double_blocks→transformer_blocks),
causing "Failed to find module" warnings for non-block LoRA keys like img_in,
txt_in, modulation.lin, time_in, and final_layer.

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Alexander Eichhorn <alex@eichhorn.dev>
2026-02-27 00:45:13 +00:00
Lincoln Stein
21efa70b4d chore(CI/CD): add pfannkuchensack to codeowners for backend (#8915) 2026-02-25 21:30:49 -05:00
joshistoast
306fdf3882 feat(model manager queue): remove backend status badge 2026-02-25 12:28:54 -07:00
joshistoast
30e8cbd4b2 fix(model manager queue): play/pause button condition 2026-02-25 12:20:51 -07:00
joshistoast
7dd18ad1cb fix(model manager queue): add missing imports
dammit zed editor
2026-02-24 18:07:34 -07:00
joshistoast
33e1a1e39a feat(model manager queue): improve ui/ux
- standardized table row widths
- sticky table header
- reverse table data direction (new items on top)
- queue empty state
- ui and icon tweaks
- add progress tooltip
- add code comments for sanity
2026-02-24 18:07:34 -07:00
joshistoast
2b61addb51 feat(model manager): redesign queue 2026-02-24 18:07:34 -07:00
Weblate (bot)
7aa3c95767 ui: translations update from weblate (#8905)
* translationBot(ui): update translation (Italian)

Currently translated at 98.0% (2205 of 2250 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 97.8% (2210 of 2259 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.8% (2224 of 2272 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 98.1% (2252 of 2295 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 98.0% (2264 of 2309 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
2026-02-24 18:40:25 -05:00
Alexander Eichhorn
afbd45ace7 Feature: flux2 klein lora support (#8862)
* WIP: Add FLUX.2 Klein LoRA support (BFL PEFT format)

Initial implementation for loading and applying LoRA models trained
with BFL's PEFT format for FLUX.2 Klein transformers.

Changes:
- Add LoRA_Diffusers_Flux2_Config and LoRA_LyCORIS_Flux2_Config
- Add BflPeft format to FluxLoRAFormat taxonomy
- Add flux_bfl_peft_lora_conversion_utils for weight conversion
- Add Flux2KleinLoraLoaderInvocation node

Status: Work in progress - not yet fully tested

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(flux2): add LoRA support for FLUX.2 Klein models

Add BFL PEFT LoRA support for FLUX.2 Klein, including runtime conversion
of BFL-format keys to diffusers format with fused QKV splitting, improved
detection of Klein 4B LoRAs via MLP ratio check, and frontend graph wiring.

* feat(flux2): detect Klein LoRA variant (4B/9B) and filter by compatibility

Auto-detect FLUX.2 Klein LoRA variant from tensor dimensions during model
probe, warn on variant mismatch at load time, and filter the LoRA picker
to only show variant-compatible LoRAs.

* Chore Ruff

* Chore pnpm

* Fix detection and loading of 3 unrecognized Flux.2 Klein LoRA formats

Three Flux.2 Klein LoRAs were either unrecognized or misclassified due to
format detection gaps:

1. PEFT-wrapped BFL format (base_model.model.* prefix) was not recognized
   because the detector only accepted the diffusion_model.* prefix.
2. Klein 4B LoRAs with hidden_size=3072 were misidentified as Flux.1 due to
   a break statement exiting the detection loop before txt_in/vector_in
   dimensions could be checked.
3. Flux2 native diffusers format (to_qkv_mlp_proj, ff.linear_in) was not
   detected because the detector only checked for Flux.1 diffusers keys.

Also handles mixed PEFT/standard LoRA suffix formats within the same file.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-02-24 02:55:39 +00:00
DustyShoe
b9f9015214 Feat(Model Manager): Add improved download manager with pause/resume partial download. (#8864)
* Refine messaging and pause behavior

* Improved resume download behavior

* Syntax fix

* Formatting

* Improved partial download recovering

* fix(downloads): resume integrity, serialized parts, and UI feedback

* Fix download test expectations and multifile totals

* Ruff  appease

* schema updates

* schema fix

* Added toast msg if partial file was deleted.

* Formatting

* Fixed "missing temp file" message pop up

* Update invokeai/app/services/download/download_default.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fix: Add bulk action buttons and force resync on backend reconnect.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-02-24 02:31:56 +00:00
Harikrishna KP
ddaa12b0fd Fix bare except clauses and mutable default arguments (#8871)
* Fix bare except clauses and mutable default arguments

Replace bare `except:` with `except Exception:` in sqlite_database.py
and mlsd/utils.py to avoid catching KeyboardInterrupt and SystemExit,
which can prevent graceful shutdowns and mask critical errors (PEP 8
E722).

Replace mutable default arguments (lists) with None in
imwatermark/vendor.py to prevent shared state between calls, which
is a known Python gotcha that can cause subtle bugs when default
mutable objects are modified in place.

* add tests for mutable defaults and bare except fixes

* Simplify exception propagation tests

* Remove unused db initialization in error propagation tests

Removed unused database initialization in tests for KeyboardInterrupt and SystemExit.

---------

Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-02-22 23:25:15 -05:00
DustyShoe
c8dfea8681 Fix: Improve non square bbox coverage for linear gradient tool. (#8889)
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-02-21 15:18:15 +00:00
John Hendrikx
1730193883 Fix Create Board API call (#8866)
Remove 5th parameter for function that expects 4 parameters

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-02-21 15:13:18 +00:00
Copilot
33c7b2a1f9 Fix: canvas text tool broke global hotkeys (#8887)
* Initial plan

* Fix canvas text tool breaking hotkeys when canvas manager is null

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore(frontend): fix eslint issue

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-02-19 23:07:11 -05:00
DustyShoe
033ff77f94 Feature (UI): Add Invert button for Regional Guidance layers (#8876)
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-02-20 02:41:17 +00:00
Weblate (bot)
89df130ca1 ui: translations update from weblate (#8881)
* translationBot(ui): update translation (Italian)

Currently translated at 98.0% (2205 of 2250 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 97.8% (2210 of 2259 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.8% (2224 of 2272 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 98.1% (2252 of 2295 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
2026-02-19 20:49:12 -05:00
DustyShoe
e9246c1899 Feature(UI): Add text tool to canvas (#8723)
* Initial mashup of mentioned feature. Still need to resolve some quirks and kinks.

* Clean text tool integration

* Fixed text tool opions bar jumping and added more fonts

* Touch up for cursor styling

* Minor addition to doc file

* Appeasing frontend checks

* Prettier fix

* knip fixes

* Added safe zones to font selection and color picker to be clickable without commiting text.

* Removed color probing on cursor and added dynamic font display for fallback, minor tweaks

* Finally fixed the text shifting on commit

* Cursor now represent actual input field size. Tidy up options UI

* Some strikethrough and underline line tweaks

* Replaced the focus retry loop with a callback‑ref based approach in in CanvasTextOverlay.tsx
Renamed containerMetrics to textContainerData in CanvasTextOverlay.tsx
Fixed mouse cursor disapearing during typing.

* Added missing localistaion string

* Moved canvas-text-tool.md to docs/contributing/frontend

* ui: Improve functionality of the text toolbar

Few things done with this commit.

- The varying size of the font selector box has been fixed. The UI no longer shifts and moves with font change.
- We no longer format the font size input to add px each time. Instead now just have a permanent px indicator.
- The bug with the random text inputs on the slider value has also been fixed.
- The font size value is only committed on blur keeping it consistent with other editing apps.
- Fixed the spacing of the toolbar to make it look cleaner.
- Font size now permits increments of 1.

* Added autoselect text in font size on click allowing immediate imput

* Improvement: Added uncommited layer state with CTRL-move and options to select line spacing.

* Added rotation handle to rotate uncommiitted text layer.

* Fix: Redirect user facing labels to use localization file + Add tool discription to docs

* Fixed box padding. Disable tool swich when text input is active, added message on canvas for better UX.

* Updated  Text tool description

* Updated  Text tool description

* Typo

* Add draggable text-box border with improved cursor feedback and larger hit targets. Supress hotkeys on uncommitted text.

* Lint

* Fix(bug): text commit to link uploaded image assets instead of embedding full base64

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
Co-authored-by: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com>
2026-02-20 01:43:32 +00:00
Alexander Eichhorn
b0f7b555b7 feat(z-image): add Z-Image Base (undistilled) model variant support (#8799)
* feat(z-image): add Z-Image Base (undistilled) model variant support

- Add ZImageVariantType enum with 'turbo' and 'zbase' variants
- Auto-detect variant on import via scheduler_config.json shift value (3.0=turbo, 6.0=zbase)
- Add database migration to populate variant field for existing Z-Image models
- Re-add LCM scheduler with variant-aware filtering (LCM hidden for zbase)
- Auto-reset scheduler to Euler when switching to zbase model if LCM selected
- Update frontend to show/hide LCM option based on model variant
- Add toast notification when scheduler is auto-reset

Z-Image Base models are undistilled and require more steps (28-50) with higher
guidance (3.0-5.0), while Z-Image Turbo is distilled for ~8 steps with CFG 1.0.
LCM scheduler only works with distilled (Turbo) models.

* Chore ruff format

* Chore fix windows path

* feat(z-image): filter LoRAs by variant compatibility and warn on mismatch

LoRA picker now hides Z-Image LoRAs with incompatible variants (e.g. ZBase
LoRAs when using Turbo model). LoRAs without a variant are always shown.
Backend loaders warn at runtime if a LoRA variant doesn't match the
transformer variant.

* Chore typegen

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-02-20 00:32:38 +00:00
Alexander Eichhorn
467ae66a87 fix(flux2): apply BN normalization to latents for inpainting (#8868)
The FLUX.2 Klein transformer operates in BN-normalized latent space,
but init_latents from VAE encode were not being normalized before
being passed to the InpaintExtension. This caused a scale mismatch
when merging intermediate_latents (normalized) with noised_init_latents
(unnormalized), resulting in visible artifacts at mask blur boundaries.

Now normalize:
- init_latents_packed before passing to InpaintExtension
- noise_packed for correct interpolation in normalized space
- x (starting latents) for img2img/inpainting workflows

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com>
2026-02-19 19:25:47 -05:00
Lincoln Stein
848cc12d63 Feature(backend): Add a command-line utility for running gallery maintenance (#8827)
* (bugfix) Add a command-line utility for running gallery maintenance

* chore(backend): ruff
2026-02-16 23:44:19 +00:00
dunkeroni
dbb20a011a Feature: Canvas Blend and Boolean modes (#8661)
* feat(canvas): add raster layer blend modes and boolean operations submenu; support per-layer globalCompositeOperation in compositor; UI to toggle and select color blend modes (multiply, screen, darken, lighten, color-dodge, color-burn, hard-light, soft-light, difference, hue, saturation, color, luminosity).

* feat(canvas): boolean ops submenu and UI polish

* (chore): prettier lint

* add icons to boolean submenu items

* add delete button for color blend operations

* move composite operation type and imports

* chore: pnpm eslint

* update blend modes order

* update default blend mode to 'color'

* add i18n for blend modes

* actually use translations for blend modes now

* move composite options into types.ts

* cleanup and comments

* update names

* move constant mapping out of function

* feat(ui): Refactor Blend Mode Implementation

- Blend Modes are not right click menu options anymore. Instead they rest above the layer panel as they do in other art programs readily available for each layer.
- Blend Modes have been resorted to match the listings of other art programs so users can avail their muscle memory.
- Blend Mode now defaults to `Normal` for each layer as it should.
- The extra layer operations have now been moved down to the `Operations Bar` at the bottom of the layer stack. This is to increase familiarity again with other art programs and also to make space for us in the top action bar.
- The Operations Bars operations have been resorted in order of usage that makes sense.

* fix: use source-over instead of normal

* fix: pixel fix for slightly offset action bar labels.

* feat(canvas): boolean raster merge creates new layer and disables sources

* (fix) lint errors

* remove extra typecast

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
Co-authored-by: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com>
2026-02-16 23:31:55 +00:00
Lincoln Stein
3ada1dc743 Feature(app): Add an endpoint to recall generation parameters (#8758)
* feature(app): Add an endpoint to recall generation parameters and transmit to frontend

-core generation parameters
-support for LoRAs and IP-adapters
-controlnets
-documentation in docs/contributing/RECALL_PARAMETERS

* chore(app): refactor controlnet image processing in recall_parameters route

* docs(app): updated recall endpoint documentation

* chore(app): ruff format

* chore(frontend): rerun typegen

---------

Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com>
2026-02-16 23:27:10 +00:00
Weblate (bot)
0fb2ae4fae ui: translations update from weblate (#8878)
* translationBot(ui): update translation (Italian)

Currently translated at 98.0% (2205 of 2250 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 97.8% (2210 of 2259 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Italian)

Currently translated at 97.8% (2224 of 2272 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
2026-02-16 18:20:59 -05:00
girlyoulookthebest
ec2eedb000 fix(flux2): resolve device mismatch in Klein text encoder (#8851)
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-02-07 15:48:14 -05:00
Weblate (bot)
77e1ac19fc ui: translations update from weblate (#8856)
* translationBot(ui): update translation (Italian)

Currently translated at 98.0% (2205 of 2250 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 97.8% (2210 of 2259 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
2026-02-06 17:48:12 -05:00
Lincoln Stein
b23f18734b feat(model_manager): Add scan and delete of orphaned models (#8826)
* Add script and UI to remove orphaned model files

- This commit adds command-line and Web GUI functionality for
  identifying and optionally removing models in the models directory
  that are not referenced in the database.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Add backend service and API routes for orphaned models sync

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

Add expandable file list to orphaned models dialog

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* Fix cache invalidation after deleting orphaned models

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* (bugfix) improve status messages

* docs(backend): add info on the orphaned model detection/removal feature

* Update docs/features/orphaned_model_removal.md

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: dunkeroni <dunkeroni@gmail.com>
2026-02-06 22:32:10 +00:00
Lincoln Stein
8c3cc3a970 chore(CI/CD): bump version to 6.11.1.post1 (#8852) 2026-02-06 15:24:00 -05:00
Jonathan
86eff471fd Update presets.py (#8846) 2026-02-06 17:14:47 +05:30
Alexander Eichhorn
a42fdb0f44 fix(flux2): Fix FLUX.2 Klein image generation quality (#8838)
* fix(flux2): Fix image quality degradation at resolutions > 1024x1024

This commit addresses severe quality degradation and artifacts when
generating images larger than 1024x1024 with FLUX.2 Klein models.

Root causes fixed:

1. Dynamic max_image_seq_len in scheduler (flux2_denoise.py)
   - Previously hardcoded to 4096 (1024x1024 only)
   - Now dynamically calculated based on actual resolution
   - Allows proper schedule shifting at all resolutions

2. Smoothed mu calculation discontinuity (sampling_utils.py)
   - Eliminated 40-50% mu value drop at seq_len 4300 threshold
   - Implemented smooth cosine interpolation (4096-4500 transition zone)
   - Gradual blend between low-res and high-res formulas

Impact:
- FLUX.2 Klein 9B: Major quality improvement at high resolutions
- FLUX.2 Klein 4B: Improved quality at high resolutions
- Baseline 1024x1024: Unchanged (no regression)
- All generation modes: T2I and Kontext (reference images)

Fixes: Community-reported quality degradation issue
See: Discord discussions in #garbage-bin and #devchat

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix(flux2): Fix high-resolution quality degradation for FLUX.2 Klein

  Fixes grid/diamond artifacts and color loss at resolutions > 1024x1024.

  Root causes identified and fixed:
  - BN normalization was incorrectly applied to random noise input
    (diffusers only normalizes image latents from VAE.encode)
  - BN denormalization must be applied to output before VAE decode
  - mu parameter was resolution-dependent causing over-shifted schedules
    at high resolutions (now fixed to 2.02, matching ComfyUI)

  Changes:
  - Remove BN normalization on noise input (not needed for N(0,1) noise)
  - Preserve BN denormalization on denoised output (required for VAE)
  - Fix mu to constant 2.02 for all resolutions (matches ComfyUI)

  Tested at 2048x2048 with FLUX.2 Klein 4B

* Chore Ruff

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com>
2026-02-06 00:34:54 -05:00
Jonathan
bacdfecb13 Add dype area option (#8844)
* Add DyPE area option

* Added tests and fixed frontend build

* Made more pythonic
2026-02-06 00:55:29 +05:30
Lincoln Stein
76b0838094 Feature(backend): Add user toggle to run encoder models on CPU (#8777)
* feature(backend) Add user toggle to run encoder models on CPU

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

Add frontend UI for CPU-only model execution toggle

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* chore(frontend): remove package lock file created by npm

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com>
2026-02-04 15:13:29 -05:00
DustyShoe
b7d7cd0748 Feat(UI): Add linear and radial gradient tools to canvas (#8774)
* Adding gradient tool to canvas. Lineara and radial.

* Formatting again...

* Formatting again 2...

* Minor bug fix

* Some button design tweaking

* Fixed icorrect wording where Circular was used instead of Radial.

* Update invokeai/frontend/web/src/features/controlLayers/konva/CanvasObject/CanvasObjectGradient.ts

Co-authored-by: dunkeroni <dunkeroni@gmail.com>

* Update invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolGradientButton.tsx

Co-authored-by: dunkeroni <dunkeroni@gmail.com>

* Update invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolGradientButton.tsx

Co-authored-by: dunkeroni <dunkeroni@gmail.com>

* Update invokeai/frontend/web/src/features/controlLayers/components/Tool/ToolGradientButton.tsx

Co-authored-by: dunkeroni <dunkeroni@gmail.com>

* Autocommit fix on mouse leaving canvas area

* feature(canvas): move gradient mode controls to top toolbar; remove popover mode buttons and group clip+mode cluster

* (chore) prettier

* remove fixed icon size

---------

Co-authored-by: dunkeroni <dunkeroni@gmail.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-02-03 20:20:59 +00:00
Weblate (bot)
d5c59ee64e ui: translations update from weblate (#8834)
* translationBot(ui): update translation (Italian)

Currently translated at 98.0% (2205 of 2250 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
2026-02-03 15:07:11 -05:00
Alexander Eichhorn
3f79159249 fix(ui): remove duplicate DyPE preset dropdown in generation settings (#8831)
The ParamFluxDypePreset component was rendered twice in the FLUX
generation settings accordion, causing the DyPE dropdown to appear
both after the scheduler and after the guidance slider.

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-02-02 02:16:29 +00:00
Weblate (bot)
c186e51b30 translationBot(ui): update translation (Russian) (#8830)
Currently translated at 59.7% (1344 of 2249 strings)


Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/
Translation: InvokeAI/Web UI

Co-authored-by: DustyShoe <warukeichi@gmail.com>
2026-02-01 20:04:47 -05:00
Alexander Eichhorn
c072fd8261 The FLUX.2 Klein PR (b92c6ae63) replaced the user's denoising strength (#8828)
setting with hardcoded full denoising (start=0, end=1) in addOutpaint.
   This caused denoising strength to be completely ignored whenever the
   canvas bbox extended beyond the raster layer content, triggering outpaint
   mode. The issue affected all model types (SDXL, SD1.5, FLUX, etc.).

   Restore the original behavior by reading denoising_start/end from the
   user's img2imgStrength setting via getDenoisingStartAndEnd().

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-02-02 00:13:42 +00:00
Alexander Eichhorn
f013fa6ff2 fix(ui): reset seed variance toggle when recalling images without that metadata (#8829)
When recalling an image that lacks `z_image_seed_variance_enabled` metadata
   (e.g. older images), the toggle now defaults to off instead of retaining the
   previous state.
2026-02-01 19:03:00 -05:00
DustyShoe
9566f9a23f Feat(UI): Reintroduce paged gallery view as option (#8772)
* Switched to use v5.x gallery pagination design.

* Improved pagination UX and gallery grid calculation

* Minor bug fix

* Formatting...

* Fixed Jump to page input behavior and "Locate in gallery" logic.

* Changed Jump input field to select text on click for better UX.
2026-02-01 21:37:53 +00:00
Alexander Eichhorn
62ee1b820d fix(ui): only show FLUX.1 VAEs when a FLUX.1 main model is selected (#8821)
Use useFlux1VAEModels() instead of useFluxVAEModels() in the FLUX VAE
selector, which was incorrectly returning both FLUX.1 and FLUX.2 VAEs.
Remove the now-unused useFluxVAEModels hook.

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-02-01 21:28:48 +00:00
Alexander Eichhorn
33779f3072 fix(flux2): support Heun scheduler for FLUX.2 Klein models (#8794)
* fix(flux2): support Heun scheduler for FLUX.2 Klein models

FlowMatchHeunDiscreteScheduler does not support dynamic shifting parameters
(use_dynamic_shifting, base_shift, max_shift, etc.) or sigmas/mu in set_timesteps.
This caused FLUX.2 Klein to fail when using Heun scheduler.

- Create Heun scheduler with only num_train_timesteps and shift parameters
- Use num_inference_steps instead of sigmas for Heun's set_timesteps call
- Euler and LCM schedulers continue to use full dynamic shifting support

* fix(flux2): fix Heun scheduler detection using inspect.signature

The previous hasattr check for state_in_first_order failed because
the attribute doesn't exist before set_timesteps() is called. Now
using inspect.signature to check for sigmas parameter support,
matching the FLUX1 implementation.

---------

Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com>
2026-02-01 16:25:39 -05:00
Jonathan
8cf83a9221 Implemented ordering for expanded iterators (#8741)
* Implemented ordering for expanded iterators

* Update test_graph_execution_state.py

Added a test for nested iterator execution ordering. (Failing at commit time!)

* Filter invalid nested-iterator parent mappings in _prepare()

When a graph has nested iterators, some "ready to run" node combinations do not actually belong together. Previously, the scheduler would still try to build nodes for those mismatched combinations, which could cause the same work to run more than once. This change skips any combination that is missing a valid iterator parent, so nested iterator expansions run once per intended item.

* Fixed Collect node ordering

* ruff

* Removed ordering guarantees from test_node_graph.py

* Fix iterator prep and type compatibility in graph execution

Include iterator nodes in nx_graph_flat so iterators are prepared/expanded correctly. Fix connection type checks to allow subclass-to-base via issubclass. Harden iterator/collector validation to fail cleanly instead of crashing on missing edges. Remove unused nx_graph_with_data(). Added tests to verify proper functionality.
2026-02-01 05:00:04 +00:00
Alexander Eichhorn
1281c9d211 feat(model_manager): add missing models filter to Model Manager (#8801)
* feat(model_manager): add missing models filter to Model Manager

Adds the ability to view and manage orphaned model database entries
where the underlying files have been deleted externally.

Changes:
- Add GET /v2/models/missing API endpoint to list models with missing files
- Add "Missing Files" filter option to Model Manager type filter dropdown
- Display "Missing Files" badge on models with missing files in the list
- Automatically exclude missing models from model selection dropdowns
  to prevent users from selecting unavailable models for generation

* fix(ui): enable Select All checkbox for missing models filter

The Select All checkbox was disabled when the missing models filter was
active because the bulk actions component didn't use the missing models
query data. Now it correctly uses useGetMissingModelsQuery when the
filter is set to 'missing'.

* test(model_manager): add tests for missing model detection and bulk delete

Tests _scan_for_missing_models and the unregister/delete workflow for
models whose files have been removed externally.

* Chore Ruff check
2026-02-01 04:51:33 +00:00
Lincoln Stein
4a09594230 chore(CI/CD): bump version to 6.11.0.post1 (#8818) 2026-01-31 23:46:07 -05:00
Weblate (bot)
a873ce0175 ui: translations update from weblate (#8816)
* translationBot(ui): update translation (Italian)

Currently translated at 95.0% (2124 of 2235 strings)

translationBot(ui): update translation (Italian)

Currently translated at 94.5% (2114 of 2235 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 98.2% (2195 of 2235 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 98.2% (2197 of 2235 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Russian)

Currently translated at 60.0% (1341 of 2235 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Co-authored-by: DustyShoe <warukeichi@gmail.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-31 22:18:09 +00:00
Alexander Eichhorn
9ee7baaba5 fix(ui): convert reference image configs when switching main model base (#8811)
When switching between FLUX.2 (model-less reference images) and other
models that require IP adapter/Redux models, the reference image configs
were not being converted, leaving stale config types that hid or showed
the wrong UI controls.

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-31 22:04:23 +00:00
Weblate (bot)
fb5c43a905 ui: translations update from weblate (#8814)
* translationBot(ui): update translation (Italian)

Currently translated at 95.0% (2124 of 2235 strings)

translationBot(ui): update translation (Italian)

Currently translated at 94.5% (2114 of 2235 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 98.2% (2195 of 2235 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 98.2% (2197 of 2235 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Russian)

Currently translated at 60.0% (1341 of 2235 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Co-authored-by: DustyShoe <warukeichi@gmail.com>
2026-01-31 17:03:47 -05:00
Weblate (bot)
0f69f4bb9a ui: translations update from weblate (#8813)
* translationBot(ui): update translation (Italian)

Currently translated at 95.0% (2124 of 2235 strings)

translationBot(ui): update translation (Italian)

Currently translated at 94.5% (2114 of 2235 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 98.2% (2195 of 2235 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 98.2% (2197 of 2235 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Russian)

Currently translated at 60.0% (1341 of 2235 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Co-authored-by: DustyShoe <warukeichi@gmail.com>
2026-01-31 16:41:12 -05:00
Weblate (bot)
8a355e66fa ui: translations update from weblate (#8812)
* translationBot(ui): update translation (Italian)

Currently translated at 95.0% (2124 of 2235 strings)

translationBot(ui): update translation (Italian)

Currently translated at 94.5% (2114 of 2235 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 98.2% (2195 of 2235 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 98.2% (2197 of 2235 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

* translationBot(ui): update translation (Russian)

Currently translated at 60.0% (1341 of 2235 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Co-authored-by: DustyShoe <warukeichi@gmail.com>
2026-01-31 08:52:27 -05:00
blessedcoolant
b811602b38 fix(ui): Flux 2 Model Manager default settings not showing Guidance (#8810) 2026-01-31 13:41:05 +00:00
DustyShoe
0716b2fa75 Fix blur filter clipping by expanding padded bounds (#8773)
Co-authored-by: Alexander Eichhorn <alex@eichhorn.dev>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-30 20:56:51 +00:00
Alexander Eichhorn
4d71609115 fix(ui): remove scheduler selection for FLUX.2 Klein (#8808)
The scheduler dropdown is no longer shown for FLUX.2 Klein models.
The backend default (Euler) is used instead.

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-30 02:16:12 +00:00
blessedcoolant
0ecb903ae2 fix: Klein 2 Inpainting breaking when there is a reference image (#8803)
Co-authored-by: Alexander Eichhorn <alex@eichhorn.dev>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-30 02:12:41 +00:00
Alexander Eichhorn
736f4ffeb1 fix(ui): improve DyPE field ordering and add 'On' preset option (#8793)
* fix(ui): improve DyPE field ordering and add 'On' preset option

- Add ui_order to DyPE fields (100, 101, 102) to group them at bottom of node
- Change DyPEPreset from Enum to Literal type for proper frontend dropdown support
- Add ui_choice_labels for human-readable dropdown options
- Add new 'On' preset to enable DyPE regardless of resolution
- Fix frontend input field sorting to respect ui_order (unordered first, then ordered)
- Bump flux_denoise node version to 4.4.0

* Chore Ruff check fix

* fix(flux): remove .value from dype_preset logging

DyPEPreset is now a Literal type (string) instead of an Enum,
so .value is no longer needed.

* fix(tests): update DyPE tests for Literal type change

Update test imports and assertions to use string constants
instead of Enum attributes since DyPEPreset is now a Literal type.

* feat(flux): add DyPE scale and exponent controls to Linear UI

- Add dype_scale (λs) and dype_exponent (λt) sliders to generation settings
- Add Zod schemas and parameter types for DyPE scale/exponent
- Pass custom values from Linear UI to flux_denoise node
- Fix bug where DyPE was enabled even when preset was "off"
- Add enhanced logging showing all DyPE parameters when enabled

* fix(flux): apply DyPE scale/exponent and add metadata recall

- Fix DyPE scale and exponent parameters not being applied in frequency
  computation (compute_vision_yarn_freqs, compute_yarn_freqs now call
  get_timestep_mscale)
- Add metadata handlers for dype_scale and dype_exponent to enable
  recall from generated images
- Add i18n translations referencing existing parameter labels

* fix(flux): apply DyPE scale/exponent and add metadata recall

- Fix DyPE scale and exponent parameters not being applied in frequency
  computation (compute_vision_yarn_freqs, compute_yarn_freqs now call
  get_timestep_mscale)
- Add metadata handlers for dype_scale and dype_exponent to enable
  recall from generated images
- Add i18n translations referencing existing parameter labels

* feat(ui): show DyPE scale/exponent only when preset is "on"

- Hide scale/exponent controls in UI when preset is not "on"
- Only parse/recall scale/exponent from metadata when preset is "on"
- Prevents confusion where custom values override preset behavior

* fix(dype): only allow custom scale/exponent with 'on' preset

Presets (auto, 4k) now use their predefined values and ignore
any custom_scale/custom_exponent parameters. Only the 'on' preset
allows manual override of these values.

This matches the frontend UI behavior where the scale/exponent
fields are only shown when 'On' is selected.

* refactor(dype): rename 'on' preset to 'manual'

Rename the 'on' DyPE preset to 'manual' to better reflect its purpose:
allowing users to manually configure scale and exponent values.

Updated in:
- Backend presets (DYPE_PRESET_ON -> DYPE_PRESET_MANUAL)
- Frontend UI labels and options
- Redux slice type definitions
- Zod schema validation
- Tests

* refactor(dype): rename 'on' preset to 'manual'

Rename the 'on' DyPE preset to 'manual' to better reflect its purpose:
allowing users to manually configure scale and exponent values.

Updated in:
- Backend presets (DYPE_PRESET_ON -> DYPE_PRESET_MANUAL)
- Frontend UI labels and options
- Redux slice type definitions
- Zod schema validation
- Tests

* fix(dype): update remaining 'on' references to 'manual'

- Update docstrings, comments, and error messages to use 'manual' preset name
- Simplify FLUX graph builder to always send dype_scale/dype_exponent
- Fix UI condition to show DyPE controls for 'manual' preset

---------

Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-30 01:28:28 +00:00
Weblate (bot)
2102b43edc ui: translations update from weblate (#8807)
* translationBot(ui): update translation (Italian)

Currently translated at 95.0% (2124 of 2235 strings)

translationBot(ui): update translation (Italian)

Currently translated at 94.5% (2114 of 2235 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 98.2% (2195 of 2235 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 98.2% (2197 of 2235 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-30 01:22:50 +00:00
Lincoln Stein
5801e59e2b Documentation: InvokeAI PR review and merge policy (#8795)
* docs: Add a PR review and merge policy

* doc(release): add policy on release candidates

* docs(CD/CI): add best practice for external components
2026-01-30 01:03:43 +00:00
Lincoln Stein
5fc950b745 Release Workflow: Fix workflow edge case (#8792)
* release(docker): fix workflow edge case that prevented CUDA build from completing

* bugfix(release): fix yaml syntax error

* bugfix(CI/CD): fix similar problem in typegen check
2026-01-30 01:02:24 +00:00
Weblate (bot)
63dec985cd ui: translations update from weblate (#8806)
* translationBot(ui): update translation (Italian)

Currently translated at 95.0% (2124 of 2235 strings)

translationBot(ui): update translation (Italian)

Currently translated at 94.5% (2114 of 2235 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 98.2% (2195 of 2235 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 98.2% (2197 of 2235 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
2026-01-29 20:52:21 +00:00
Weblate (bot)
03cdd6df2e ui: translations update from weblate (#8804)
* translationBot(ui): update translation (Italian)

Currently translated at 95.0% (2124 of 2235 strings)

translationBot(ui): update translation (Italian)

Currently translated at 94.5% (2114 of 2235 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 98.2% (2195 of 2235 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 98.2% (2197 of 2235 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-29 15:42:10 -05:00
Lincoln Stein
99f4070ce7 translationBot(ui): update translation (Italian) (#8805)
Currently translated at 98.2% (2197 of 2235 strings)

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
2026-01-29 15:36:44 -05:00
Alexander Eichhorn
cf07f8be14 Add new model type integration guide (#8779)
* Add new model type integration guide

Comprehensive documentation covering all steps required to integrate
a new model type into InvokeAI, including:

- Backend: Model manager, configs, loaders, invocations, sampling
- Frontend: Graph building, state management, parameter recall
- Metadata, starter models, and optional features (ControlNet, LoRA, IP-Adapter)

Uses FLUX.1, FLUX.2 Klein, SD3, SDXL, and Z-Image as reference implementations.

* docs: improve new model integration guide

- Move document to docs/contributing/ directory
- Fix broken TOC links by replacing '&' with 'and' in headings
- Add code example for text encoder config (section 2.4)
- Add text encoder loader example (new section 3.3)
- Expand text encoder invocation to show full conditioning flow (section 4.2)

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-29 13:45:29 +00:00
Alexander Eichhorn
1f0d92defc fix(ui): allow guidance slider to reach 1 for FLUX.2 Klein
FLUX.2 Klein models require guidance=1 (no CFG), but the slider minimum
was set to 2. Changed sliderMin from 2 to 1 to allow proper configuration.
2026-01-29 07:27:18 +05:30
blessedcoolant
68089ca688 fix(ui): use proper FLUX2 latent RGB factors for preview images (#8802)
## Summary

Replace placeholder zeros with actual 32-channel factors from ComfyUI
and add latent_rgb_bias support for improved FLUX2 denoising previews.

## Related Issues / Discussions

https://github.com/Comfy-Org/ComfyUI/blob/main/comfy/latent_formats.py

https://github.com/user-attachments/assets/dfbc3d81-b855-46b8-8217-50b140f13520

## QA Instructions

1. Generate an image with a FLUX2 model (e.g. FLUX.2 Kontext)
2. Observe the denoising preview during generation
3. Preview should now show more accurate colors instead of
washed-out/incorrect colors from the previous placeholder factors
2026-01-29 07:12:39 +05:30
blessedcoolant
32e2132948 Merge branch 'main' into fix/flux2-latent-preview-factors 2026-01-29 07:07:50 +05:30
Alexander Eichhorn
bec3586930 fix(ui): use proper FLUX2 latent RGB factors for preview images
Replace placeholder zeros with actual 32-channel factors from ComfyUI
and add latent_rgb_bias support for improved FLUX2 denoising previews.
2026-01-29 02:22:17 +01:00
Weblate (bot)
8bf4d1ea59 ui: translations update from weblate (#8797)
* translationBot(ui): update translation (Italian)

Currently translated at 95.0% (2124 of 2235 strings)

translationBot(ui): update translation (Italian)

Currently translated at 94.5% (2114 of 2235 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Remove blank strings" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 98.2% (2195 of 2235 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-28 17:21:42 -05:00
Jonathan
fd7a3aebd2 Add input connectors to the FLUX model loader (#8785)
* Update flux_model_loader.py

Added nodal points for inputs to the model loader since we should be able to use a model selection node and pass in for Flux models.

* typegen

* Fixed existing ruff error

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-28 16:49:16 -05:00
Alexander Eichhorn
72491e2153 Fix ref_images metadata format for FLUX Kontext recall (#8791)
Remove extra array wrapper when saving ref_images metadata for FLUX.2 Klein

and FLUX.1 Kontext reference images. The double-nested array [[...]] was

preventing recall from parsing the metadata correctly.
2026-01-27 08:44:44 -05:00
Lincoln Stein
3d0725072d Prep for 6.11.0.rc1 (#8771)
* chore(release): add flux.2-klein to whats new items & bump version

* doc(release): update the WhatsNew text

* chore(frontend): run lint:prettier and frontend-typegen
2026-01-27 05:40:09 +00:00
Alexander Eichhorn
0ae7392c81 fix(model_manager): detect Flux1/2 VAE by latent space dimensions instead of filename (#8790)
* fix(model_manager): detect Flux VAE by latent space dimensions instead of filename

VAE detection previously relied solely on filename pattern matching, which failed
for Flux VAE files with generic names like "ae.safetensors". Now probes the model's
decoder.conv_in weight shape to determine the latent space dimensions:
- 16 channels -> Flux VAE
- 4 channels -> SD/SDXL VAE (with filename fallback for SD1/SD2/SDXL distinction)

* fix(model_manager): add latent space probing for Flux2 VAE detection

Extend Flux2 VAE detection to also check for 32-dimensional latent space
(decoder.conv_in with 32 input channels) in addition to BatchNorm layers.
This provides more robust detection for Flux2 VAE files regardless of filename.

* Chore Ruff format

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-27 05:20:50 +00:00
Alexander Eichhorn
cff20b45f3 Feature: Add DyPE (Dynamic Position Extrapolation) support to FLUX models for improved high-resolution image generation (#8763)
* docs: add DyPE implementation plan for FLUX high-resolution generation

Add detailed plan for porting ComfyUI-DyPE (Dynamic Position Extrapolation)
to InvokeAI, enabling 4K+ image generation with FLUX models without
training. Estimated effort: 5-7 developer days.

* docs: update DyPE plan with design decisions

- Integrate DyPE directly into FluxDenoise (no separate node)
- Add 4K preset and "auto" mode for automatic activation
- Confirm FLUX Schnell support (same base resolution as Dev)

* docs: add activation threshold for DyPE auto mode

FLUX can handle resolutions up to ~1.5x natively without artifacts.
Set activation_threshold=1536 so DyPE only kicks in above that.

* feat(flux): implement DyPE for high-resolution generation

Add Dynamic Position Extrapolation (DyPE) support to FLUX models,
enabling artifact-free generation at 4K+ resolutions.

New files:
- invokeai/backend/flux/dype/base.py: DyPEConfig and scaling calculations
- invokeai/backend/flux/dype/rope.py: DyPE-enhanced RoPE functions
- invokeai/backend/flux/dype/embed.py: DyPEEmbedND position embedder
- invokeai/backend/flux/dype/presets.py: Presets (off, auto, 4k)
- invokeai/backend/flux/extensions/dype_extension.py: Pipeline integration

Modified files:
- invokeai/backend/flux/denoise.py: Add dype_extension parameter
- invokeai/app/invocations/flux_denoise.py: Add UI parameters

UI parameters:
- dype_preset: off | auto | 4k
- dype_scale: Custom magnitude override (0-8)
- dype_exponent: Custom decay speed override (0-1000)

Auto mode activates DyPE for resolutions > 1536px.

Based on: https://github.com/wildminder/ComfyUI-DyPE

* feat(flux): add DyPE preset selector to Linear UI

Add Linear UI integration for FLUX DyPE (Dynamic Position Extrapolation):

- Add ParamFluxDypePreset component with Off/Auto/4K options
- Integrate preset selector in GenerationSettingsAccordion for FLUX models
- Add state management (paramsSlice, types) for fluxDypePreset
- Add dype_preset to FLUX denoise graph builder and metadata
- Add translations for DyPE preset label and popover
- Add zFluxDypePresetField schema definition

Fix DyPE frequency computation:
- Remove incorrect mscale multiplication on frequencies
- Use only NTK-aware theta scaling for position extrapolation

* feat(flux): add DyPE preset to metadata recall

- Add FluxDypePreset handler to ImageMetadataHandlers
- Parse dype_preset from metadata and dispatch setFluxDypePreset on recall
- Add translation key metadata.dypePreset

* chore: remove dype-implementation-plan.md

Remove internal planning document from the branch.

* chore(flux): bump flux_denoise version to 4.3.0

Version bump for dype_preset field addition.

* chore: ruff check fix

* chore: ruff format

* Fix truncated DyPE label in advanced options UI

Shorten the label from "DyPE (High-Res)" to "DyPE" to prevent text truncation in the sidebar. The high-resolution context is preserved in the informational popover tooltip.

* Add DyPE preset to recall parameters in image viewer

The dype_preset metadata was being saved but not displayed in the Recall Parameters tab. Add FluxDypePreset handler to ImageMetadataActions so users can see and recall this parameter.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com>
2026-01-26 23:54:44 -05:00
Alexander Eichhorn
b92c6ae633 feat(flux2): add FLUX.2 klein model support (#8768)
* WIP: feat(flux2): add FLUX 2 Kontext model support

- Add new invocation nodes for FLUX 2:
  - flux2_denoise: Denoising invocation for FLUX 2
  - flux2_klein_model_loader: Model loader for Klein architecture
  - flux2_klein_text_encoder: Text encoder for Qwen3-based encoding
  - flux2_vae_decode: VAE decoder for FLUX 2

- Add backend support:
  - New flux2 module with denoise and sampling utilities
  - Extended model manager configs for FLUX 2 models
  - Updated model loaders for Klein architecture

- Update frontend:
  - Extended graph builder for FLUX 2 support
  - Added FLUX 2 model types and configurations
  - Updated readiness checks and UI components

* fix(flux2): correct VAE decode with proper BN denormalization

FLUX.2 VAE uses Batch Normalization in the patchified latent space
(128 channels). The decode must:
1. Patchify latents from (B, 32, H, W) to (B, 128, H/2, W/2)
2. Apply BN denormalization using running_mean/running_var
3. Unpatchify back to (B, 32, H, W) for VAE decode

Also fixed image normalization from [-1, 1] to [0, 255].

This fixes washed-out colors in generated FLUX.2 Klein images.

* feat(flux2): add FLUX.2 Klein model support with ComfyUI checkpoint compatibility

- Add FLUX.2 transformer loader with BFL-to-diffusers weight conversion
- Fix AdaLayerNorm scale-shift swap for final_layer.adaLN_modulation weights
- Add VAE batch normalization handling for FLUX.2 latent normalization
- Add Qwen3 text encoder loader with ComfyUI FP8 quantization support
- Add frontend components for FLUX.2 Klein model selection
- Update configs and schema for FLUX.2 model types

* Chore Ruff

* Fix Flux1 vae probing

* Fix Windows Paths schema.ts

* Add 4B und 9B klein to Starter Models.

* feat(flux2): add non-commercial license indicator for FLUX.2 Klein 9B

- Add isFlux2Klein9BMainModelConfig and isNonCommercialMainModelConfig functions
- Update MainModelPicker and InitialStateMainModelPicker to show license icon
- Update license tooltip text to include FLUX.2 Klein 9B

* feat(flux2): add Klein/Qwen3 variant support and encoder filtering

Backend:
- Add klein_4b/klein_9b variants for FLUX.2 Klein models
- Add qwen3_4b/qwen3_8b variants for Qwen3 encoder models
- Validate encoder variant matches Klein model (4B↔4B, 9B↔8B)
- Auto-detect Qwen3 variant from hidden_size during probing

Frontend:
- Show variant field for all model types in ModelView
- Filter Qwen3 encoder dropdown to only show compatible variants
- Update variant type definitions (zFlux2VariantType, zQwen3VariantType)
- Remove unused exports (isFluxDevMainModelConfig, isFlux2Klein9BMainModelConfig)

* Chore Ruff

* feat(flux2): add Klein 9B Base (undistilled) variant support

Distinguish between FLUX.2 Klein 9B (distilled) and Klein 9B Base (undistilled)
models by checking guidance_embeds in diffusers config or guidance_in keys in
safetensors. Klein 9B Base requires more steps but offers higher quality.

* feat(flux2): improve diffusers compatibility and distilled model support

Backend changes:
- Update text encoder layers from [9,18,27] to (10,20,30) matching diffusers
- Use apply_chat_template with system message instead of manual formatting
- Change position IDs from ones to zeros to match diffusers implementation
- Add get_schedule_flux2() with empirical mu computation for proper schedule shifting
- Add txt_embed_scale parameter for Qwen3 embedding magnitude control
- Add shift_schedule toggle for base (28+ steps) vs distilled (4 steps) models
- Zero out guidance_embedder weights for Klein models without guidance_embeds

UI changes:
- Clear Klein VAE and Qwen3 encoder when switching away from flux2 base
- Clear Qwen3 encoder when switching between different Klein model variants
- Add toast notification informing user to select compatible encoder

* feat(flux2): fix distilled model scheduling with proper dynamic shifting

- Configure scheduler with FLUX.2 Klein parameters from scheduler_config.json
  (use_dynamic_shifting=True, shift=3.0, time_shift_type="exponential")
- Pass mu parameter to scheduler.set_timesteps() for resolution-aware shifting
- Remove manual shift_schedule parameter (scheduler handles this automatically)
- Simplify get_schedule_flux2() to return linear sigmas only
- Remove txt_embed_scale parameter (no longer needed)

This matches the diffusers Flux2KleinPipeline behavior where the
FlowMatchEulerDiscreteScheduler applies dynamic timestep shifting
based on image resolution via the mu parameter.

Fixes 4-step distilled Klein 9B model quality issues.

* fix(ui): fix FLUX.1 graph building with posCondCollect node lookup

The posCondCollect node was created with getPrefixedId() which generates
a random suffix (e.g., 'pos_cond_collect:abc123'), but g.getNode() was
called with the plain string 'pos_cond_collect', causing a node lookup
failure.

Fix by declaring posCondCollect as a module-scoped variable and
referencing it directly instead of using g.getNode().

* Remove Flux2 Klein Base from Starter Models

* Remove Logging

* Add Default Values for Flux2 Klein and add variant as additional info to from_base

* Add migrations for the z-image qwen3 encoder without a variant value

* Add img2img, inpainting and outpainting support for FLUX.2 Klein

- Add flux2_vae_encode invocation for encoding images to FLUX.2 latents
- Integrate inpaint_extension into FLUX.2 denoise loop for proper mask handling
- Apply BN normalization to init_latents and noise for consistency in inpainting
- Use manual Euler stepping for img2img/inpaint to preserve exact timestep schedule
- Add flux2_img2img, flux2_inpaint, flux2_outpaint generation modes
- Expand starter models with FP8 variants, standalone transformers, and separate VAE/encoders
- Fix outpainting to always use full denoising (0-1) since strength doesn't apply
- Improve error messages in model loader with clear guidance for standalone models

* Add GGUF quantized model support and Diffusers VAE loader for FLUX.2 Klein

- Add Main_GGUF_Flux2_Config for GGUF-quantized FLUX.2 transformer models
- Add VAE_Diffusers_Flux2_Config for FLUX.2 VAE in diffusers format
- Add Flux2GGUFCheckpointModel loader with BFL-to-diffusers conversion
- Add Flux2VAEDiffusersLoader for AutoencoderKLFlux2
- Add FLUX.2 Klein 4B/9B hardware requirements to documentation
- Update starter model descriptions to clarify dependencies install together
- Update frontend schema for new model configs

* Fix FLUX.2 model detection and add FP8 weight dequantization support

- Improve FLUX.2 variant detection for GGUF/checkpoint models (BFL format keys)
- Fix guidance_embeds logic: distilled=False, undistilled=True
- Add FP8 weight dequantization for ComfyUI-style quantized models
- Prevent FLUX.2 models from being misidentified as FLUX.1
- Preserve user-editable fields (name, description, etc.) on model reidentify
- Improve Qwen3Encoder detection by variant in starter models
- Add defensive checks for tensor operations

* Chore ruff format

* Chore Typegen

* Fix FLUX.2 Klein 9B model loading by detecting hidden_size from weights

Previously num_attention_heads was hardcoded to 24, which is correct for
Klein 4B but causes size mismatches when loading Klein 9B checkpoints.

Now dynamically calculates num_attention_heads from the hidden_size
dimension of context_embedder weights:
- Klein 4B: hidden_size=3072 → num_attention_heads=24
- Klein 9B: hidden_size=4096 → num_attention_heads=32

Fixes both Checkpoint and GGUF loaders for FLUX.2 models.

* Only clear Qwen3 encoder when FLUX.2 Klein variant changes

Previously the encoder was cleared whenever switching between any Klein
models, even if they had the same variant. Now compares the variant of
the old and new model and only clears the encoder when switching between
different variants (e.g., klein_4b to klein_9b).

This allows users to switch between different Klein 9B models without
having to re-select the Qwen3 encoder each time.

* Add metadata recall support for FLUX.2 Klein parameters

The scheduler, VAE model, and Qwen3 encoder model were not being
recalled correctly for FLUX.2 Klein images. This adds dedicated
metadata handlers for the Klein-specific parameters.

* Fix FLUX.2 Klein denoising scaling and Z-Image VAE compatibility

- Apply exponential denoising scaling (exponent 0.2) to FLUX.2 Klein,
  matching FLUX.1 behavior for more intuitive inpainting strength
- Add isFlux1VAEModelConfig type guard to filter FLUX 1.0 VAEs only
- Restrict Z-Image VAE selection to FLUX 1.0 VAEs, excluding FLUX.2
  Klein 32-channel VAEs which are incompatible

* chore pnpm fix

* Add FLUX.2 Klein to starter bundles and documentation

- Add FLUX.2 Klein hardware requirements to quick start guide
- Create flux2_klein_bundle with GGUF Q4 model, VAE, and Qwen3 encoder
- Add "What's New" entry announcing FLUX.2 Klein support

* Add FLUX.2 Klein built-in reference image editing support

FLUX.2 Klein has native multi-reference image editing without requiring
a separate model (unlike FLUX.1 which needs a Kontext model).

Backend changes:
- Add Flux2RefImageExtension for encoding reference images with FLUX.2 VAE
- Apply BN normalization to reference image latents for correct scaling
- Use T-coordinate offset scale=10 like diffusers (T=10, 20, 30...)
- Concatenate reference latents with generated image during denoising
- Extract only generated portion in step callback for correct preview

Frontend changes:
- Add flux2_reference_image config type without model field
- Hide model selector for FLUX.2 reference images (built-in support)
- Add type guards to handle configs without model property
- Update validators to skip model validation for FLUX.2
- Add 'flux2' to SUPPORTS_REF_IMAGES_BASE_MODELS

* Chore windows path fix

* Add reference image resizing for FLUX.2 Klein

Resize large reference images to match BFL FLUX.2 sampling.py limits:
- Single reference: max 2024² pixels (~4.1M)
- Multiple references: max 1024² pixels (~1M)

Uses same scaling approach as BFL's cap_pixels() function.
2026-01-26 23:21:37 -05:00
DustyShoe
729bae19a5 Feat(UI): Search bar in image info code tabs and add vertical margins for improved UX in Recall Parameters tab. (#8786)
* Adjusted Search bar position and added padding in image info viewer.

* Minor bug fix with spaces being highlighted.
2026-01-25 22:38:43 +01:00
Copilot
fcc81f17a5 Limit automated issue closure to bug issues only (#8776)
* Initial plan

* Add only-labels parameter to limit automated issue closure to bugs only

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
2026-01-21 02:43:59 +05:30
Weblate (bot)
27ae70a428 translationBot(ui): update translation files (#8767)
Updated by "Cleanup translation files" hook in Weblate.


Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI
2026-01-20 15:50:21 -05:00
Lincoln Stein
82819cdadc Add user survey section to README (#8766)
* Add user survey section to README

Added a section for new and returning users to take a survey.

* docs: add user survey link to WhatsNew

* Fix formatting issues in WhatsNew.tsx

---------

Co-authored-by: Alexander Eichhorn <alex@eichhorn.dev>
2026-01-16 03:32:16 +01:00
Alexander Eichhorn
b2b8820519 fix(model_manager): prevent Z-Image LoRAs from being misclassified as main models (#8754)
* fix(model_manager): prevent Z-Image LoRAs from being misclassified as main models

Z-Image LoRAs containing keys like `diffusion_model.context_refiner.*` were being
incorrectly classified as main checkpoint models instead of LoRAs. This happened
because the `_has_z_image_keys()` function checked for Z-Image specific keys
(like `context_refiner`) without verifying if the file was actually a LoRA.

Since main models have higher priority than LoRAs in the classification sort order,
the incorrect main model classification would win.

The fix adds detection of LoRA-specific weight suffixes (`.lora_down.weight`,
`.lora_up.weight`, `.lora_A.weight`, `.lora_B.weight`, `.dora_scale`) and returns
False if any are found, ensuring LoRAs are correctly classified.

* refactor(mm): simplify _has_z_image_keys with early return

Return True directly when a Z-Image key is found instead of using an
intermediate variable.
2026-01-14 22:35:17 -05:00
Alexander Eichhorn
bb6c544603 feat(z-image): add Seed Variance Enhancer node and Linear UI integration (#8753)
* feat(z-image): add Seed Variance Enhancer node and Linear UI integration

Add a new conditioning node for Z-Image models that injects seed-based
noise into text embeddings to increase visual variation between seeds.

Backend:
- New invocation: z_image_seed_variance_enhancer.py
- Parameters: strength (0-2), randomize_percent (1-100%), seed

Frontend:
- State management in paramsSlice with selectors and reducers
- UI components in SeedVariance/ folder with toggle and sliders
- Integration in GenerationSettingsAccordion (Advanced Options)
- Graph builder integration in buildZImageGraph.ts
- Metadata recall handlers for remix functionality
- Translations and tooltip descriptions

Based on: github.com/Pfannkuchensack/invokeai-z-image-seed-variance-enhancer

* chore: ruff and typegen fix

* chore: ruff and typegen fix

* Revise seedVarianceStrength explanation

Updated description for seedVarianceStrength.

* Update description for seedVarianceStrength

* fix(z-image): correct noise range comment from [-1, 1] to [-1, 1)

torch.rand() generates [0, 1), so the scaled range excludes 1.
2026-01-12 20:36:21 +01:00
blessedcoolant
8a18914637 chore(CI/CD): Remove codeowners from /docs directory (#8737)
## Summary

This PR removes codeowners from the `/docs` directory, allowing any team
member with repo write permissions to review and approve PRs involving
documentation.

## Related Issues / Discussions

Documentation review is a shared responsibility.

## QA Instructions

None needed.

## Merge Plan

Simple merge.

## Checklist

- [X] _The PR has a short but descriptive title, suitable for a
changelog_
- [ ] _Tests added / updated (if applicable)_
- [ ] _Changes to a redux slice have a corresponding migration_
- [ ] _Documentation added / updated (if applicable)_
- [ ] _Updated `What's New` copy (if doing a release after this PR)_
2026-01-12 15:19:22 +05:30
blessedcoolant
d66df9a0d0 Merge branch 'main' into lstein/chore/codeowners 2026-01-12 15:18:19 +05:30
DustyShoe
5c00684701 Feat(UI): Canvas high level transform smoothing (#8756)
* WIP transform smoothing controls

* Fix transform smoothing control typings

* High level resize algo for transformation

* ESLint fix

* format with prettier

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-11 15:48:27 -05:00
DustyShoe
d93ce6ac42 Fix(UI): Canvas numeric brush size (#8761)
* Fix for brush/eraser size not updating on up/down arrow click

* Made further improvements on brush size selection behavior

---------

Co-authored-by: Alexander Eichhorn <alex@eichhorn.dev>
2026-01-11 15:23:06 -05:00
blessedcoolant
13bf5feb4d Fix(UI): Error message for extract region (#8759)
## Summary

This PR fixes misleading popup message "Canvas is empty" when attempting
to extract region with empty mask layer.
Replaced with correct message "Mask layer is empty". Also redirected few
other popups to use translation file.


## Checklist

- [x] _The PR has a short but descriptive title, suitable for a
changelog_
- [ ] _Tests added / updated (if applicable)_
- [ ] _Changes to a redux slice have a corresponding migration_
- [ ] _Documentation added / updated (if applicable)_
- [ ] _Updated `What's New` copy (if doing a release after this PR)_
2026-01-11 21:53:48 +05:30
DustyShoe
53ab178edd Merge branch 'invoke-ai:main' into Fix(UI)--Error-messsage-for-extract-region 2026-01-11 02:13:35 +02:00
DustyShoe
2d8317f1aa Corrected error message and redirected popup messages to use translation file 2026-01-11 02:08:47 +02:00
Lincoln Stein
04f815638c chore(invocation stats): remove old dangling debug statement 2026-01-10 11:32:37 -05:00
Lincoln Stein
d6ad6a2dcb fix(invocation stats): Report delta VRAM for each invocation and fix reporting of RAM cache size 2026-01-10 11:32:37 -05:00
Hosted Weblate
784503e484 translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI
2026-01-08 16:48:16 -05:00
RyoKoba
da2809b000 translationBot(ui): update translation (Japanese)
Currently translated at 99.6% (2155 of 2163 strings)

Co-authored-by: RyoKoba <kobayashi_ryo@cyberagent.co.jp>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ja/
Translation: InvokeAI/Web UI
2026-01-08 16:48:16 -05:00
Weblate (bot)
53c34eb95e ui: translations update from weblate (#8748)
* translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2099 of 2132 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2130 of 2163 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2130 of 2163 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Japanese)

Currently translated at 99.6% (2155 of 2163 strings)

Co-authored-by: RyoKoba <kobayashi_ryo@cyberagent.co.jp>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ja/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2103 of 2136 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): added translation (English (United Kingdom))

* translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Co-authored-by: RyoKoba <kobayashi_ryo@cyberagent.co.jp>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-08 15:56:33 -05:00
Weblate (bot)
18fc822d37 ui: translations update from weblate (#8747)
* translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2099 of 2132 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2130 of 2163 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2130 of 2163 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Japanese)

Currently translated at 99.6% (2155 of 2163 strings)

Co-authored-by: RyoKoba <kobayashi_ryo@cyberagent.co.jp>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ja/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2103 of 2136 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): added translation (English (United Kingdom))

* translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translation: InvokeAI/Web UI
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Co-authored-by: RyoKoba <kobayashi_ryo@cyberagent.co.jp>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-08 20:31:33 +00:00
Lincoln Stein
89dc50bd7c Chore: Fix weblate merge conflicts (#8744)
* translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2099 of 2132 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2130 of 2163 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2130 of 2163 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Japanese)

Currently translated at 99.6% (2155 of 2163 strings)

Co-authored-by: RyoKoba <kobayashi_ryo@cyberagent.co.jp>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ja/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2103 of 2136 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* chore: add weblate.ini file to gitignore

* Fix duplicate entry in ja.json

Removed duplicate 'jump' entry in Japanese locale.

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Co-authored-by: RyoKoba <kobayashi_ryo@cyberagent.co.jp>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
2026-01-08 15:25:11 -05:00
Lincoln Stein
d34655fd58 Fix(model manager): Improve calculation of Z-Image VAE working memory needs (#8740)
* Fix Z-Image VAE encode/decode to request working memory

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>

* fix: remove check for non-flux vae

* fix: remove check for non-flux vae: latents_to_image

* Remove conditional estimation tests

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
2026-01-08 17:48:09 +00:00
Lincoln Stein
c1a8300e96 chore(release): bump development version to 6.10.0.post1 (#8745)
* chore(release): bump development version 6.10.0post1

* chore: fix version syntax
2026-01-08 12:42:11 -05:00
Lincoln Stein
9c5b2f6498 (chore) Bump to version 6.10.0 (#8742)
* (chore) Prep for v6.10.0rc2

* (chore) bump to version v6.10.0
2026-01-05 23:47:57 -05:00
Alexander Eichhorn
dbb4a07a8f feat(z-image): add add_noise option to Z-Image Denoise (#8739)
* feat(z-image): add `add_noise` option to Z-Image Denoise

Add the same `add_noise` option that exists in FLUX Denoise to Z-Image Denoise.
When set to false, no noise is added to the input latents during image-to-image,
allowing for more controlled transformations.
2026-01-05 21:24:44 -05:00
Lincoln Stein
f66a1a38c8 Merge branch 'main' into lstein/chore/codeowners 2026-01-05 15:16:33 -05:00
Alexander Eichhorn
be2635161c Feature: z-image + metadata node (#8733)
## Summary

Add a new "Denoise - Z-Image + Metadata" node
(`ZImageDenoiseMetaInvocation`) that extends the Z-Image denoise node
with metadata output for image recall functionality.

This follows the same pattern as existing `denoise_latents_meta`
(SD1.5/SDXL) and `flux_denoise_meta` (FLUX) nodes.

**Captured metadata:**
- `width` / `height`
- `steps`
- `guidance` (guidance_scale)
- `denoising_start` / `denoising_end`
- `scheduler`
- `model` (transformer)
- `seed`
- `loras` (if applied)

## Related Issues / Discussions

Enables metadata recall for Z-Image generated images, similar to
existing support for SD1.5, SDXL, and FLUX models.

## QA Instructions

1. Create a workflow using the new "Denoise - Z-Image + Metadata" node
2. Connect the metadata output to a "Save Image" node
3. Generate an image
4. Check that metadata is saved with the image (visible in image info
panel)
5. Verify all generation parameters are captured correctly

## Merge Plan

Requires `feature/zimage-scheduler-support` #8705 branch to be merged
first (base branch).

## Checklist

- [x] _The PR has a short but descriptive title, suitable for a
changelog_
- [ ] _Tests added / updated (if applicable)_
- [ ] _Changes to a redux slice have a corresponding migration_
- [ ] _Documentation added / updated (if applicable)_
- [ ] _Updated `What's New` copy (if doing a release after this PR)_
2026-01-05 01:56:22 +01:00
Alexander Eichhorn
384a1a689d Merge branch 'main' into z-image_metadata_node 2026-01-05 01:50:28 +01:00
Lincoln Stein
0021404639 chore: remove dangling debug statements (#8738) 2026-01-05 00:47:46 +00:00
Alexander Eichhorn
a05a626644 Fix typegen 2026-01-05 01:42:49 +01:00
Alexander Eichhorn
97b82d752e Add configurable model cache timeout for automatic memory management (#8693)
## Summary

Adds `model_cache_keep_alive_min` config option (minutes, default 5) to
automatically clear model cache after inactivity. Addresses memory
contention when running InvokeAI alongside other GPU applications like
Ollama.

**Implementation:**
- **Config**: New `model_cache_keep_alive_min` field in
`InvokeAIAppConfig` with 5-minute default
- **ModelCache**: Activity tracking on get/lock/unlock/put operations,
threading.Timer for scheduled clearing
- **Thread safety**: Double-check pattern handles race conditions,
daemon threads for clean shutdown
- **Integration**: ModelManagerService passes config to cache, calls
shutdown() on stop
- **Logging**: Smart timeout logging that only shows messages when
unlocked models are actually cleared
- **Tests**: Comprehensive unit tests with properly configured mock
logger

**Usage:**
```yaml
# invokeai.yaml
model_cache_keep_alive_min: 10  # Clear after 10 minutes idle
model_cache_keep_alive_min: 0   # Set to 0 for indefinite caching (old behavior)
```

**Key Behavior:**
- **Default timeout**: 5 minutes - models are automatically cleared
after 5 minutes of inactivity
- Clearing uses same logic as "Clear Model Cache" button (make_room with
1000GB)
- Only clears **unlocked** models (respects models actively in use
during generation)
- Timeout message only appears when models are actually cleared
- Debug logging available for timeout events when no action is taken
- Prevents misleading log entries during active generation
- Users can set to 0 to restore indefinite caching behavior

## Related Issues / Discussions

Addresses enhancement request for automatic model unloading from memory
after inactivity period.

## QA Instructions

1. **Test default behavior (5-minute timeout)**:
   - Start InvokeAI without explicit config
   - Run a generation
   - Wait 6 minutes with no activity
   - Check logs for "Clearing X unlocked model(s) from cache" message
   - Verify cache is empty

2. **Test custom timeout**:
   - Set `model_cache_keep_alive_min: 0.1` (6 seconds) in config
   - Load a model (run generation)
   - Wait 7+ seconds with no activity
   - Check logs for "Clearing X unlocked model(s) from cache" message
   - Verify cache is empty

3. **Test no timeout (old behavior)**:
   - Set `model_cache_keep_alive_min: 0` in config
   - Run generations and wait extended periods
   - Verify models remain cached indefinitely

4. **Test during active use**:
   - Run continuous generations with any timeout setting
- Verify no timeout messages appear during active use (models are
locked)
- After generation completes, wait for timeout and verify unlocked
models are cleared

## Merge Plan

N/A - Additive change with sensible defaults. The 5-minute default
enables automatic memory management while remaining practical for
typical workflows.

## Checklist

- [x] _The PR has a short but descriptive title, suitable for a
changelog_
- [x] _Tests added / updated (if applicable)_
- [ ] _Changes to a redux slice have a corresponding migration_
- [x] _Documentation added / updated (if applicable)_
- [ ] _Updated `What's New` copy (if doing a release after this PR)_

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>[enhancement]: option to unload from memory
</issue_title>
> <issue_description>### Is there an existing issue for this?
> 
> - [X] I have searched the existing issues
> 
> ### Contact Details
> 
> ### What should this feature add?
> 
> a command line option to unload model from RAM after a defined period
of time
> 
> ### Alternatives
> 
> running as a container and using Sablier to shutdown the container
after some time, this has the downside of if traffic isn't see through
the web interface it will be shut even if jobs are running.
> 
> ### Additional Content
> 
> _No response_</issue_description>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> <comment_new><author>@lstein</author><body>
> I am reopening this issue. I'm running ollama and invoke on the same
server and I find their memory requirements are frequently clashing. It
would be helpful to offer users the option to have the model cache
automatically cleared after a fixed amount of inactivity. I would
suggest the following:
> 
> 1. Introduce a new config file option `model_cache_keep_alive` which
specifies, in minutes, how long to keep a model in cache between
generations. The default is 0, which means to keep the model in cache
indefinitely, as is currently the case.
> 2. If no model generations occur within the timeout period, the model
cache is cleared using the same backend code as the "Clear Model Cache"
button in the queue tab.
> 
> I'm going to assign this to GitHub copilot, partly to test how well it
can manage the Invoke code base. </body></comment_new>
> </comments>
> 


</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes invoke-ai/InvokeAI#6856

<!-- START COPILOT CODING AGENT TIPS -->
---

 Let Copilot coding agent [set things up for
you](https://github.com/invoke-ai/InvokeAI/issues/new?title=+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot)
— coding agent works faster and does higher quality work when set up for
your repo.
2026-01-05 01:41:40 +01:00
Alexander Eichhorn
f29820a7ba feat(ui): improve Z-Image model selector UX with auto-clearing conflicts
Instead of disabling mutually exclusive model selectors, automatically
clear conflicting models when a new selection is made. This applies to
VAE, Qwen3 Encoder, and Qwen3 Source selectors - selecting one now
clears the others. Also applies same logic during metadata recall.
2026-01-05 00:57:45 +01:00
Lincoln Stein
47a634d8fb fix(naming style) change name of model_cache_keep_alive to model_cache_keep_alive_min 2026-01-04 17:36:55 -05:00
Lincoln Stein
768f3dbde0 chore: remove codeowners from /docs directory 2026-01-04 17:08:45 -05:00
Alexander Eichhorn
1ca589ea10 Merge branch 'main' into z-image_metadata_node 2026-01-04 23:07:06 +01:00
Jonathan
3a21e7699f Merge branch 'main' into copilot/add-unload-model-option 2026-01-04 10:22:44 -05:00
Lincoln Stein
56fd7bc7c4 docs(z-image) add Z-Image requirements and starter bundle (#8734)
* docs(z-image) add minimum requirements for Z-Image and create Z-Image starter bundle

* fix(model manager) add flux VAE to Z-Image bundle

* docs(model manager) remove out-of-date model info link

* chore: fix frontendchecks

* chore: lint:prettier

* docs(model manager): clarify minimum hardware for z-image turbo

* (fix) add flux VAE to ZIT starter dependencies & tweak UI docs
2026-01-04 10:17:26 -05:00
Lincoln Stein
2425005aad chore: typegen update 2026-01-04 09:28:43 -05:00
Lincoln Stein
2ccadd1834 Merge branch 'main' into z-image_metadata_node 2026-01-04 07:03:25 -05:00
Lincoln Stein
5cef8bd364 (fix) default timeout to 0 min, to disable timeout feature and restore previous default behavior 2026-01-04 07:01:01 -05:00
Lincoln Stein
8a6d593fe8 Merge branch 'main' into copilot/add-unload-model-option 2026-01-03 22:48:36 -05:00
Lincoln Stein
14309562b8 chore: typegen 2026-01-03 22:48:19 -05:00
Alexander Eichhorn
9f8f9965f9 fix(model-loaders): add local_files_only=True to prevent network requests (#8735) 2026-01-03 22:21:42 -05:00
Jonathan
44a21a348d Merge branch 'main' into copilot/add-unload-model-option 2026-01-03 22:00:11 -05:00
Alexander Eichhorn
81d83d5aab Merge branch 'main' into z-image_metadata_node 2026-01-03 23:06:42 +01:00
Alexander Eichhorn
d99707fdcb fix(ui): fix z-image scheduler recall by reordering metadata handlers
Move Scheduler handler after MainModel in ImageMetadataHandlers so that
base-dependent recall logic (z-image scheduler) works correctly. The
Scheduler handler checks `base === 'z-image'` before dispatching the
z-image scheduler action, but this check failed when Scheduler ran
before MainModel was recalled.
2026-01-03 22:33:18 +01:00
dunkeroni
252dd5b426 Add @dunkeroni as code owner for some paths (#8732)
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-03 20:56:21 +00:00
Alexander Eichhorn
f922f6c634 Update CODEOWNERS (#8731)
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-03 20:53:16 +00:00
Alexander Eichhorn
be0cbe046c feat(flux): add scheduler selection for Flux models (#8704)
* feat(flux): add scheduler selection for Flux models

Add support for alternative diffusers Flow Matching schedulers:
- Euler (default, 1st order)
- Heun (2nd order, better quality, 2x slower)
- LCM (optimized for few steps)

Backend:
- Add schedulers.py with scheduler type definitions and class mapping
- Modify denoise.py to accept optional scheduler parameter
- Add scheduler InputField to flux_denoise invocation (v4.2.0)

Frontend:
- Add fluxScheduler to Redux state and paramsSlice
- Create ParamFluxScheduler component for Linear UI
- Add scheduler to buildFLUXGraph for generation

* fix(flux): prevent progress percentage overflow with LCM scheduler

LCM scheduler may have more internal timesteps than user-facing steps,
causing user_step to exceed total_steps. This resulted in progress
percentage > 1.0, which caused a pydantic validation error.

Fix: Only call step_callback when user_step <= total_steps.

* Ruff format

* fix(flux): remove initial step-0 callback for consistent step count

Remove the initial step_callback at step=0 to match SD/SDXL behavior.
Previously Flux showed N+1 steps (step 0 + N denoising steps), while
SD/SDXL showed only N steps. Now all models display N steps consistently.

* feat(flux): add scheduler support with metadata recall

- Handle LCM scheduler by using num_inference_steps instead of custom sigmas
- Fix progress bar to show user-facing steps instead of internal scheduler steps
- Pass scheduler parameter to Flux denoise node in graph builder
- Add model-aware metadata recall for Flux scheduler

---------

Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-03 15:52:00 -05:00
Jonathan
e39b880f6d Merge branch 'main' into copilot/add-unload-model-option 2026-01-03 15:41:59 -05:00
Jonathan
4f8ec07d2f Update CODEOWNERS (#8728)
Adding @JPPhoto to CODEOWNERS

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-03 20:40:27 +00:00
Alexander Eichhorn
689953e3cf Feature/zimage scheduler support (#8705)
* feat(flux): add scheduler selection for Flux models

Add support for alternative diffusers Flow Matching schedulers:
- Euler (default, 1st order)
- Heun (2nd order, better quality, 2x slower)
- LCM (optimized for few steps)

Backend:
- Add schedulers.py with scheduler type definitions and class mapping
- Modify denoise.py to accept optional scheduler parameter
- Add scheduler InputField to flux_denoise invocation (v4.2.0)

Frontend:
- Add fluxScheduler to Redux state and paramsSlice
- Create ParamFluxScheduler component for Linear UI
- Add scheduler to buildFLUXGraph for generation

* feat(z-image): add scheduler selection for Z-Image models

Add support for alternative diffusers Flow Matching schedulers for Z-Image:
- Euler (default) - 1st order, optimized for Z-Image-Turbo (8 steps)
- Heun (2nd order) - Better quality, 2x slower
- LCM - Optimized for few-step generation

Backend:
- Extend schedulers.py with Z-Image scheduler types and mapping
- Add scheduler InputField to z_image_denoise invocation (v1.3.0)
- Refactor denoising loop to support diffusers schedulers

Frontend:
- Add zImageScheduler to Redux state in paramsSlice
- Create ParamZImageScheduler component for Linear UI
- Add scheduler to buildZImageGraph for generation

* fix ruff check

* fix(schedulers): prevent progress percentage overflow with LCM scheduler

LCM scheduler may have more internal timesteps than user-facing steps,
causing user_step to exceed total_steps. This resulted in progress
percentage > 1.0, which caused a pydantic validation error.

Fix: Only call step_callback when user_step <= total_steps.

* Ruff format

* fix(schedulers): remove initial step-0 callback for consistent step count

Remove the initial step_callback at step=0 to match SD/SDXL behavior.
Previously Flux/Z-Image showed N+1 steps (step 0 + N denoising steps),
while SD/SDXL showed only N steps. Now all models display N steps
consistently in the server log.

* feat(z-image): add scheduler support with metadata recall

- Handle LCM scheduler by using num_inference_steps instead of custom sigmas
- Fix progress bar to show user-facing steps instead of internal scheduler steps
- Pass scheduler parameter to Z-Image denoise node in graph builder
- Add model-aware metadata recall for Flux and Z-Image schedulers

---------

Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-03 20:37:04 +00:00
Lincoln Stein
61c2589e39 (chore) update WhatsNew translation text (#8727) 2026-01-03 20:31:50 +00:00
Lincoln Stein
8cf4c6944a (style) ruff fix 2026-01-03 14:54:15 -05:00
Lincoln Stein
db228ddc4f (style) add @record_activity and @synchronized to locked methods 2026-01-03 14:52:31 -05:00
Lincoln Stein
858c94b575 Merge remote-tracking branch 'refs/remotes/origin/copilot/add-unload-model-option' into copilot/add-unload-model-option 2026-01-03 14:26:20 -05:00
Alexander Eichhorn
252794d717 ruff fix 2026-01-03 19:50:08 +01:00
Alexander Eichhorn
7847ccea13 fix typegen 2026-01-03 19:48:11 +01:00
Alexander Eichhorn
1bcf589d19 feat(z-image): add Z-Image Denoise + Metadata node
Add ZImageDenoiseMetaInvocation that extends ZImageDenoiseInvocation
with metadata output for image recall. Captures generation parameters
including steps, guidance, scheduler, seed, model, and LoRAs.
2026-01-03 18:28:17 +01:00
Alexander Eichhorn
132a48497b feat(z-image): add scheduler support with metadata recall
- Handle LCM scheduler by using num_inference_steps instead of custom sigmas
- Fix progress bar to show user-facing steps instead of internal scheduler steps
- Pass scheduler parameter to Z-Image denoise node in graph builder
- Add model-aware metadata recall for Flux and Z-Image schedulers
2026-01-03 17:11:05 +01:00
Jonathan
f49e1b8dae Merge branch 'main' into copilot/add-unload-model-option 2026-01-01 21:31:08 -05:00
Jonathan
e7233efb79 Merge branch 'main' into feature/zimage-scheduler-support 2026-01-01 21:30:44 -05:00
Alexander Eichhorn
3b2d2ef10a fix(gguf): ensure dequantized tensors are on correct device for MPS (#8713)
When using GGUF-quantized models on MPS (Apple Silicon), the
dequantized tensors could end up on a different device than the
other operands in math operations, causing "Expected all tensors
to be on the same device" errors.

This fix ensures that after dequantization, tensors are moved to
the same device as the other tensors in the operation.

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-02 00:45:50 +00:00
Alexander Eichhorn
66974841f1 fix(model-manager): support offline Qwen3 tokenizer loading for Z-Image (#8719)
Add local_files_only fallback for Qwen3 tokenizer loading in both
Checkpoint and GGUF loaders. This ensures Z-Image models can generate
images offline after the initial tokenizer download.

The tokenizer is now loaded with local_files_only=True first, falling
back to network download only if files aren't cached yet.

Fixes #8716

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-02 00:40:08 +00:00
Lincoln Stein
87608ade45 (chore) update config docstrings 2026-01-01 19:35:15 -05:00
Weblate (bot)
1e83aeeb79 ui: translations update from weblate (#8725)
* translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2099 of 2132 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2130 of 2163 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2130 of 2163 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Japanese)

Currently translated at 99.6% (2155 of 2163 strings)

Co-authored-by: RyoKoba <kobayashi_ryo@cyberagent.co.jp>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ja/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2103 of 2136 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): added translation (English (United Kingdom))

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Co-authored-by: RyoKoba <kobayashi_ryo@cyberagent.co.jp>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-01-02 00:35:09 +00:00
Alex Yankov
1c76d295a2 fix(docs) Bump versions in mkdocs github actions (#8722) 2026-01-01 19:31:33 -05:00
Lincoln Stein
384250ff8c Merge branch 'main' into copilot/add-unload-model-option 2026-01-01 19:28:45 -05:00
Lincoln Stein
6c3ce8e7e9 Merge branch 'main' into feature/zimage-scheduler-support 2026-01-01 19:08:56 -05:00
Weblate (bot)
d658ef4322 ui: translations update from weblate (#8724)
* translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2099 of 2132 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2130 of 2163 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2130 of 2163 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Japanese)

Currently translated at 99.6% (2155 of 2163 strings)

Co-authored-by: RyoKoba <kobayashi_ryo@cyberagent.co.jp>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ja/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2103 of 2136 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Co-authored-by: RyoKoba <kobayashi_ryo@cyberagent.co.jp>
2025-12-30 13:21:36 -05:00
Alexander Eichhorn
8d880ef5a0 fix(schedulers): remove initial step-0 callback for consistent step count
Remove the initial step_callback at step=0 to match SD/SDXL behavior.
Previously Flux/Z-Image showed N+1 steps (step 0 + N denoising steps),
while SD/SDXL showed only N steps. Now all models display N steps
consistently in the server log.
2025-12-29 12:39:39 +01:00
Lincoln Stein
c6775cc999 (style) ruff and typegen updates 2025-12-28 22:40:36 -05:00
Lincoln Stein
d44b99ae0a Merge branch 'main' into copilot/add-unload-model-option 2025-12-28 22:39:45 -05:00
blessedcoolant
1675712094 Implement PBR Maps Node (#8700)
* feat: Implement PBR Maps Generation Node

* feat(ui): Add PBR Maps Generation to UI

* chore: fix typegen checks

* chore: possible fix for nvidia 5000 series cards

* fix: Use safetensor models for PBR maps instead of pickles.

* fix: incorrect naming of upconv_block for PBR network

* fix: incorrect naming of displacement map variable

* chore: add relevant docs to the PBR generate function

* fix: clear cuda cache after loading state_dict for PBR maps

* fix: load torch_device only once as multiple models are loaded

* chore(ui): update the filter icon for PBR to CubeBold

More relevant

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2025-12-29 02:11:46 +00:00
Kyle H
2924d052c5 Fix an issue with regional guidance and multiple quick-queued generations after moving bbox (#8613)
* Fix an issue with multiple quick-queued generations after moving bbox

After moving the canvas bbox we still handed out the previous regional-guidance mask because only two parts of the system knew anything had changed. The adapter’s
cache key doesn’t include the bbox, so the next few graph builds reused the stale mask from before the move; if the user queued several runs back‑to‑back, every
background enqueue except the last skipped rerasterizing altogether because another raster job was still in flight. The fix makes the canvas manager invalidate each
region adapter’s cached mask whenever the bbox (or a related setting) changes, and—if a reraster is already running—queues up and waits instead of bailing. Now the
first run after a bbox edit forces a new mask, and rapid-fire enqueues just wait their turn, so every queued generation gets the correct regional prompt.

* (fix) Update invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts

Fixes race condition identified during copilot review.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update invokeai/frontend/web/src/features/controlLayers/konva/CanvasStateApiModule.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-29 02:01:21 +00:00
Lincoln Stein
f1624a6215 Merge branch 'main' into copilot/add-unload-model-option 2025-12-28 20:38:42 -05:00
Alexander Eichhorn
b7e28e4fa6 fix(ui): make Z-Image model selects mutually exclusive (#8717)
* fix(ui): make Z-Image model selects mutually exclusive

VAE and Qwen3 Encoder selects are disabled when Qwen3 Source is selected,
and vice versa. This prevents invalid model combinations.

* feat(ui): auto-select Z-Image component models on model change

When switching to a Z-Image model, automatically set valid defaults
if no configuration exists:
- Prefers Qwen3 Source (Diffusers model) if available
- Falls back to Qwen3 Encoder + FLUX VAE combination

This ensures the generate button is enabled immediately after selecting
a Z-Image model, without requiring manual configuration.

* fix(ui): save and restore Qwen3 Source model in metadata

Qwen3 Source (Diffusers Z-Image) model was not being saved to image
metadata or restored during Remix. This adds:
- Saving qwen3_source to metadata in buildZImageGraph
- ZImageQwen3SourceModel metadata handler for parsing and recall
- i18n translation for qwen3Source
2025-12-28 20:25:35 -05:00
Alexander Eichhorn
d7d051200f fix(z_image): use unrestricted image self-attention for regional prompting (#8718)
Changes image self-attention from restricted (region-isolated) to unrestricted
(all image tokens can attend to each other), similar to the FLUX approach.

This fixes the issue where ZImage-Turbo with multiple regional guidance layers
would generate two separate/disconnected images instead of compositing them
into a single unified image.

The regional text-image attention remains restricted so that each region still
responds to its corresponding prompt.

Fixes #8715
2025-12-28 11:32:50 -05:00
Alexander Eichhorn
0f830ddd00 Ruff format 2025-12-28 12:37:21 +01:00
Alexander Eichhorn
9617140b7f Merge branch 'feature/zimage-scheduler-support' of https://github.com/Pfannkuchensack/InvokeAI into feature/zimage-scheduler-support 2025-12-28 12:29:19 +01:00
Alexander Eichhorn
bc4783028f Merge branch 'main' into feature/zimage-scheduler-support 2025-12-28 12:29:14 +01:00
Alexander Eichhorn
16fedfb538 fix(schedulers): prevent progress percentage overflow with LCM scheduler
LCM scheduler may have more internal timesteps than user-facing steps,
causing user_step to exceed total_steps. This resulted in progress
percentage > 1.0, which caused a pydantic validation error.

Fix: Only call step_callback when user_step <= total_steps.
2025-12-28 12:22:28 +01:00
Lincoln Stein
d781a3b8a2 Merge branch 'main' into copilot/add-unload-model-option 2025-12-27 23:27:19 -05:00
blessedcoolant
7182ff26dc fix(ui): misaligned Color Compensation Option (#8714) 2025-12-27 23:11:48 -05:00
Lincoln Stein
95ee27d5c0 Merge branch 'main' into copilot/add-unload-model-option 2025-12-27 21:56:55 -05:00
Lincoln Stein
b4f05d3fe7 Merge branch 'main' into feature/zimage-scheduler-support 2025-12-27 21:50:05 -05:00
Josh Corbett
8deafabe6b feat(prompts): 💄 increase prompt font size (#8712)
* feat(prompts): 💄 increase prompt font size

* style(prompts): 🚨 satisfy linter
2025-12-27 21:18:23 -05:00
copilot-swe-agent[bot]
1bd1c76a2c Change default model_cache_keep_alive to 5 minutes
Changed the default value of model_cache_keep_alive from 0 (indefinite)
to 5 minutes as requested. This means models will now be automatically
cleared from cache after 5 minutes of inactivity by default, unless
users explicitly configure a different value.

Users can still set it to 0 in their config to get the old behavior
of keeping models indefinitely.

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
2025-12-28 02:11:20 +00:00
Lincoln Stein
56fd1da888 Merge branch 'main' into copilot/add-unload-model-option 2025-12-27 21:08:17 -05:00
Alexander Eichhorn
0956ce0cd3 Merge branch 'main' into feature/zimage-scheduler-support 2025-12-28 00:44:10 +01:00
blessedcoolant
d42bf9c941 fix(model-manager): add Z-Image LoRA/DoRA detection support (#8709)
## Summary

Fix Z-Image LoRA/DoRA model detection failing during installation.

Z-Image LoRAs use different key patterns than SD/SDXL LoRAs. The base
`LoRA_LyCORIS_Config_Base` class only checked for key suffixes like
`lora_A.weight` and `lora_B.weight`, but Z-Image LoRAs (especially those
in DoRA format) use:
- `lora_down.weight` / `lora_up.weight` (standard LoRA format)
- `dora_scale` (DoRA weight decomposition)

This PR overrides `_validate_looks_like_lora` in
`LoRA_LyCORIS_ZImage_Config` to recognize Z-Image specific patterns:
- Keys starting with `diffusion_model.layers.` (Z-Image S3-DiT
architecture)
- Keys ending with `lora_down.weight`, `lora_up.weight`,
`lora_A.weight`, `lora_B.weight`, or `dora_scale`

## Related Issues / Discussions

Fixes installation of Z-Image LoRAs trained with DoRA (Weight-Decomposed
Low-Rank Adaptation).

## QA Instructions

1. Download a Z-Image LoRA in DoRA format (e.g., from CivitAI with keys
like `diffusion_model.layers.X.attention.to_k.lora_down.weight`)
2. Try to install the LoRA via Model Manager
3. Verify the model is recognized as a Z-Image LoRA and installs
successfully
4. Verify the LoRA can be applied when generating with Z-Image

## Merge Plan

Standard merge, no special considerations.

## Checklist

- [x] _The PR has a short but descriptive title, suitable for a
changelog_
- [ ] _Tests added / updated (if applicable)_
- [ ] _Changes to a redux slice have a corresponding migration_
- [ ] _Documentation added / updated (if applicable)_
- [ ] _Updated `What's New` copy (if doing a release after this PR)_
2025-12-27 23:10:06 +05:30
Alexander Eichhorn
d403587c7f Merge branch 'fix/z-image-lora-dora-detection' of https://github.com/Pfannkuchensack/InvokeAI into fix/z-image-lora-dora-detection 2025-12-27 09:17:33 +01:00
Alexander Eichhorn
355c985cc3 fix(model-manager): add Z-Image LoRA/DoRA detection and loading support
Two fixes for Z-Image LoRA support:

1. Override _validate_looks_like_lora in LoRA_LyCORIS_ZImage_Config to
   recognize Z-Image specific LoRA formats that use different key patterns
   than SD/SDXL LoRAs. Z-Image LoRAs use lora_down.weight/lora_up.weight
   and dora_scale suffixes instead of lora_A.weight/lora_B.weight.

2. Fix _group_by_layer in z_image_lora_conversion_utils.py to correctly
   group LoRA keys by layer name. The previous logic used rsplit with
   maxsplit=2 which incorrectly grouped keys like:
   - "to_k.alpha" -> layer "diffusion_model.layers.17.attention"
   - "lora_down.weight" -> layer "diffusion_model.layers.17.attention.to_k"

   Now uses suffix matching to ensure all keys for a layer are grouped
   together (alpha, dora_scale, lora_down.weight, lora_up.weight).
2025-12-27 09:17:29 +01:00
Alexander Eichhorn
41742146e2 fix(model-manager): add Z-Image LoRA/DoRA detection support
Override _validate_looks_like_lora in LoRA_LyCORIS_ZImage_Config to
recognize Z-Image specific LoRA formats that use different key patterns
than SD/SDXL LoRAs.

Z-Image LoRAs (including DoRA format) use keys like:
- diffusion_model.layers.X.attention.to_k.lora_down.weight
- diffusion_model.layers.X.attention.to_k.dora_scale

The base LyCORIS config only checked for lora_A.weight/lora_B.weight
suffixes, missing the lora_down.weight/lora_up.weight and dora_scale
patterns used by Z-Image LoRAs.
2025-12-27 07:06:12 +01:00
Jonathan
eb516e1998 Merge branch 'main' into feature/zimage-scheduler-support 2025-12-26 22:06:49 -05:00
Lincoln Stein
0b1befa9ab (chore) Prep for v6.10.0rc2 (#8701) 2025-12-26 18:26:04 -05:00
Alexander Eichhorn
bd678b1c95 fix ruff check 2025-12-26 21:22:46 +01:00
Alexander Eichhorn
56bef0b089 feat(z-image): add scheduler selection for Z-Image models
Add support for alternative diffusers Flow Matching schedulers for Z-Image:
- Euler (default) - 1st order, optimized for Z-Image-Turbo (8 steps)
- Heun (2nd order) - Better quality, 2x slower
- LCM - Optimized for few-step generation

Backend:
- Extend schedulers.py with Z-Image scheduler types and mapping
- Add scheduler InputField to z_image_denoise invocation (v1.3.0)
- Refactor denoising loop to support diffusers schedulers

Frontend:
- Add zImageScheduler to Redux state in paramsSlice
- Create ParamZImageScheduler component for Linear UI
- Add scheduler to buildZImageGraph for generation
2025-12-26 21:15:26 +01:00
Alexander Eichhorn
99fc1243cb feat(flux): add scheduler selection for Flux models
Add support for alternative diffusers Flow Matching schedulers:
- Euler (default, 1st order)
- Heun (2nd order, better quality, 2x slower)
- LCM (optimized for few steps)

Backend:
- Add schedulers.py with scheduler type definitions and class mapping
- Modify denoise.py to accept optional scheduler parameter
- Add scheduler InputField to flux_denoise invocation (v4.2.0)

Frontend:
- Add fluxScheduler to Redux state and paramsSlice
- Create ParamFluxScheduler component for Linear UI
- Add scheduler to buildFLUXGraph for generation
2025-12-26 20:53:59 +01:00
Lincoln Stein
a7205e4e36 Merge branch 'main' into copilot/add-unload-model-option 2025-12-25 21:33:59 -05:00
Alexander Eichhorn
65efc3db7d Feature: Add Z-Image-Turbo regional guidance (#8672)
* feat: Add Regional Guidance support for Z-Image model

Implements regional prompting for Z-Image (S3-DiT Transformer) allowing
different prompts to affect different image regions using attention masks.

Backend changes:
- Add ZImageRegionalPromptingExtension for mask preparation
- Add ZImageTextConditioning and ZImageRegionalTextConditioning data classes
- Patch transformer forward to inject 4D regional attention masks
- Use additive float mask (0.0 attend, -inf block) in bfloat16 for compatibility
- Alternate regional/full attention layers for global coherence

Frontend changes:
- Update buildZImageGraph to support regional conditioning collectors
- Update addRegions to create z_image_text_encoder nodes for regions
- Update addZImageLoRAs to handle optional negCond when guidance_scale=0
- Add Z-Image validation (no IP adapters, no autoNegative)

* @Pfannkuchensack
Fix windows path again

* ruff check fix

* ruff formating

* fix(ui): Z-Image CFG guidance_scale check uses > 1 instead of > 0

Changed the guidance_scale check from > 0 to > 1 for Z-Image models.
Since Z-Image uses guidance_scale=1.0 as "no CFG" (matching FLUX convention),
negative conditioning should only be created when guidance_scale > 1.

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2025-12-26 02:25:38 +00:00
Lincoln Stein
de1aa557b8 chore: bump version to v6.10.0rc1 (#8695)
* chore: bump version to v6.10.0rc1

* docs: fix names of code owners in release doc
2025-12-26 02:08:14 +00:00
Lincoln Stein
b9493ddce7 Workaround for Windows being unable to remove tmp directories when installing GGUF files (#8699)
* (bugfix)(mm) work around Windows being unable to rmtree tmp directories after GGUF install

* (style) fix ruff error

* (fix) add workaround for Windows Permission Denied on GGUF file move() call

* (fix) perform torch copy() in GGUF reader to avoid deletion failures on Windows

* (style) fix ruff formatting issues
2025-12-26 02:02:39 +00:00
Lincoln Stein
ca14c5c9e1 Merge branch 'main' into copilot/add-unload-model-option 2025-12-25 00:08:28 -05:00
Josh Corbett
ddb85ca669 fix(prompts): 🐛 prompt attention behaviors, add tests (#8683)
* fix(prompts): 🐛 prompt attention adjust elevation edge cases, added tests

* refactor(prompts): ♻️ create attention edit helper for prompt boxes

* feat(prompts):  apply attention keybinds to negative prompt

* feat(prompts): 🚀 reconsider behaviors, simplify code

* fix(prompts): 🐛 keybind attention update not tracked by undo/redo

* feat(prompts):  overhaul prompt attention behavior

* fix(prompts): 🩹 remove unused type

* fix(prompts): 🩹 remove unused `Token` type

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2025-12-24 17:38:24 -05:00
Lincoln Stein
5b69403ba8 Merge branch 'main' into copilot/add-unload-model-option 2025-12-24 15:39:46 -05:00
Alexander Eichhorn
ac245cbf6c feat(backend): add support for xlabs Flux LoRA format (#8686)
Add support for loading Flux LoRA models in the xlabs format, which uses
keys like `double_blocks.X.processor.{qkv|proj}_lora{1|2}.{down|up}.weight`.

The xlabs format maps:
- lora1 -> img_attn (image attention stream)
- lora2 -> txt_attn (text attention stream)
- qkv -> query/key/value projection
- proj -> output projection

Changes:
- Add FluxLoRAFormat.XLabs enum value
- Add flux_xlabs_lora_conversion_utils.py with detection and conversion
- Update formats.py to detect xlabs format
- Update lora.py loader to handle xlabs format
- Update model probe to accept recognized Flux LoRA formats
- Add unit tests for xlabs format detection and conversion

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2025-12-24 20:18:11 +00:00
Alexander Eichhorn
5be1e03d73 Feature/user workflow tags (#8698)
* Feature: Add Tag System for user made Workflows

* feat(ui): display tags on workflow library tiles

Show workflow tags at the bottom of each tile in the workflow browser,
making it easier to identify workflow categories at a glance.

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2025-12-24 14:54:22 -05:00
Josh Corbett
87314142b5 feat(hotkeys modal): loading state + performance improvements (#8694)
* feat(hotkeys modal):  loading state + performance improvements

* feat(hotkeys modal): add tooltip to edit button and adjust layout spacing

* style(hotkeys modal): 🚨 satisfy the linter

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2025-12-24 14:39:14 -05:00
Alexander Eichhorn
4cb9b8d97d Feature: add prompt template node (#8680)
* feat(nodes): add Prompt Template node

Add a new node that applies Style Preset templates to prompts in workflows.
The node takes a style preset ID and positive/negative prompts as inputs,
then replaces {prompt} placeholders in the template with the provided prompts.

This makes Style Preset templates accessible in Workflow mode, enabling
users to apply consistent styling across their workflow-based generations.

* feat(nodes): add StylePresetField for database-driven preset selection

Adds a new StylePresetField type that enables dropdown selection of
style presets from the database in the workflow editor.

Changes:
- Add StylePresetField to backend (fields.py)
- Update Prompt Template node to use StylePresetField instead of string ID
- Add frontend field type definitions (zod schemas, type guards)
- Create StylePresetFieldInputComponent with Combobox
- Register field in InputFieldRenderer and nodesSlice
- Add translations for preset selection

* fix schema.ts on windows.

* chore(api): regenerate schema.ts after merge

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-24 14:33:16 -05:00
Lincoln Stein
83deb0233e Merge remote-tracking branch 'refs/remotes/origin/copilot/add-unload-model-option' into copilot/add-unload-model-option 2025-12-24 00:44:32 -05:00
Lincoln Stein
8ebb6dd3d9 (chore) regenerate typescript schema 2025-12-24 00:43:06 -05:00
copilot-swe-agent[bot]
b7afd9b5b3 Fix test failures caused by MagicMock TypeError
Configure mock logger to return a valid log level for getEffectiveLevel()
to prevent TypeError when comparing with logging.DEBUG constant.

The issue was that ModelCache._log_cache_state() checks
self._logger.getEffectiveLevel() > logging.DEBUG, and when the logger
is a MagicMock without configuration, getEffectiveLevel() returns another
MagicMock, causing a TypeError when compared with an int.

Fixes all 4 test failures in test_model_cache_timeout.py

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
2025-12-24 05:42:45 +00:00
copilot-swe-agent[bot]
4987b4da1c Fix timeout message appearing during active generation
Only log "Clearing model cache" message when there are actually unlocked
models to clear. This prevents the misleading message from appearing during
active generation when all models are locked.

Changes:
- Check for unlocked models before logging clear message
- Add count of unlocked models in log message
- Add debug log when all models are locked
- Improves user experience by avoiding confusing messages

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
2025-12-24 05:31:11 +00:00
Lincoln Stein
a21b7792d8 (chore) regenerate config docstrings 2025-12-24 00:29:48 -05:00
Lincoln Stein
8819cc30be (chore) regenerate schema.ts 2025-12-24 00:28:55 -05:00
Lincoln Stein
9d1de81fe2 (style) correct ruff formatting error 2025-12-24 00:19:25 -05:00
Lincoln Stein
1e15b8c106 Merge branch 'main' into copilot/add-unload-model-option 2025-12-24 00:14:45 -05:00
Alexander Eichhorn
21138e5d52 fix support multi-subfolder downloads for Z-Image Qwen3 encoder (#8692)
* fix(model-install): support multi-subfolder downloads for Z-Image Qwen3 encoder

The Z-Image Qwen3 text encoder requires both text_encoder and tokenizer
subfolders from the HuggingFace repo, but the previous implementation
only downloaded the text_encoder subfolder, causing model identification
to fail.

Changes:
- Add subfolders property to HFModelSource supporting '+' separated paths
- Extend filter_files() and download_urls() to handle multiple subfolders
- Update _multifile_download() to preserve subfolder structure
- Make Qwen3Encoder probe check both nested and direct config.json paths
- Update Qwen3EncoderLoader to handle both directory structures
- Change starter model source to text_encoder+tokenizer

* ruff format

* fix schema description

* fix schema description

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2025-12-23 23:39:43 -05:00
copilot-swe-agent[bot]
8d76b4e4d4 Fix ruff whitespace errors and improve timeout logging
- Remove all trailing whitespace (W293 errors)
- Add debug logging when timeout fires but activity detected
- Add debug logging when timeout fires but cache is empty
- Only log "Clearing model cache" message when actually clearing
- Prevents misleading timeout messages during active generation

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
2025-12-24 04:05:57 +00:00
Lincoln Stein
9662d1fdb6 Merge branch 'main' into copilot/add-unload-model-option 2025-12-23 22:48:11 -05:00
Alexander Eichhorn
39114b0ad0 Feature (UI): add model path update for external models (#8675)
* feat(ui): add model path update for external models

Add ability to update file paths for externally managed models (models with
absolute paths). Invoke-controlled models (with relative paths in the models
directory) are excluded from this feature to prevent breaking internal
model management.

- Add ModelUpdatePathButton component with modal dialog
- Only show button for external models (absolute path check)
- Add translations for path update UI elements

* Added support for Windows UNC paths in ModelView.tsx:38-41. The isExternalModel function now detects:
Unix absolute paths: /home/user/models/...
Windows drive paths: C:\Models\... or D:/Models/...
Windows UNC paths: \\ServerName\ShareName\... or //ServerName/ShareName/...

* fix(ui): validate path format in Update Path modal to prevent invalid paths

When updating an external model's path, the new path is now validated to ensure
it follows an absolute path format (Unix, Windows drive, or UNC). This prevents
users from accidentally entering invalid paths that would cause the Update Path
button to disappear, leaving them unable to correct the mistake.

* fix(ui): extract isExternalModel to separate file to fix circular dependency

Moves the isExternalModel utility function to its own file to break the
circular dependency between ModelView.tsx and ModelUpdatePathButton.tsx.

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2025-12-23 22:46:50 -05:00
Josh Corbett
3fe5f62c48 feat(hotkeys): Overhaul hotkeys modal UI (#8682)
* feat(hotkeys):  overhaul hotkeys modal UI

* fix(model manager): 🩹 improved check for hotkey search clear button

* fix(model manager): 🩹 remove unused exports

* feat(starter-models): add Z-Image Turbo starter models

Add Z-Image Turbo and related models to the starter models list:
- Z-Image Turbo (full precision, ~13GB)
- Z-Image Turbo quantized (GGUF Q4_K, ~4GB)
- Z-Image Qwen3 Text Encoder (full precision, ~8GB)
- Z-Image Qwen3 Text Encoder quantized (GGUF Q6_K, ~3.3GB)
- Z-Image ControlNet Union (Canny, HED, Depth, Pose, MLSD, Inpainting)

The quantized Turbo model includes the quantized Qwen3 encoder as a
dependency for automatic installation.

* feat(starter-models): add Z-Image Q8 quant and ControlNet Tile

Add higher quality Q8_0 quantization option for Z-Image Turbo (~6.6GB)
to complement existing Q4_K variant, providing better quality for users
with more VRAM.

Add dedicated Z-Image ControlNet Tile model (~6.7GB) for upscaling and
detail enhancement workflows.

* feat(hotkeys):  overhaul hotkeys modal UI

* feat(hotkeys modal): 💄 shrink add hotkey button

* fix(hotkeys): normalization and detection issues

* style: 🚨 satisfy the linter

* fix(hotkeys modal): 🩹 remove unused exports

---------

Co-authored-by: Alexander Eichhorn <alex@eichhorn.dev>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2025-12-23 22:24:00 -05:00
Josh Corbett
73c6b31011 feat(model manager): 💄 refactor model manager bulk actions UI (#8684)
* feat(model manager): 💄 refactor model manager bulk actions UI

* feat(model manager): 💄 tweak model list item ui for checkbox selects

* style(model manager): 🚨 satisfy the linter

* feat(model manager): 💄 tweak search and actions dropdown placement

* refactor(model manager): 🔥 remove unused `ModelListHeader` component

* fix(model manager): 🐛 list items overlapping sticky headers

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2025-12-23 22:17:07 -05:00
copilot-swe-agent[bot]
b16717bbf8 Explicitly pass all ModelCache constructor parameters
- Add explicit storage_device parameter (cpu)
- Add explicit log_memory_usage parameter from config
- Improves code clarity and configuration transparency

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
2025-12-24 00:30:51 +00:00
copilot-swe-agent[bot]
c3217d8a08 Address code review feedback
- Remove unused variable in test
- Add clarifying comment for daemon thread setting
- Add detailed comment explaining cache clearing with 1000 GB value
- Improve code documentation

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
2025-12-24 00:27:39 +00:00
blessedcoolant
f82bcd40fc fix: CFG Scale min value reset to zero (#8691)
No longer needed coz Z Image works at 1.0
2025-12-23 19:27:30 -05:00
copilot-swe-agent[bot]
2500153ed8 Fix race condition in timeout mechanism
- Added clarifying comment that _record_activity is called with lock held
- Enhanced double-check in _on_timeout for thread safety
- Added lock protection to shutdown method
- Improved handling of edge cases where timer fires during activity

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
2025-12-24 00:26:01 +00:00
copilot-swe-agent[bot]
75a14e2a4b Add unit tests for model cache timeout functionality
- Created test_model_cache_timeout.py with comprehensive tests
- Tests timeout clearing behavior
- Tests activity resetting timeout
- Tests no-timeout default behavior
- Tests shutdown canceling timers

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
2025-12-24 00:24:31 +00:00
copilot-swe-agent[bot]
9bbd2b3f11 Add model_cache_keep_alive config option and timeout mechanism
- Added model_cache_keep_alive config field (minutes, default 0 = infinite)
- Implemented timeout tracking in ModelCache class
- Added _record_activity() to track model usage
- Added _on_timeout() to auto-clear cache when timeout expires
- Added shutdown() method to clean up timers
- Integrated timeout with get(), lock(), unlock(), and put() operations
- Updated ModelManagerService to pass keep_alive parameter
- Added cleanup in stop() method

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
2025-12-24 00:22:59 +00:00
copilot-swe-agent[bot]
c26445253c Initial plan 2025-12-24 00:16:17 +00:00
blessedcoolant
5a0b227256 feat(starter-models): add Z-Image Turbo starter models (#8689)
## Summary

Add Z-Image Turbo and related models to the starter models list for easy
installation via the Model Manager:

- **Z-Image Turbo** - Full precision Diffusers format (~13GB)
- **Z-Image Turbo (quantized)** - GGUF Q4_K format (~4GB)
- **Z-Image Qwen3 Text Encoder** - Full precision (~8GB)
- **Z-Image Qwen3 Text Encoder (quantized)** - GGUF Q6_K format (~3.3GB)
- **Z-Image ControlNet Union** - Unified ControlNet supporting Canny,
HED, Depth, Pose, MLSD, and Inpainting modes

The quantized Turbo model includes the quantized Qwen3 encoder as a
dependency for automatic installation.

## Related Issues / Discussions

Builds on the Z-Image Turbo support added in main.

## QA Instructions

1. Open Model Manager → Starter Models
2. Search for "Z-Image"
3. Verify all 5 models appear with correct descriptions
4. Install the quantized version and confirm the Qwen3 encoder
dependency is also installed

## Merge Plan

Standard merge, no special considerations.

## Checklist

- [x] _The PR has a short but descriptive title, suitable for a
changelog_
- [ ] _Tests added / updated (if applicable)_
- [ ] _Changes to a redux slice have a corresponding migration_
- [ ] _Documentation added / updated (if applicable)_
- [ ] _Updated `What's New` copy (if doing a release after this PR)_
2025-12-23 08:31:34 +05:30
blessedcoolant
1b5d91d1cf Merge branch 'main' into feat/z-image-starter-models 2025-12-23 08:27:25 +05:30
Alexander Eichhorn
a748519e92 feat(starter-models): add Z-Image Q8 quant and ControlNet Tile
Add higher quality Q8_0 quantization option for Z-Image Turbo (~6.6GB)
to complement existing Q4_K variant, providing better quality for users
with more VRAM.

Add dedicated Z-Image ControlNet Tile model (~6.7GB) for upscaling and
detail enhancement workflows.
2025-12-23 03:27:09 +01:00
blessedcoolant
90e34002f0 fix(z-image): Fix padding token shape mismatch for GGUF models (#8690)
## Summary

Fix shape mismatch when loading GGUF-quantized Z-Image transformer
models.

GGUF Z-Image models store `x_pad_token` and `cap_pad_token` with shape
`[3840]`, but diffusers `ZImageTransformer2DModel` expects `[1, 3840]`
(with batch dimension). This caused a `RuntimeError` on Linux systems
when loading models like `z_image_turbo-Q4_K.gguf`.

The fix:
- Dequantizes GGMLTensors first (since they don't support `unsqueeze`)
- Reshapes the tensors to add the missing batch dimension

## Related Issues / Discussions

Reported by Linux user using:
-
https://huggingface.co/leejet/Z-Image-Turbo-GGUF/resolve/main/z_image_turbo-Q4_K.gguf
-
https://huggingface.co/worstplayer/Z-Image_Qwen_3_4b_text_encoder_GGUF/resolve/main/Qwen_3_4b-Q6_K.gguf

## QA Instructions

1. Install a GGUF-quantized Z-Image model (e.g.,
`z_image_turbo-Q4_K.gguf`)
2. Install a Qwen3 GGUF encoder
3. Run a Z-Image generation
4. Verify no `RuntimeError: size mismatch for x_pad_token` error occurs

## Merge Plan

None, straightforward fix.

## Checklist

- [x] _The PR has a short but descriptive title, suitable for a
changelog_
- [ ] _Tests added / updated (if applicable)_
- [ ] _Changes to a redux slice have a corresponding migration_
- [ ] _Documentation added / updated (if applicable)_
- [ ] _Updated `What's New` copy (if doing a release after this PR)_
2025-12-23 06:04:40 +05:30
blessedcoolant
7068cf956a Merge branch 'main' into pr/8690 2025-12-23 05:59:49 +05:30
blessedcoolant
aa764f8bf4 Feature: z-image Turbo Control Net (#8679)
## Summary

Add support for Z-Image ControlNet V2.0 alongside the existing V1
support.

**Key changes:**
- Auto-detect `control_in_dim` from adapter weights (16 for V1, 33 for
V2.0)
- Auto-detect `n_refiner_layers` from state dict
- Add zero-padding for V2.0's additional control channels (diffusers
approach)
- Use `accelerate.init_empty_weights()` for more efficient model
creation
- Add `ControlNet_Checkpoint_ZImage_Config` to frontend schema

## Related Issues / Discussions

Part of Z-Image feature implementation.

## QA Instructions

1. Load a Z-Image ControlNet V1 model (control_in_dim=16) and verify it
works
2. Load a Z-Image ControlNet V2.0 model (control_in_dim=33) and verify
it works
3. Test with different control types: Canny, Depth, Pose
4. Recommended `control_context_scale`: 0.65-0.80

## Merge Plan

Can be merged after review. No special considerations needed.

## Checklist

- [x] _The PR has a short but descriptive title, suitable for a
changelog_
- [ ] _Tests added / updated (if applicable)_
- [ ] _Changes to a redux slice have a corresponding migration_
- [ ] _Documentation added / updated (if applicable)_
- [ ] _Updated `What's New` copy (if doing a release after this PR)_
2025-12-23 05:58:58 +05:30
Alexander Eichhorn
73be5e5d35 Merge branch 'main' into feature/z-image-control 2025-12-22 22:56:30 +01:00
DustyShoe
259304bac5 Feature(UI): add extract masked area from raster layers (#8667)
* chore: localize extraction errors

* chore: rename extract masked area menu item

* chore: rename inpaint mask extract component

* fix: use mask bounds for extraction region

* Prettier format applied to InpaintMaskMenuItemsExtractMaskedArea.tsx

* Fix base64 image import bug in extracted area in InpaintMaskMenuItemsExtractMaskedArea.tsx and removed unused locales entries in en.json

* Fix formatting issue in InpaintMaskMenuItemsExtractMaskedArea.tsx

* Minor comment fix

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2025-12-22 15:57:27 -05:00
Alexander Eichhorn
2be701cfe3 Feature: Add Tag System for user made Workflows (#8673)
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2025-12-22 15:41:48 -05:00
blessedcoolant
874b547598 chore: format code for ruff checks 2025-12-23 01:04:22 +05:30
blessedcoolant
7b9ce35806 Merge branch 'main' into pr/8679 2025-12-23 01:03:43 +05:30
Alexander Eichhorn
84f3e44a5d Merge branch 'main' into feat/z-image-starter-models 2025-12-22 20:16:05 +01:00
Alexander Eichhorn
5264b7511c Merge branch 'main' into fix/z-image-gguf-padding-token-shape 2025-12-22 20:15:18 +01:00
Alexander Eichhorn
f8b1f42f6d fix(z-image): Fix padding token shape mismatch for GGUF models
GGUF Z-Image models store x_pad_token and cap_pad_token with shape [dim],
but diffusers ZImageTransformer2DModel expects [1, dim]. This caused a
RuntimeError when loading GGUF-quantized Z-Image models.

The fix dequantizes GGMLTensors first (since they don't support unsqueeze),
then reshapes to add the batch dimension.
2025-12-22 18:31:57 +01:00
Josh Corbett
e1acb636d8 fix(ui): 🐛 HotkeysModal and SettingsModal initial focus (#8687)
* fix(ui): 🐛 `HotkeysModal` and `SettingsModal` initial focus

instead of using the `initialFocusRef` prop, the `Modal` component was focusing on the last available Button. This is a workaround that uses `tabIndex` instead which seems to be working.

Closes #8685

* style: 🚨 satisfy linter

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2025-12-22 11:20:44 -05:00
Alexander Eichhorn
b08accd4be feat(starter-models): add Z-Image Turbo starter models
Add Z-Image Turbo and related models to the starter models list:
- Z-Image Turbo (full precision, ~13GB)
- Z-Image Turbo quantized (GGUF Q4_K, ~4GB)
- Z-Image Qwen3 Text Encoder (full precision, ~8GB)
- Z-Image Qwen3 Text Encoder quantized (GGUF Q6_K, ~3.3GB)
- Z-Image ControlNet Union (Canny, HED, Depth, Pose, MLSD, Inpainting)

The quantized Turbo model includes the quantized Qwen3 encoder as a
dependency for automatic installation.
2025-12-22 15:04:27 +01:00
Alexander Eichhorn
3668d5b83b feat(z-image): add Extension-based Z-Image ControlNet support
Implement Z-Image ControlNet as an Extension pattern (similar to FLUX ControlNet)
instead of merging control weights into the base transformer. This provides:
- Lower memory usage (no weight duplication)
- Flexibility to enable/disable control per step
- Cleaner architecture with separate control adapter

Key implementation details:
- ZImageControlNetExtension: computes control hints per denoising step
- z_image_forward_with_control: custom forward pass with hint injection
- patchify_control_context: utility for control image patchification
- ZImageControlAdapter: standalone adapter with control_layers and noise_refiner

Architecture matches original VideoX-Fun implementation:
- Hints computed ONCE using INITIAL unified state (before main layers)
- Hints injected at every other main transformer layer (15 control blocks)
- Control signal added after each designated layer's forward pass

V2.0 ControlNet support (control_in_dim=33):
- Channels 0-15: control image latents
- Channels 16-31: reference image (zeros for pure control)
- Channel 32: inpaint mask (1.0 = don't inpaint, use control signal)
2025-12-21 22:30:28 +01:00
Alexander Eichhorn
1c13ca8159 style: apply ruff formatting 2025-12-21 18:52:12 +01:00
Alexander Eichhorn
3ed0e55d9d fix: resolve linting errors in Z-Image ControlNet support
- Add missing ControlNet_Checkpoint_ZImage_Config import
- Remove unused imports (Any, Dict, ADALN_EMBED_DIM, is_torch_version)
- Add strict=True to zip() calls
- Replace mutable list defaults with immutable tuples
- Replace dict() calls with literal syntax
- Sort imports in z_image_denoise.py
2025-12-21 18:50:43 +01:00
Alexander Eichhorn
8db8aa8594 Add Z-Image ControlNet V2.0 support
VRAM usage is high.

- Auto-detect control_in_dim from adapter weights (16 for V1, 33 for V2.0)
- Auto-detect n_refiner_layers from state dict
- Add zero-padding for V2.0's additional channels
- Use accelerate.init_empty_weights() for efficient model creation
- Add ControlNet_Checkpoint_ZImage_Config to frontend schema
2025-12-21 18:43:02 +01:00
Alexander Eichhorn
456d578f20 WIP not working.
feat: Add Z-Image ControlNet support with spatial conditioning

Add comprehensive ControlNet support for Z-Image models including:

Backend:
- New ControlNet_Checkpoint_ZImage_Config for Z-Image control adapter models
- Z-Image control key detection (_has_z_image_control_keys) to identify control layers
- ZImageControlAdapter loader for standalone control models
- ZImageControlTransformer2DModel combining base transformer with control layers
- Memory-efficient model loading by building combined state dict
2025-12-21 18:43:02 +01:00
blessedcoolant
ab6b6721dc Feature: Add Z-Image-Turbo model support (#8671)
Add comprehensive support for Z-Image-Turbo (S3-DiT) models including:

Backend:
- New BaseModelType.ZImage in taxonomy
- Z-Image model config classes (ZImageTransformerConfig,
Qwen3TextEncoderConfig)
- Model loader for Z-Image transformer and Qwen3 text encoder
- Z-Image conditioning data structures
- Step callback support for Z-Image with FLUX latent RGB factors

Invocations:
- z_image_model_loader: Load Z-Image transformer and Qwen3 encoder
- z_image_text_encoder: Encode prompts using Qwen3 with chat template
- z_image_denoise: Flow matching denoising with time-shifted sigmas
- z_image_image_to_latents: Encode images to 16-channel latents
- z_image_latents_to_image: Decode latents using FLUX VAE

Frontend:
- Z-Image graph builder for text-to-image generation
- Model picker and validation updates for z-image base type
- CFG scale now allows 0 (required for Z-Image-Turbo)
- Clip skip disabled for Z-Image (uses Qwen3, not CLIP)
- Optimal dimension settings for Z-Image (1024x1024)

Technical details:
- Uses Qwen3 text encoder (not CLIP/T5)
- 16 latent channels with FLUX-compatible VAE
- Flow matching scheduler with dynamic time shift
- 8 inference steps recommended for Turbo variant
- bfloat16 inference dtype

## Summary

<!--A description of the changes in this PR. Include the kind of change
(fix, feature, docs, etc), the "why" and the "how". Screenshots or
videos are useful for frontend changes.-->

## Related Issues / Discussions

<!--WHEN APPLICABLE: List any related issues or discussions on github or
discord. If this PR closes an issue, please use the "Closes #1234"
format, so that the issue will be automatically closed when the PR
merges.-->

## QA Instructions

- Install a Z-Image-Turbo model (e.g., from HuggingFace)
- Select the model in the Model Picker
- Generate a text-to-image with:
- CFG Scale: 0
- Steps: 8
- Resolution: 1024x1024
- Verify the generated image is coherent (not noise)

## Merge Plan

Standard merge, no special considerations needed.

## Checklist

- [x] _The PR has a short but descriptive title, suitable for a
changelog_
- [ ] _Tests added / updated (if applicable)_
- [ ] _Changes to a redux slice have a corresponding migration_
- [ ] _Documentation added / updated (if applicable)_
- [ ] _Updated `What's New` copy (if doing a release after this PR)_
2025-12-21 22:11:37 +05:30
blessedcoolant
93a587da90 Merge branch 'main' into feat/z-image-turbo-support 2025-12-21 21:58:22 +05:30
blessedcoolant
87bebf9c28 chore: upgrade diffusers to 0.36.0 to support z image 2025-12-21 21:54:47 +05:30
Alexander Eichhorn
f417c269d1 fix(vae): Fix dtype mismatch in FP32 VAE decode mode
The previous mixed-precision optimization for FP32 mode only converted
some VAE decoder layers (post_quant_conv, conv_in, mid_block) to the
latents dtype while leaving others (up_blocks, conv_norm_out) in float32.
This caused "expected scalar type Half but found Float" errors after
recent diffusers updates.

Simplify FP32 mode to consistently use float32 for both VAE and latents,
removing the incomplete mixed-precision logic. This trades some VRAM
usage for stability and correctness.

Also removes now-unused attention processor imports.
2025-12-16 15:58:48 +01:00
Alexander Eichhorn
4ce0ef5260 stupid windows file path again. 2025-12-16 10:31:52 +01:00
Alexander Eichhorn
39cdcdc9e8 fix(z-image): remove unused WithMetadata and WithBoard mixins from denoise node
The Z-Image denoise node outputs latents, not images, so these mixins
were unnecessary. Metadata and board handling is correctly done in the
L2I (latents-to-image) node. This aligns with how FLUX denoise works.
2025-12-16 09:41:26 +01:00
Josh Corbett
926923bb2b feat(prompts): hotkey controlled prompt weighting (#8647)
* feat(prompts): add abstract syntax tree (AST) builder for prompts

* fix(prompts): add escaped parens to AST

* test(prompts): add AST tests

* fix(prompts): appease the linter

* perf(prompts): break up tokenize function into subroutines

* feat(prompts): add hotkey controlled prompt attention adjust

* fix(hotkeys): 🩹 add translations for hotkey dialog

* fix: 🏷️ remove unused exports

* fix(keybinds): 🐛 use `arrowup`/`arrowdown` over `up`/`down`

* refactor(prompts): ♻️ use better language for attention direction

* style: 🚨 appease the linter

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2025-12-15 21:53:58 -05:00
blessedcoolant
8785d9a3a9 chore: fix ruff checks 2025-12-14 19:51:22 +05:30
Alexander Eichhorn
1e72feb744 Remove unneeded Loggging 2025-12-14 06:44:29 +01:00
Alexander Eichhorn
3ee24cbdde Remove the ParamScheduler for z-images
Fixed the DEFAULT_TOKENIZER_SOURCE to Qwen/Qwen3-4B
2025-12-13 04:23:34 +01:00
Alexander Eichhorn
f9605e18a0 z-image-turbo-fp8-e5m2 works. the z-image-turbo_fp8_scaled_e4m3fn_KJ dont. 2025-12-10 17:15:54 +01:00
Alexander Eichhorn
8551ff8569 fix typegen 2025-12-10 15:04:39 +01:00
Alexander Eichhorn
fb1a99b650 feat(cache): add partial loading support for Z-Image RMSNorm and LayerNorm
- Add CustomDiffusersRMSNorm for diffusers.models.normalization.RMSNorm
- Add CustomLayerNorm for torch.nn.LayerNorm
- Register both in AUTOCAST_MODULE_TYPE_MAPPING

Enables partial loading (enable_partial_loading: true) for Z-Image models
by wrapping their normalization layers with device autocast support
2025-12-10 03:45:42 +01:00
Alexander Eichhorn
3b5d9c26d3 feat(z-image): add Qwen3 GGUF text encoder support and default parameters
- Add Qwen3EncoderGGUFLoader for llama.cpp GGUF quantized text encoders
- Convert llama.cpp key format (blk.X., token_embd) to PyTorch format
- Handle tied embeddings (lm_head.weight ↔ embed_tokens.weight)
- Dequantize embed_tokens for embedding lookups (GGMLTensor limitation)
- Add QK normalization key mappings (q_norm, k_norm) for Qwen3
- Set Z-Image defaults: steps=9, cfg_scale=0.0, width/height=1024
- Allow cfg_scale >= 0 (was >= 1) for Z-Image Turbo compatibility
- Add GGUF format detection for Qwen3 model probing
2025-12-10 03:07:07 +01:00
Alexander Eichhorn
0a986c2720 fix(ui): replace misused isCheckpointMainModelConfig with isFluxDevMainModelConfig
The FLUX Dev license warning in model pickers used isCheckpointMainModelConfig
incorrectly:
```
isCheckpointMainModelConfig(config) && config.variant === 'dev'
```

This caused a TypeScript error because CheckpointModelConfig type doesn't
include the 'variant' property (it's extracted as `{ type: 'main'; format:
'checkpoint' }` which doesn't narrow to include variant).

Changes:
- Add isFluxDevMainModelConfig type guard that properly checks
  base='flux' AND variant='dev', returning MainModelConfig
- Update MainModelPicker and InitialStateMainModelPicker to use new guard
- Remove isCheckpointMainModelConfig as it had no other usages

The function was removed because:
1. It was only used for detecting FLUX Dev models (incorrect use case)
2. No other code needs a generic "is checkpoint format" check
3. The pattern in this codebase is specific type guards per model variant
   (isFluxFillMainModelModelConfig, isRefinerMainModelModelConfig, etc.)
2025-12-09 08:18:17 +01:00
Alexander Eichhorn
3e862ced25 fix typegen wrong 2025-12-09 07:46:12 +01:00
Alexander Eichhorn
ba2475c3f0 fix(z-image): improve device/dtype compatibility and error handling
Add robust device capability detection for bfloat16, replacing hardcoded
dtype with runtime checks that fallback to float16/float32 on unsupported
hardware. This prevents runtime failures on GPUs and CPUs without bfloat16.

Key changes:
- Add TorchDevice.choose_bfloat16_safe_dtype() helper for safe dtype selection
- Fix LoRA device mismatch in layer_patcher.py (add device= to .to() call)
- Replace all assert statements with descriptive exceptions (TypeError/ValueError)
- Add hidden_states bounds check and apply_chat_template fallback in text encoder
- Add GGUF QKV tensor validation (divisible by 3 check)
- Fix CPU noise generation to use float32 for compatibility
- Remove verbose debug logging from LoRA conversion utils
2025-12-09 07:37:06 +01:00
Alexander Eichhorn
841372944f feat(z-image): add metadata recall for VAE and Qwen3 encoder
Add support for saving and recalling Z-Image component models (VAE and
Qwen3 Encoder) in image metadata.

Backend:
- Add qwen3_encoder field to CoreMetadataInvocation (version 2.1.0)

Frontend:
- Add vae and qwen3_encoder to Z-Image graph metadata
- Add Qwen3EncoderModel metadata handler for recall
- Add ZImageVAEModel metadata handler (uses zImageVaeModelSelected
  instead of vaeSelected to set Z-Image-specific VAE state)
- Add qwen3Encoder translation key

This enables "Recall Parameters" / "Remix Image" to restore the VAE
and Qwen3 Encoder settings used for Z-Image generations.
2025-12-09 07:12:36 +01:00
Alexander Eichhorn
e9d52734d1 feat(z-image): add single-file checkpoint support for Z-Image models
Add support for loading Z-Image transformer and Qwen3 encoder models
from single-file safetensors format (in addition to existing diffusers
directory format).

Changes:
- Add Main_Checkpoint_ZImage_Config and Main_GGUF_ZImage_Config for
  single-file Z-Image transformer models
- Add Qwen3Encoder_Checkpoint_Config for single-file Qwen3 text encoder
- Add ZImageCheckpointModel and ZImageGGUFCheckpointModel loaders with
  automatic key conversion from original to diffusers format
- Add Qwen3EncoderCheckpointLoader using Qwen3ForCausalLM with fast
  loading via init_empty_weights and proper weight tying for lm_head
- Update z_image_denoise to accept Checkpoint format models
2025-12-09 06:32:51 +01:00
Alexander Eichhorn
2e0cd4d68c Patch from @lstein for the update of diffusers 2025-12-06 03:12:50 +01:00
Alexander Eichhorn
b28d58b8ce Merge branch 'feat/z-image-turbo-support' of https://github.com/Pfannkuchensack/InvokeAI into feat/z-image-turbo-support 2025-12-05 01:12:34 +01:00
Alexander Eichhorn
4a1710b795 fix for the typegen-checks 2025-12-05 01:12:19 +01:00
Alexander Eichhorn
9f6d04c690 Merge branch 'main' into feat/z-image-turbo-support 2025-12-05 00:45:02 +01:00
Alexander Eichhorn
66729ea9eb Fix windows path again again again... 2025-12-03 03:28:43 +01:00
Alexander Eichhorn
280202908a feat: Add GGUF quantized Z-Image support and improve VAE/encoder flexibility
Add comprehensive support for GGUF quantized Z-Image models and improve component flexibility:

Backend:
- New Main_GGUF_ZImage_Config for GGUF quantized Z-Image transformers
- Z-Image key detection (_has_z_image_keys) to identify S3-DiT models
- GGUF quantization detection and sidecar LoRA patching for quantized models
- Qwen3Encoder_Qwen3Encoder_Config for standalone Qwen3 encoder models

Model Loader:
- Split Z-Image model
2025-12-02 20:31:11 +01:00
Alexander Eichhorn
2b062b21cd fix: Improve Flux AI Toolkit LoRA detection to prevent Z-Image misidentification
Move Flux layer structure check before metadata check to prevent misidentifying Z-Image LoRAs (which use `diffusion_model.layers.X`) as Flux AI Toolkit format. Flux models use `double_blocks` and `single_blocks` patterns which are now checked first regardless of metadata presence.
2025-12-02 15:50:01 +01:00
Alexander Eichhorn
6f9f8e57ac Feature(UI): bulk remove models loras (#8659)
* feat: Add bulk delete functionality for models, LoRAs, and embeddings

Implements a comprehensive bulk deletion feature for the model manager that allows users to select and delete multiple models, LoRAs, and embeddings at once.

Key changes:

Frontend:
- Add multi-selection state management to modelManagerV2 slice
- Update ModelListItem to support Ctrl/Cmd+Click multi-selection with checkboxes
- Create ModelListHeader component showing selection count and bulk actions
- Create BulkDeleteModelsModal for confirming bulk deletions
- Integrate bulk delete UI into ModelList with proper error handling
- Add API mutation for bulk delete operations

Backend:
- Add POST /api/v2/models/i/bulk_delete endpoint
- Implement BulkDeleteModelsRequest and BulkDeleteModelsResponse schemas
- Handle partial failures with detailed error reporting
- Return lists of successfully deleted and failed models

This feature significantly improves user experience when managing large model libraries, especially when restructuring model storage locations.

Fixes issue where users had to delete models individually after moving model files to new storage locations.

* fix: prevent model list header from scrolling with content

* fix: improve error handling in bulk model deletion

- Added proper error serialization using serialize-error for better error logging
- Explicitly defined BulkDeleteModelsResponse type instead of relying on generated schema reference

* refactor: improve code organization in ModelList components

- Reordered imports to follow conventional grouping (external, internal, then third-party utilities)
- Added type assertion for error serialization to satisfy TypeScript
- Extracted inline event handler into named callback function for better readability

* refactor: consolidate Button component props to single line

* feat(ui): enhance model manager bulk selection with select-all and actions menu

- Added select-all checkbox in navigation header with indeterminate state support
- Replaced single delete button with actions dropdown menu for future extensibility
- Made checkboxes always visible instead of conditionally showing on selection
- Moved model filtering logic to ModelListNavigation for select-all functionality
- Improved UX by showing selection state for filtered models only

* fix the wrong path seperater from my windows system

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-01 20:09:27 -05:00
Alexander Eichhorn
eaf4742799 Fix windows path again again 2025-12-01 22:28:39 +01:00
Alexander Eichhorn
f05ea28cbd feat: Add Z-Image LoRA support
Add comprehensive LoRA support for Z-Image models including:

Backend:
- New Z-Image LoRA config classes (LoRA_LyCORIS_ZImage_Config, LoRA_Diffusers_ZImage_Config)
- Z-Image LoRA conversion utilities with key mapping for transformer and Qwen3 encoder
- LoRA prefix constants (Z_IMAGE_LORA_TRANSFORMER_PREFIX, Z_IMAGE_LORA_QWEN3_PREFIX)
- LoRA detection logic to distinguish Z-Image from Flux models
- Layer patcher improvements for proper dtype conversion and parameter
2025-12-01 22:23:30 +01:00
Alexander Eichhorn
13ac16e2c0 fix windows path again. 2025-12-01 00:30:53 +01:00
Alexander Eichhorn
eb3f1c9a61 feat: Add Z-Image-Turbo model support
Add comprehensive support for Z-Image-Turbo (S3-DiT) models including:

Backend:
- New BaseModelType.ZImage in taxonomy
- Z-Image model config classes (ZImageTransformerConfig, Qwen3TextEncoderConfig)
- Model loader for Z-Image transformer and Qwen3 text encoder
- Z-Image conditioning data structures
- Step callback support for Z-Image with FLUX latent RGB factors

Invocations:
- z_image_model_loader: Load Z-Image transformer and Qwen3 encoder
- z_image_text_encoder: Encode prompts using Qwen3 with chat template
- z_image_denoise: Flow matching denoising with time-shifted sigmas
- z_image_image_to_latents: Encode images to 16-channel latents
- z_image_latents_to_image: Decode latents using FLUX VAE

Frontend:
- Z-Image graph builder for text-to-image generation
- Model picker and validation updates for z-image base type
- CFG scale now allows 0 (required for Z-Image-Turbo)
- Clip skip disabled for Z-Image (uses Qwen3, not CLIP)
- Optimal dimension settings for Z-Image (1024x1024)

Technical details:
- Uses Qwen3 text encoder (not CLIP/T5)
- 16 latent channels with FLUX-compatible VAE
- Flow matching scheduler with dynamic time shift
- 8 inference steps recommended for Turbo variant
- bfloat16 inference dtype
2025-12-01 00:22:32 +01:00
Kent Keirsey
c6a9847bbd feat(ui): Color Picker V2 (#8585)
* pinned colorpicker

* hex options

* remove unused consts

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2025-11-16 09:49:55 -05:00
Alexander Eichhorn
a2e109b3c2 feat(ui): improve hotkey customization UX with interactive controls and validation (#8649)
* feat: remove the ModelFooter in the ModelView and add the Delete Model Button from the Footer into the View

* forget to run pnpm fix

* chore(ui): reorder the model view buttons

* Initial plan

* Add customizable hotkeys infrastructure with UI

Co-authored-by: dunkeroni <3298737+dunkeroni@users.noreply.github.com>

* Fix ESLint issues in HotkeyEditor component

Co-authored-by: dunkeroni <3298737+dunkeroni@users.noreply.github.com>

* Fix knip unused export warning

Co-authored-by: dunkeroni <3298737+dunkeroni@users.noreply.github.com>

* Add tests for hotkeys slice

Co-authored-by: dunkeroni <3298737+dunkeroni@users.noreply.github.com>

* Fix tests to actually call reducer and add documentation

Co-authored-by: dunkeroni <3298737+dunkeroni@users.noreply.github.com>

* docs: add comprehensive hotkeys system documentation

- Created new HOTKEYS.md technical documentation for developers explaining architecture, data flow, and implementation details
- Added user-facing hotkeys.md guide with features overview and usage instructions
- Removed old CUSTOMIZABLE_HOTKEYS.md in favor of new split documentation
- Expanded documentation with detailed sections on:
  - State management and persistence
  - Component architecture and responsibilities
  - Developer integration

* Behavior changed to hotkey press instead of input + checking for allready used hotkeys

---------

Co-authored-by: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: dunkeroni <3298737+dunkeroni@users.noreply.github.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2025-11-16 14:35:37 +00:00
dunkeroni
5642099a40 Feat: SDXL Color Compensation (#8637)
* feat(nodes/UI): add SDXL color compensation option

* adjust value

* Better warnings on wrong VAE base model

* Restrict XL compensation to XL models

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>

* fix: BaseModelType missing import

* (chore): appease the ruff

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2025-11-16 14:32:12 +00:00
gogurtenjoyer
382d85ee23 Fix memory issues when installing models on Windows (#8652)
* Wrap GGUF loader for context managed close()

Wrap gguf.GGUFReader and then use a context manager to load memory-mapped GGUF files, so that they will automatically close properly when no longer needed. Should prevent the 'file in use in another process' errors on Windows.

* Additional check for cached state_dict

Additional check for cached state_dict as path is now optional - should solve model manager 'missing' this and the resultant memory errors.

* Appease ruff

* Further ruff appeasement

* ruff

* loaders.py fix for linux

No longer attempting to delete internal object.

* loaders.py - one more _mmap ref removed

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2025-11-16 09:25:52 -05:00
Jonathan
abcc987f6f Rework graph.py (#8642)
* Rework graph, add documentation

* Minor fixes to README.md

* Updated schema

* Fixed test to match behavior - all nodes executed, parents before children

* Update invokeai/app/services/shared/graph.py

Cleaned up code

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>

* Change silent corrections to enforcing invariants

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2025-11-16 09:10:47 -05:00
Lincoln Stein
36e400dd5d (chore) Update requirements to python 3.11-12 (#8657)
* (chore) update requirements to python 3.11-12

* update uv.lock
2025-11-08 21:29:43 -05:00
Weblate (bot)
0113931956 ui: translations update from weblate (#8599)
* translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2099 of 2132 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2130 of 2163 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2130 of 2163 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Japanese)

Currently translated at 99.6% (2155 of 2163 strings)

Co-authored-by: RyoKoba <kobayashi_ryo@cyberagent.co.jp>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ja/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI

* translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2103 of 2136 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI

---------

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Co-authored-by: RyoKoba <kobayashi_ryo@cyberagent.co.jp>
2025-11-04 02:29:05 +00:00
DustyShoe
8d6e00533e Fix to enable loading fp16 repo variant ControlNets (#8643)
* Fix ControlNet repo variant detection for fp16 weights

* Remove ControlNet diffusers fp16 regression test

* Update invokeai/backend/model_manager/configs/controlnet.py

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>

* style: ruff format controlnet.py

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2025-11-03 21:23:35 -05:00
Lincoln Stein
10eebb6c0c remove jazzhaiku as well 2025-11-02 15:22:13 -05:00
Lincoln Stein
68bcf2ebe0 chore(codeowners): remove commercial dev codeowners 2025-11-02 15:22:13 -05:00
blessedcoolant
ad0b09c738 chore(ui): reorder the model view buttons 2025-10-28 00:16:20 +05:30
Alexander Eichhorn
737cf795e8 forget to run pnpm fix 2025-10-28 00:16:20 +05:30
Alexander Eichhorn
6192ff5abb feat: remove the ModelFooter in the ModelView and add the Delete Model Button from the Footer into the View 2025-10-28 00:16:20 +05:30
blessedcoolant
066ba5fb19 fix(mm): directory path leakage on scan folder error (#8641)
## Summary

This fixes a bug in which private directory paths on the host could be
leaked to the user interface. The error occurs during the `scan_folders`
operation when a subdirectory is not accessible. The UI shows a
permission denied error message, followed by the path of the offending
directory. This patch limits the error message to the error type only
and does not give further details.

## Related Issues / Discussions

This bug was reported in a private DM on the Discord server.

## QA Instructions

Before applying this PR, go to ***Model Manager -> Add Model -> Scan
Folder*** and enter the path of a directory that has subdirectories that
the backend should not have access to, for example `/etc`. Press the
***Scan Folder*** button. You will see a Permission Denied error message
that gives away the path of the first inaccesislbe subdirectory.

After applying this PR, you will see just the Permission Denied error
without details.

## Merge Plan

Merge when approved.

## Checklist

- [X] _The PR has a short but descriptive title, suitable for a
changelog_
- [X] _Tests added / updated (if applicable)_
- [X] _Changes to a redux slice have a corresponding migration_
- [X] _Documentation added / updated (if applicable)_
- [ ] _Updated `What's New` copy (if doing a release after this PR)_
2025-10-28 00:02:59 +05:30
Lincoln Stein
2fb4c92310 fix(mm): directory path leakage on scan folder error 2025-10-27 08:54:57 -04:00
psychedelicious
3fdceba5fc chore: bump version to v6.9.0 2025-10-17 12:13:01 +11:00
psychedelicious
ae4bcc08f2 chore(ui): point ui lib dep at gh repo 2025-10-17 07:22:39 +11:00
psychedelicious
e1d88f93ca fix(ui): generator nodes
Closes #8617
2025-10-16 10:37:14 +11:00
psychedelicious
4ad2574835 feat(ui): add button to reidentify model to mm 2025-10-16 10:33:02 +11:00
psychedelicious
0e3d4beb48 chore(ui): typegen 2025-10-16 10:33:02 +11:00
psychedelicious
dcfd4ea756 feat(mm): reidentify models
Add route and model record service method to reidentify a model. This
re-probes the model files and replaces the model's config with the new
one if it does not error.
2025-10-16 10:33:02 +11:00
psychedelicious
093f8d6720 fix(mm): ignore files in hidden directories when identifying models 2025-10-16 10:33:02 +11:00
psychedelicious
22fdfab764 chore: bump version to v6.9.0rc3 2025-10-16 08:08:44 +11:00
psychedelicious
7a0b157fb8 feat(mm): more exports in invocation api 2025-10-16 08:08:44 +11:00
psychedelicious
563da9ee8e feat(mm): write warning README file to models dir 2025-10-16 08:08:44 +11:00
psychedelicious
c8d9cdc22e docs(mm): add readme for updating or adding new model support 2025-10-16 08:08:44 +11:00
psychedelicious
e9c2411da9 chore: bump version to v6.9.0rc2 2025-10-16 08:08:44 +11:00
psychedelicious
90989291ed fix(ui): wait for nav api to be ready before loading main app component 2025-10-16 08:08:44 +11:00
psychedelicious
d04fc343f0 feat(ui): add flag for connected status 2025-10-16 08:08:44 +11:00
psychedelicious
437594915a feat(mm): add model taxonomy and other classes to public API exports 2025-10-16 08:08:44 +11:00
psychedelicious
875aba8979 tidy(mm): remove unused class 2025-10-16 08:08:44 +11:00
psychedelicious
61d13f20ea chore: bump version to v6.9.0rc1 2025-10-16 08:08:44 +11:00
psychedelicious
3b0dd5768b chore(ui): update whatsnew 2025-10-16 08:08:44 +11:00
psychedelicious
d9888912e8 chore(ui): fix schema 2025-10-15 10:46:16 +11:00
psychedelicious
7cfaca804a chore: ruff 2025-10-15 10:46:16 +11:00
psychedelicious
f37f817fd1 fix(ui): move logging setup to react code 2025-10-15 10:46:16 +11:00
psychedelicious
85e5b40627 docs(ui): add canvas overview and design doc 2025-10-15 10:46:16 +11:00
psychedelicious
e93cda8b2b docs(ui): more documentation 2025-10-15 10:46:16 +11:00
psychedelicious
3d0f29f85f tidy: app "config", settings modal, infill methods
We had an "infill methods" route that long ago told the frontend infill
method, upscale method (model), NSFW checker, and watermark feature
availability.

None of these were used except for the patchmatch check. Removed them,
made the check exclusively for patchmatch, updated related code in redux
app startup listeners and settings modal.
2025-10-15 10:46:16 +11:00
psychedelicious
117fbd97af tidy(ui): clean up useSyncQueueStatus 2025-10-15 10:46:16 +11:00
psychedelicious
0bc11b9af9 docs(ui): add high-level readmes for various features 2025-10-15 10:46:16 +11:00
psychedelicious
79be70d918 tidy(ui): rename GalleryImageGrid and add comments 2025-10-15 10:46:16 +11:00
psychedelicious
16330a14cd tidy(ui): rename GalleryPanel file 2025-10-15 10:46:16 +11:00
psychedelicious
a6690072f1 docs(ui): add comments for image context menu 2025-10-15 10:46:16 +11:00
psychedelicious
26aebb0165 docs(ui): add comments for nes 2025-10-15 10:46:16 +11:00
psychedelicious
0742ebe11a tidy(ui): lift error boundary reset cb outside component 2025-10-15 10:46:16 +11:00
psychedelicious
c80c89dfdb tidy: removing unused code paths 7 2025-10-15 10:46:16 +11:00
psychedelicious
240dc673e4 tidy: removing unused code paths 6 2025-10-15 10:46:16 +11:00
psychedelicious
b2e93d7be7 tidy: removing unused code paths 5 2025-10-15 10:46:16 +11:00
psychedelicious
3126726abf tidy: removing unused code paths 4 2025-10-15 10:46:16 +11:00
psychedelicious
c09dc8e14c tidy: removing unused code paths 3 2025-10-15 10:46:16 +11:00
psychedelicious
906ec4519d tidy: removing unused code paths 2 2025-10-15 10:46:16 +11:00
psychedelicious
7cff5da2c0 tidy: removing unused code paths 1 2025-10-15 10:46:16 +11:00
psychedelicious
454d05bbde refactor: model manager v3 (#8607)
* feat(mm): add UnknownModelConfig

* refactor(ui): move model categorisation-ish logic to central location, simplify model manager models list

* refactor(ui)refactor(ui): more cleanup of model categories

* refactor(ui): remove unused excludeSubmodels

I can't remember what this was for and don't see any reference to it.
Maybe it's just remnants from a previous implementation?

* feat(nodes): add unknown as model base

* chore(ui): typegen

* feat(ui): add unknown model base support in ui

* feat(ui): allow changing model type in MM, fix up base and variant selects

* feat(mm): omit model description instead of making it "base type filename model"

* feat(app): add setting to allow unknown models

* feat(ui): allow changing model format in MM

* feat(app): add the installed model config to install complete events

* chore(ui): typegen

* feat(ui): toast warning when installed model is unidentified

* docs: update config docstrings

* chore(ui): typegen

* tests(mm): fix test for MM, leave the UnknownModelConfig class in the list of configs

* tidy(ui): prefer types from zod schemas for model attrs

* chore(ui): lint

* fix(ui): wrong translation string

* feat(mm): normalized model storage

Store models in a flat directory structure. Each model is in a dir named
its unique key (a UUID). Inside that dir is either the model file or the
model dir.

* feat(mm): add migration to flat model storage

* fix(mm): normalized multi-file/diffusers model installation no worky

now worky

* refactor: port MM probes to new api

- Add concept of match certainty to new probe
- Port CLIP Embed models to new API
- Fiddle with stuff

* feat(mm): port TIs to new API

* tidy(mm): remove unused probes

* feat(mm): port spandrel to new API

* fix(mm): parsing for spandrel

* fix(mm): loader for clip embed

* fix(mm): tis use existing weight_files method

* feat(mm): port vae to new API

* fix(mm): vae class inheritance and config_path

* tidy(mm): patcher types and import paths

* feat(mm): better errors when invalid model config found in db

* feat(mm): port t5 to new API

* feat(mm): make config_path optional

* refactor(mm): simplify model classification process

Previously, we had a multi-phase strategy to identify models from their
files on disk:
1. Run each model config classes' `matches()` method on the files. It
checks if the model could possibly be an identified as the candidate
model type. This was intended to be a quick check. Break on the first
match.
2. If we have a match, run the config class's `parse()` method. It
derive some additional model config attrs from the model files. This was
intended to encapsulate heavier operations that may require loading the
model into memory.
3. Derive the common model config attrs, like name, description,
calculate the hash, etc. Some of these are also heavier operations.

This strategy has some issues:
- It is not clear how the pieces fit together. There is some
back-and-forth between different methods and the config base class. It
is hard to trace the flow of logic until you fully wrap your head around
the system and therefore difficult to add a model architecture to the
probe.
- The assumption that we could do quick, lightweight checks before
heavier checks is incorrect. We often _must_ load the model state dict
in the `matches()` method. So there is no practical perf benefit to
splitting up the responsibility of `matches()` and `parse()`.
- Sometimes we need to do the same checks in `matches()` and `parse()`.
In these cases, splitting the logic is has a negative perf impact
because we are doing the same work twice.
- As we introduce the concept of an "unknown" model config (i.e. a model
that we cannot identify, but still record in the db; see #8582), we will
_always_ run _all_ the checks for every model. Therefore we need not try
to defer heavier checks or resource-intensive ops like hashing. We are
going to do them anyways.
- There are situations where a model may match multiple configs. One
known case are SD pipeline models with merged LoRAs. In the old probe
API, we relied on the implicit order of checks to know that if a model
matched for pipeline _and_ LoRA, we prefer the pipeline match. But, in
the new API, we do not have this implicit ordering of checks. To resolve
this in a resilient way, we need to get all matches up front, then use
tie-breaker logic to figure out which should win (or add "differential
diagnosis" logic to the matchers).
- Field overrides weren't handled well by this strategy. They were only
applied at the very end, if a model matched successfully. This means we
cannot tell the system "Hey, this model is type X with base Y. Trust me
bro.". We cannot override the match logic. As we move towards letting
users correct mis-identified models (see #8582), this is a requirement.

We can simplify the process significantly and better support "unknown"
models.

Firstly, model config classes now have a single `from_model_on_disk()`
method that attempts to construct an instance of the class from the
model files. This replaces the `matches()` and `parse()` methods.

If we fail to create the config instance, a special exception is raised
that indicates why we think the files cannot be identified as the given
model config class.

Next, the flow for model identification is a bit simpler:
- Derive all the common fields up-front (name, desc, hash, etc).
- Merge in overrides.
- Call `from_model_on_disk()` for every config class, passing in the
fields. Overrides are handled in this method.
- Record the results for each config class and choose the best one.

The identification logic is a bit more verbose, with the special
exceptions and handling of overrides, but it is very clear what is
happening.

The one downside I can think of for this strategy is we do need to check
every model type, instead of stopping at the first match. It's a bit
less efficient. In practice, however, this isn't a hot code path, and
the improved clarity is worth far more than perf optimizations that the
end user will likely never notice.

* refactor(mm): remove unused methods in config.py

* refactor(mm): add model config parsing utils

* fix(mm): abstractmethod bork

* tidy(mm): clarify that model id utils are private

* fix(mm): fall back to UnknownModelConfig correctly

* feat(mm): port CLIPVisionDiffusersConfig to new api

* feat(mm): port SigLIPDiffusersConfig to new api

* feat(mm): make match helpers more succint

* feat(mm): port flux redux to new api

* feat(mm): port ip adapter to new api

* tidy(mm): skip optimistic override handling for now

* refactor(mm): continue iterating on config

* feat(mm): port flux "control lora" and t2i adapter to new api

* tidy(ui): use Extract to get model config types

* fix(mm): t2i base determination

* feat(mm): port cnet to new api

* refactor(mm): add config validation utils, make it all consistent and clean

* feat(mm): wip port of main models to new api

* feat(mm): wip port of main models to new api

* feat(mm): wip port of main models to new api

* docs(mm): add todos

* tidy(mm): removed unused model merge class

* feat(mm): wip port main models to new api

* tidy(mm): clean up model heuristic utils

* tidy(mm): clean up ModelOnDisk caching

* tidy(mm): flux lora format util

* refactor(mm): make config classes narrow

Simpler logic to identify, less complexity to add new model, fewer
useless attrs that do not relate to the model arch, etc

* refactor(mm): diffusers loras

w

* feat(mm): consistent naming for all model config classes

* fix(mm): tag generation & scattered probe fixes

* tidy(mm): consistent class names

* refactor(mm): split configs into separate files

* docs(mm): add comments for identification utils

* chore(ui): typegen

* refactor(mm): remove legacy probe, new configs dir structure, update imports

* fix(mm): inverted condition

* docs(mm): update docsstrings in factory.py

* docs(mm): document flux variant attr

* feat(mm): add helper method for legacy configs

* feat(mm): satisfy type checker in flux denoise

* docs(mm): remove extraneous comment

* fix(mm): ensure unknown model configs get unknown attrs

* fix(mm): t5 identification

* fix(mm): sdxl ip adapter identification

* feat(mm): more flexible config matching utils

* fix(mm): clip vision identification

* feat(mm): add sanity checks before probing paths

* docs(mm): add reminder for self for field migrations

* feat(mm): clearer naming for main config class hierarchy

* feat(mm): fix clip vision starter model bases, add ref to actual models

* feat(mm): add model config schema migration logic

* fix(mm): duplicate import

* refactor(mm): split big migration into 3

Split the big migration that did all of these things into 3:

- Migration 22: Remove unique contraint on base/name/type in models
table
- Migration 23: Migrate configs to v6.8.0 schemas
- Migration 24: Normalize file storage

* fix(mm): pop base/type/format when creating unknown model config

* fix(db): migration 22 insert only real cols

* fix(db): migration 23 fall back to unknown model when config change fails

* feat(db): run migrations 23 and 24

* fix(mm): false negative on flux lora

* fix(mm): vae checkpoint probe checking for dir instead of file

* fix(mm): ModelOnDisk skips dirs when looking for weights

Previously a path w/ any of the known weights suffixes would be seen as
a weights file, even if it was a directory. We now check to ensure the
candidate path is actually a file before adding it to the list of
weights.

* feat(mm): add method to get main model defaults from a base

* feat(mm): do not log when multiple non-unknown model matches

* refactor(mm): continued iteration on model identifcation

* tests(mm): refactor model identification tests

Overhaul of model identification (probing) tests. Previously we didn't
test the correctness of probing except in a few narrow cases - now we
do.

See tests/model_identification/README.md for a detailed overview of the
new test setup. It includes instructions for adding a new test case. In
brief:

- Download the model you want to add as a test case
- Run a script against it to generate the test model files
- Fill in the expected model type/format/base/etc in the generated test
metadata JSON file

Included test cases:
- All starter models
- A handful of other models that I had installed
- Models present in the previous test cases as smoke tests, now also
tested for correctness

* fix(mm): omit type/format/base when creating unknown config instance

* feat(mm): use ValueError for model id sanity checks

* feat(mm): add flag for updating models to allow class changes

* tests(mm): fix remaining MM tests

* feat: allow users to edit models freely

* feat(ui): add warning for model settings edit

* tests(mm): flux state dict tests

* tidy: remove unused file

* fix(mm): lora state dict loading in model id

* feat(ui): use translation string for model edit warning

* docs(db): update version numbers in migration comments

* chore: bump version to v6.9.0a1

* docs: update model id readme

* tests(mm): attempt to fix windows model id tests

* fix(mm): issue with deleting single file models

* feat(mm): just delete the dir w/ rmtree when deleting model

* tests(mm): windows CI issue

* fix(ui): typegen schema sync

* fix(mm): fixes for migration 23

- Handle CLIP Embed and Main SD models missing variant field
- Handle errors when calling the discriminator function, previously only
handled ValidationError but it could be a ValueError or something else
- Better logging for config migration

* chore: bump version to v6.9.0a2

* chore: bump version to v6.9.0a3
2025-10-15 10:18:53 +11:00
psychedelicious
a7e1f06698 chore: uv lock 2025-10-12 08:18:03 -04:00
psychedelicious
8dfaf7e697 chore: bump version to v6.8.1 2025-10-12 08:18:03 -04:00
psychedelicious
f59ffbe145 fix: schema generation bug in fastapi 0.119.0
Couldn't figure out a quick and easy fix. Needs some pydantic/FastAPI
fanagling. For now, roll back to last good version.
2025-10-12 08:18:03 -04:00
dunkeroni
bd4bb075a5 bump node version to 2.0.0 2025-10-09 17:55:13 +11:00
dunkeroni
e19b7d4afb update typegen 2025-10-09 17:55:13 +11:00
dunkeroni
f8d0b43a9b change Colorspace title to "Color Space" 2025-10-09 17:55:13 +11:00
dunkeroni
50c77d9bf0 error message for incorrect mask size 2025-10-09 17:55:13 +11:00
dunkeroni
358cc0349e (chore) cleanup and schema 2025-10-09 17:55:13 +11:00
copilot-swe-agent[bot]
417e6ebdbc Simplify mask application by pasting base on corrected instead of inverting mask
Co-authored-by: dunkeroni <3298737+dunkeroni@users.noreply.github.com>
2025-10-09 17:55:13 +11:00
copilot-swe-agent[bot]
7919d659b7 Use PIL Image.paste() for mask application instead of numpy array blending
Co-authored-by: dunkeroni <3298737+dunkeroni@users.noreply.github.com>
2025-10-09 17:55:13 +11:00
dunkeroni
ec665d2c7f remove extra conversion 2025-10-09 17:55:13 +11:00
dunkeroni
020d36b234 remove extra conversion 2025-10-09 17:55:13 +11:00
copilot-swe-agent[bot]
d67272c027 Switch from LAB to YCbCr colorspace for simpler conversions
Co-authored-by: dunkeroni <3298737+dunkeroni@users.noreply.github.com>
2025-10-09 17:55:13 +11:00
copilot-swe-agent[bot]
82548f9e41 Fix mask loading and blending: load as L, white=original, black=result
Co-authored-by: dunkeroni <3298737+dunkeroni@users.noreply.github.com>
2025-10-09 17:55:13 +11:00
copilot-swe-agent[bot]
07a2369105 Add safety check for CDF normalization in histogram matching
Co-authored-by: dunkeroni <3298737+dunkeroni@users.noreply.github.com>
2025-10-09 17:55:13 +11:00
copilot-swe-agent[bot]
b1f7e2dfdc Refactor ColorCorrectInvocation with histogram matching
Co-authored-by: dunkeroni <3298737+dunkeroni@users.noreply.github.com>
2025-10-09 17:55:13 +11:00
psychedelicious
b2b4a35bc4 chore(ui): update whatsnew 2025-10-09 07:22:56 +11:00
psychedelicious
c249a25f85 chore: bump version to v6.8.0 2025-10-09 07:22:56 +11:00
psychedelicious
25f8ab24aa tests: fix test for breaking pydantic v2.12 change
Fixes a test failure introduced by
https://github.com/pydantic/pydantic/pull/11957

TL;DR: "after" model validators should be instance methods, not class
methods. Batch model updated to use an instance method, which fixes the
failing test.
2025-10-08 17:24:47 +11:00
psychedelicious
c0469ef633 chore: bump version to v6.8.0rc2 2025-10-08 17:24:47 +11:00
hffeka
310e826d76 docs: add BiRefNet and Image Export to communityNodes.md 2025-10-06 10:08:29 +11:00
psychedelicious
a423ead99e fix(ui): correct the in-place install verbiage, add tooltip 2025-09-30 13:08:17 +10:00
psychedelicious
3707c3b034 fix(ui): do not bake opacity when rasterizing layer adjustments 2025-09-22 11:43:08 +10:00
Mary Hipp
5885db4ab5 ruff 2025-09-19 11:07:36 -04:00
Mary Hipp
36ed9b750d restore list_queue_items method 2025-09-19 11:07:36 -04:00
psychedelicious
3cec06f86e chore(ui): typegen 2025-09-19 22:13:12 +10:00
psychedelicious
28b5f7a1c5 feat(nodes): better deprecation handling for ui_type
- Move migration of model-specific ui_types into BaseInvocation. This
gives us access to the node and field names, so the warnings are more
useful to the end user.
- Ensure we serialize the fields' json_schema_extra with enum values.
This wasn't a problem until now, when it interferes with migrating
ui_type cleanly. It's a transparent change.
- Improve warnings when validating fields (which includes the ui_type
migration logic)
2025-09-19 22:13:12 +10:00
psychedelicious
22cbb23ae0 fix(ui): ref images for flux kontext & api models not parsed correctly 2025-09-19 21:40:17 +10:00
Riccardo Giovanetti
4d585e3eec translationBot(ui): update translation (Italian)
Currently translated at 98.4% (2130 of 2163 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.4% (2127 of 2161 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2025-09-18 14:01:31 +10:00
psychedelicious
006b4356bb chore(ui): typegen 2025-09-18 12:39:27 +10:00
psychedelicious
da947866f2 fix(nodes): ensure SD2 models are pickable in loader/cnet nodes 2025-09-18 12:39:27 +10:00
psychedelicious
84a2cc6fc9 chore(ui): typegen 2025-09-18 12:39:27 +10:00
psychedelicious
b50534bb49 revert(nodes): do not deprecate ui_type for output fields! only deprecate the model ui types 2025-09-18 12:39:27 +10:00
psychedelicious
c305e79fee tests(ui): update tests to reflect new model parsing logic 2025-09-18 12:39:27 +10:00
psychedelicious
c32949d113 tidy(nodes): mark all UIType.*ModelField as deprecated 2025-09-18 12:39:27 +10:00
psychedelicious
87a98902da tidy(nodes): remove unused UIType.Video 2025-09-18 12:39:27 +10:00
psychedelicious
2857a446c9 docs(nodes): update docstrings for InputField 2025-09-18 12:39:27 +10:00
psychedelicious
035d9432bd feat(ui): support filtering on model format 2025-09-18 12:39:27 +10:00
psychedelicious
bdeb9fb1cf chore(ui): typegen 2025-09-18 12:39:27 +10:00
psychedelicious
dadff57061 feat(nodes): add ui_model_format filter for nodes 2025-09-18 12:39:27 +10:00
psychedelicious
480857ae4e fix(nodes): add base to SD1 model loader 2025-09-18 12:39:27 +10:00
psychedelicious
eaf0624004 feat(ui): remove explicit model type handling from workflow editor 2025-09-18 12:39:27 +10:00
psychedelicious
58bca1b9f4 feat(nodes): use new ui_model_[base|type|variant] on all core nodes 2025-09-18 12:39:27 +10:00
psychedelicious
54aa6908fa feat(ui): update invocation parsing to handle new ui_model_[base|type|variant] attrs 2025-09-18 12:39:27 +10:00
psychedelicious
e6d9daca96 chore(ui): typegen 2025-09-18 12:39:27 +10:00
psychedelicious
6e5a529cb7 feat(nodes): add ui_model_[base|type|variant] to InputField args for dynamic UI generation 2025-09-18 12:39:27 +10:00
Iq1pl
8c742a6e38 ruff format 2025-09-18 11:05:32 +10:00
Iq1pl
693373f1c1 Update ip_adapter.py
added support for NOOB-IPA-MARK1
2025-09-18 11:05:32 +10:00
Josh Corbett
4809080fd9 fix(ui): allow scrolling in ModelPane 2025-09-18 10:33:22 +10:00
psychedelicious
efcb1bea7f chore: bump version to v6.8.0rc1 2025-09-17 13:57:43 +10:00
psychedelicious
e0d7a401f3 feat(ui): make ref images croppable 2025-09-17 13:43:13 +10:00
psychedelicious
aac979e9a4 fix(ui): issue w/ setting initial aspect ratio in cropper 2025-09-17 13:43:13 +10:00
psychedelicious
3b0d7f076d tidy(ui): rename from "editor" to "cropper", minor cleanup 2025-09-17 13:43:13 +10:00
psychedelicious
e1acbcdbd5 fix(ui): store floats for box 2025-09-17 13:43:13 +10:00
psychedelicious
7d9b81550b feat(ui): revert to original image when crop discarded 2025-09-17 13:43:13 +10:00
psychedelicious
6a447dd1fe refactor(ui): remove "apply", "start" and "cancel" concepts from editor 2025-09-17 13:43:13 +10:00
psychedelicious
c2dc63ddbc fix(ui): video graphs 2025-09-17 13:43:13 +10:00
psychedelicious
1bc689d531 docs(ui): add comments to startingframeimage 2025-09-17 13:43:13 +10:00
psychedelicious
4829975827 feat(ui): make the editor components not care about the image 2025-09-17 13:43:13 +10:00
psychedelicious
49da4e00c3 feat(ui): add concept for editable image state 2025-09-17 13:43:13 +10:00
psychedelicious
89dfe5e729 docs(ui): add comments to editor 2025-09-17 13:43:13 +10:00
psychedelicious
6816d366df tidy(ui): editor misc 2025-09-17 13:43:13 +10:00
psychedelicious
9d3d2a36c9 tidy(ui): editor listeners 2025-09-17 13:43:13 +10:00
psychedelicious
ed231044c8 refactor(ui): simplify crop constraints 2025-09-17 13:43:13 +10:00
psychedelicious
b51a232794 feat(ui): extract config to own obj 2025-09-17 13:43:13 +10:00
psychedelicious
4412143a6e feat(ui): clean up editor 2025-09-17 13:43:13 +10:00
psychedelicious
de11cafdb3 refactor(ui): editor (wip) 2025-09-17 13:43:13 +10:00
psychedelicious
4d9114aa7d refactor(ui): editor (wip) 2025-09-17 13:43:13 +10:00
psychedelicious
67e2da1ebf refactor(ui): editor (wip) 2025-09-17 13:43:13 +10:00
psychedelicious
33ecc591c3 refactor(ui): editor init 2025-09-17 13:43:13 +10:00
psychedelicious
b57459a226 chore(ui): lint 2025-09-17 13:43:13 +10:00
psychedelicious
01282b1c90 feat(ui): do not clear crop when canceling 2025-09-17 13:43:13 +10:00
psychedelicious
3f302906dc feat(ui): crop doesn't hide outside cropped region 2025-09-17 13:43:13 +10:00
psychedelicious
81d56596fb tidy(ui): cleanup 2025-09-17 13:43:13 +10:00
psychedelicious
b536b0df0c feat(ui): misc iterate on editor 2025-09-17 13:43:13 +10:00
psychedelicious
692af1d93d feat(ui): type narrowing for editor output types 2025-09-17 13:43:13 +10:00
psychedelicious
bb7ef77b50 tidy(ui): lint/react conventions for editor component 2025-09-17 13:43:13 +10:00
psychedelicious
1862548573 feat(ui): image editor bg checkerboard pattern 2025-09-17 13:43:13 +10:00
psychedelicious
242c1b6350 feat(ui): tweak editor konva styles 2025-09-17 13:43:13 +10:00
psychedelicious
fc6e4bb04e tidy(ui): editor component cleanup 2025-09-17 13:43:13 +10:00
psychedelicious
20841abca6 tidy(ui): editor cleanup 2025-09-17 13:43:13 +10:00
psychedelicious
e8b69d99a4 chore(ui): lint 2025-09-17 13:43:13 +10:00
Mary Hipp
d6eaff8237 create editImageModal that takes an imageDTO, loads blob onto canvas, and allows cropping. cropped blob is uploaded as new asset 2025-09-17 13:43:13 +10:00
Mary Hipp
068b095956 show warning state with tooltip if starting frame image aspect ratio does not match the video output aspect ratio' 2025-09-17 13:43:13 +10:00
psychedelicious
f795a47340 tidy(ui): remove unused translation string 2025-09-16 15:04:03 +10:00
psychedelicious
df47345eb0 feat(ui): add translation strings for prompt history 2025-09-16 15:04:03 +10:00
psychedelicious
def04095a4 feat(ui): tweak prompt history styling 2025-09-16 15:04:03 +10:00
psychedelicious
28be8f0911 refactor(ui): simplify prompt history shortcuts 2025-09-16 15:04:03 +10:00
Kent Keirsey
b50c44bac0 handle potential for invalid list item 2025-09-16 15:04:03 +10:00
Kent Keirsey
b4ce0e02fc lint 2025-09-16 15:04:03 +10:00
Kent Keirsey
d6442d9a34 Prompt history shortcuts 2025-09-16 15:04:03 +10:00
Josh Corbett
4528bcafaf feat(model manager): add ModelFooter component and reusable ModelDeleteButton 2025-09-16 12:29:57 +10:00
Josh Corbett
8b82b81ee2 fix(ModelImage): change MODEL_IMAGE_THUMBNAIL_SIZE to a local constant 2025-09-16 12:29:57 +10:00
Josh Corbett
757acdd49e feat(model manager): 💄 update model manager ui, initial commit 2025-09-16 12:29:57 +10:00
psychedelicious
94b7cc583a fix(ui): do not reset params state on studio init nav to generate tab 2025-09-16 12:25:25 +10:00
psychedelicious
b663a6bac4 chore: bump version to v6.7.0 2025-09-15 14:37:56 +10:00
psychedelicious
65d40153fb chore(ui): update whatsnew 2025-09-15 14:37:56 +10:00
Riccardo Giovanetti
c8b741a514 translationBot(ui): update translation (Italian)
Currently translated at 98.4% (2120 of 2153 strings)

translationBot(ui): update translation (Italian)

Currently translated at 97.3% (2097 of 2153 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2025-09-15 14:25:41 +10:00
psychedelicious
6d3aeffed9 fix(ui): dedupe prompt history 2025-09-15 14:22:44 +10:00
psychedelicious
203be96910 fix(ui): render popovers in portals to ensure they are on top of other ui elements 2025-09-15 14:19:54 +10:00
psychedelicious
b0aa48ddb8 feat(ui): simple prompt history 2025-09-12 10:19:48 -04:00
psychedelicious
867dbe51e5 fix(ui): extend lora weight schema to accept full range of weights
This could cause a failure to rehydrate LoRA state, or failure to recall
a LoRA.

Closes #8551
2025-09-12 11:50:10 +10:00
psychedelicious
ff8948b6f1 chore(ui): update whatsnew 2025-09-11 18:09:31 +10:00
psychedelicious
fa3a6425a6 tests(ui): update staging area test to reflect new behaviour 2025-09-11 18:09:31 +10:00
psychedelicious
c5992ece89 fix(ui): better logic in staging area when canceling the selected item 2025-09-11 18:09:31 +10:00
psychedelicious
12a6239929 chore: bump version to v6.7.0rc1 2025-09-11 18:09:31 +10:00
Riccardo Giovanetti
e9238c59f4 translationBot(ui): update translation (Italian)
Currently translated at 96.5% (2053 of 2127 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2025-09-11 17:42:41 +10:00
Linos
c1cbbe51d6 translationBot(ui): update translation (Vietnamese)
Currently translated at 100.0% (2127 of 2127 strings)

Co-authored-by: Linos <linos.coding@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/vi/
Translation: InvokeAI/Web UI
2025-09-11 17:42:41 +10:00
Hosted Weblate
4219b4a288 translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI
2025-09-11 17:42:41 +10:00
psychedelicious
48c8a9c09d chore(ui): lint 2025-09-11 17:25:57 +10:00
psychedelicious
a67efdf4ad perf(ui): optimize curves graph component
Do not use whole layer as trigger for histo recalc; use the canvas cache
of the layer - it more reliably indicates when the layer pixel data has
changed, and fixes an issue where we can miss the first histo calc due
to race conditiong with async layer bbox calculation.
2025-09-11 17:25:57 +10:00
psychedelicious
d6ff9c2e49 tidy(ui): split curves graph into own component 2025-09-11 17:25:57 +10:00
psychedelicious
e768a3bc7b perf(ui): use narrow selectors in adjustments to reduce rerenders
dramatically improves the feel of the sliders
2025-09-11 17:25:57 +10:00
psychedelicious
7273700f61 fix(ui): sharpness range 2025-09-11 17:25:57 +10:00
psychedelicious
f909e81d91 feat(ui): better types & runtime guarantees for filter data stored in konva node attrs 2025-09-11 17:25:57 +10:00
psychedelicious
8c85f168f6 refactor(ui): make layer adjustments schemas/types composable 2025-09-11 17:25:57 +10:00
psychedelicious
263d86d46f fix(ui): points where x=255 sorted incorrectly 2025-09-11 17:25:57 +10:00
psychedelicious
0921805160 feat(ui): tweak adjustments panel styling 2025-09-11 17:25:57 +10:00
psychedelicious
517f4811e7 feat(ui): single action to reset adjustments 2025-09-11 17:25:57 +10:00
psychedelicious
0dc73c8803 tidy(ui): move some histogram drawing logic out of components and into calblacks 2025-09-11 17:25:57 +10:00
psychedelicious
26702b54c0 feat(ui): tweak layouts, use react conventions, disabled state 2025-09-11 17:25:57 +10:00
dunkeroni
2d65e4543f minor padding changes 2025-09-11 17:25:57 +10:00
dunkeroni
309113956b remove unknown type annotations 2025-09-11 17:25:57 +10:00
dunkeroni
0ac4099bc6 allow negative sharpness to soften 2025-09-11 17:25:57 +10:00
dunkeroni
899dc739fa defaultValue on adjusters 2025-09-11 17:25:57 +10:00
dunkeroni
4e2439fc8e remove extra edit comments 2025-09-11 17:25:57 +10:00
dunkeroni
00864c24e0 layout fixes 2025-09-11 17:25:57 +10:00
dunkeroni
b73aaa7d6f fix several points of curve editor jank 2025-09-11 17:25:57 +10:00
dunkeroni
85057ae704 splitup adjustment panel objects 2025-09-11 17:25:57 +10:00
dunkeroni
c3fb3a43a2 blue mode switch indicator 2025-09-11 17:25:57 +10:00
dunkeroni
51d0a15a1b use default factory on reset 2025-09-11 17:25:57 +10:00
dunkeroni
5991067fd9 simplify adjustments type to optional not null 2025-09-11 17:25:57 +10:00
dunkeroni
32c2d3f740 remove extra casts and types from filters.ts 2025-09-11 17:25:57 +10:00
dunkeroni
c661f86b34 fix: crop to bbox doubles adjustment filters 2025-09-11 17:25:57 +10:00
dunkeroni
cc72d8eab4 curves editor syntax and structure fixes 2025-09-11 17:25:57 +10:00
dunkeroni
e8550f9355 move constants in curves editor 2025-09-11 17:25:57 +10:00
dunkeroni
a1d0386ca4 move memoized slider to component 2025-09-11 17:25:57 +10:00
dunkeroni
495d089f85 clean up right click menu 2025-09-11 17:25:57 +10:00
dunkeroni
913b91e9dd remove redundant en.json colors 2025-09-11 17:25:57 +10:00
dunkeroni
3e907f4e14 remove extra title 2025-09-11 17:25:57 +10:00
dunkeroni
756df6ebe4 Finish button on adjustments 2025-09-11 17:25:57 +10:00
dunkeroni
2a6be99152 Fix tint not shifting green in negative direction 2025-09-11 17:25:57 +10:00
dunkeroni
3099e2bf9d fix disable toggle reverts to simple view 2025-09-11 17:25:57 +10:00
dunkeroni
6921f0412a log scale and panel width compatibility 2025-09-11 17:25:57 +10:00
dunkeroni
022d5a8863 curves editor 2025-09-11 17:25:57 +10:00
dunkeroni
af99beedc5 apply filters to operations 2025-09-11 17:25:57 +10:00
dunkeroni
f3d83dc6b7 visual adjustment filters 2025-09-11 17:25:57 +10:00
psychedelicious
ebc3f18a1a ai(ui): add CLAUDE.md to frontend 2025-09-11 13:26:39 +10:00
Mary Hipp
aeb512f8d9 ruff 2025-09-11 12:41:56 +10:00
Mary Hipp
a1810acb93 accidental commit 2025-09-11 12:41:56 +10:00
Mary Hipp
aa35a5083b remove completed_at from queue list so that created_at is only sort option, restore field values in UI 2025-09-11 12:41:56 +10:00
psychedelicious
4f17de0b32 fix(ui): ensure mask image is deleted when no more inputs to select object 2025-09-11 12:15:41 +10:00
psychedelicious
370c3cd59b feat(ui): update select object info tooltip 2025-09-11 12:15:41 +10:00
psychedelicious
67214e16c0 tidy(ui): organize select object components 2025-09-11 12:15:41 +10:00
psychedelicious
4880a1d946 feat(nodes): accept neg coords for bbox
This actually works fine for SAM.
2025-09-11 12:15:41 +10:00
psychedelicious
0f0988610f feat(ui): spruce up UI a bit 2025-09-11 12:15:41 +10:00
psychedelicious
6805d28b7a feat(ui): increase hit area for bbox anchors 2025-09-11 12:15:41 +10:00
psychedelicious
9b45a24136 fix(ui): respect selected point type 2025-09-11 12:15:41 +10:00
psychedelicious
4e9d66a64b tidy(ui): clean up CanvasSegmentAnythingModule 2025-09-11 12:15:41 +10:00
psychedelicious
8fec530b0f fix(ui): restore old tooltip for select object
need to add translation strigns for new functionality
2025-09-11 12:15:41 +10:00
psychedelicious
50c66f8671 fix(ui): select obj box moving on mmb pan 2025-09-11 12:15:41 +10:00
psychedelicious
f0aa39ea81 fix(ui): prevent bbox from following cursor after middle mouse pan
Added button checks to bbox rect and transformer mousedown/touchstart handlers to only process left clicks. Also added stage dragging check in onBboxDragMove to clear bbox drag state when middle mouse panning is active.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 12:15:41 +10:00
psychedelicious
faac814a3d fix(ui): prevent middle mouse from creating points in segmentation module
When middle mouse button is used for canvas panning, the pointerup event was still creating points in the segmentation module. Added button check to onBboxDragEnd handler to only process left clicks.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 12:15:41 +10:00
psychedelicious
fb9545bb90 fix(ui): bbox no shrinkies 2025-09-11 12:15:41 +10:00
psychedelicious
8ad2ee83b6 fix(ui): prevent bbox scale accumulation in SAM module
Fixed an issue where bounding boxes could grow exponentially when created at small sizes. The problem occurred because Konva Transformer modifies scaleX/scaleY rather than width/height directly, and the scale values weren't consistently reset after being applied to dimensions.

Changes:
- Ensure scale values are always reset to 1 after applying to dimensions
- Add minimum size constraints to prevent zero/negative dimensions
- Fix scale handling in transformend, dragend, and initial bbox creation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 12:15:41 +10:00
psychedelicious
f8ad62b5eb tidy(backend) cleanup sam pipelines 2025-09-11 12:15:41 +10:00
psychedelicious
03ae78bc7c tidy(nodes): clean up sam node 2025-09-11 12:15:41 +10:00
psychedelicious
ec1a058dbe fix(backend): issue w/ multiple bbox and sam1 2025-09-11 12:15:41 +10:00
psychedelicious
9e4d441e2e feat(ui): allow adding point inside bbox 2025-09-11 12:15:41 +10:00
psychedelicious
3770fd22f8 tidy(ui): ts issues 2025-09-11 12:15:41 +10:00
psychedelicious
a0232b0e63 feat(ui): combine points and bbox in visual mode for SAM
Revised the Select Object feature to support two input modes:
- Visual mode: Combined points and bounding box input for paired SAM inputs
- Prompt mode: Text-based object selection (unchanged)

Key changes:
- Replaced three input types (points, prompt, bbox) with two (visual, prompt)
- Visual mode supports both point and bbox inputs simultaneously
- Click to add include points, Shift+click for exclude points
- Click and drag to draw bounding box
- Fixed bbox visibility issues when adding points
- Fixed coordinate system issues for proper bbox positioning
- Added proper event handling and interaction controls

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 12:15:41 +10:00
psychedelicious
e1e964bf0e experiment(ui): support bboxes in select object 2025-09-11 12:15:41 +10:00
psychedelicious
1b1759cffc feat(ui): support prompt-based selection for object selection 2025-09-11 12:15:41 +10:00
psychedelicious
d828502bc8 refactor(backend): simplify segment anything APIs
There was a really confusing aspect of the SAM pipeline classes where
they accepted deeply nested lists of different dimensions (bbox, points,
and labels).

The lengths of the lists are related; each point must have a
corresponding label, and if bboxes are provided with points, they must
be same length.

I've refactored the backend API to take a single list of SAMInput
objects. This class has a bbox and/or a list of points, making it much
simpler to provide the right shape of inputs.

Internally, the pipeline classes take rejigger these input classes to
have the correct nesting.

The Nodes still have an awkward API where you can provide both bboxes
and points of different lengths, so I added a pydantic validator that
enforces correct lenghts.
2025-09-11 12:15:41 +10:00
psychedelicious
7a073b6de7 feat(ui): hold shift to add inverse point type 2025-09-11 12:15:41 +10:00
psychedelicious
338ff8d588 chore: typegen 2025-09-11 12:15:41 +10:00
psychedelicious
a3625efd3a chore: ruff 2025-09-11 12:15:41 +10:00
Kent Keirsey
5efb37fe63 consolidate into one node. 2025-09-11 12:15:41 +10:00
Kent Keirsey
aef0b81d5b fix models 2025-09-11 12:15:41 +10:00
Kent Keirsey
544edff507 update uv.lock 2025-09-11 12:15:41 +10:00
Kent Keirsey
42b1adab22 init Sam2 2025-09-11 12:15:41 +10:00
Attila Cseh
a2b9d12e88 prettier errors fixed 2025-09-10 11:28:50 +10:00
Attila Cseh
7a94fb6c04 maths enabled on numeric input fields in worklow editor 2025-09-10 11:28:50 +10:00
psychedelicious
efcd159704 fix(app): path traversal via bulk downloads paths 2025-09-10 11:18:12 +10:00
psychedelicious
997e619a9d feat(ui): address feedback 2025-09-09 14:42:30 +10:00
Attila Cseh
4bc184ff16 LoRA number input min/max restored 2025-09-09 14:42:30 +10:00
psychedelicious
0b605a745b fix(ui): route metadata to gemini node 2025-09-09 14:31:07 +10:00
Attila Cseh
22b038ce3b unused translations removed 2025-09-08 20:41:36 +10:00
psychedelicious
0bb5d647b5 tidy(app): method naming snake case 2025-09-08 20:41:36 +10:00
psychedelicious
4a3599929b fix(ui): do not pass scroll seek props to DOM in queue list 2025-09-08 20:41:36 +10:00
psychedelicious
f959ce8323 feat(ui): reduce overscan for queue
makes it a bit less sluggish
2025-09-08 20:41:36 +10:00
Attila Cseh
74e1047870 build errors fixed 2025-09-08 20:41:36 +10:00
Attila Cseh
732881c51b createdAt column fixed 2025-09-08 20:41:36 +10:00
Attila Cseh
107be8e166 queueSlice cleaned up 2025-09-08 20:41:36 +10:00
Attila Cseh
3c2f654da8 queue api listQueueItems removed 2025-09-08 20:41:36 +10:00
Attila Cseh
474fd44e50 status column not sortable 2025-09-08 20:41:36 +10:00
Attila Cseh
0dc5f8fd65 getQueueItemIds cache invalidation added 2025-09-08 20:41:36 +10:00
Attila Cseh
d4215fb460 isOpen refactored 2025-09-08 20:41:36 +10:00
Attila Cseh
0cd05ee9fd ListContext reverted with queryArgs 2025-09-08 20:41:36 +10:00
Attila Cseh
9fcb3af1d8 ListContext removed 2025-09-08 20:41:36 +10:00
Attila Cseh
c9da7e2172 typegen fixed 2025-09-08 20:41:36 +10:00
Attila Cseh
9788735d6b code review fixes 2025-09-08 20:41:36 +10:00
Attila Cseh
d6139748e2 Update invokeai/frontend/web/src/features/queue/components/QueueList/QueueList.tsx
Co-authored-by: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
2025-09-08 20:41:36 +10:00
Attila Cseh
602dfb1e5d Update invokeai/frontend/web/src/features/queue/components/QueueList/QueueList.tsx
Co-authored-by: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
2025-09-08 20:41:36 +10:00
Attila Cseh
5bb3a78f56 Update invokeai/frontend/web/src/features/queue/components/QueueList/QueueItemComponent.tsx
Co-authored-by: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
2025-09-08 20:41:36 +10:00
Attila Cseh
d58df1e17b schema re-generated 2025-09-08 20:41:36 +10:00
Attila Cseh
5d0e37eb2f lint errors fixed 2025-09-08 20:41:36 +10:00
Attila Cseh
486b333cef queue list virtualized 2025-09-08 20:41:36 +10:00
Attila Cseh
6fa437af03 get_queue_itemIds endpoint created 2025-09-08 20:41:36 +10:00
Attila Cseh
787ef6fa27 ColumnSortIcon refactored 2025-09-08 20:41:36 +10:00
Attila Cseh
7f0571c229 QueueListHeaderColumnProps.field turned into SortBy 2025-09-08 20:41:36 +10:00
Attila Cseh
f5a58c0ceb QueueListHeaderColumn created 2025-09-08 20:41:36 +10:00
psychedelicious
d16eef4e66 chore: bump version to v6.6.0 2025-09-08 14:01:02 +10:00
psychedelicious
681ff2b2b3 chore(ui): update whatsnew 2025-09-08 14:01:02 +10:00
psychedelicious
0d81b4ce98 tidy(ui): make names a bit clearer 2025-09-08 13:54:23 +10:00
psychedelicious
99f1667ced tidy(ui): remove unused dependency 2025-09-08 13:54:23 +10:00
psychedelicious
aa5597ab4d feat(ui): use resize observer directly in component 2025-09-08 13:54:23 +10:00
psychedelicious
9bbb8e8a5e feat(ui): simpler strategy to conditionally render slider brush width 2025-09-08 13:54:23 +10:00
psychedelicious
f284d282c1 feat(ui): color picker number input outline styling 2025-09-08 13:54:23 +10:00
Attila Cseh
4231488da6 number input height set 2025-09-08 13:54:23 +10:00
Attila Cseh
a014867e68 slider number input height set 2025-09-08 13:54:23 +10:00
Attila Cseh
22654fbc9c redundant translations removed 2025-09-08 13:54:23 +10:00
Attila Cseh
daa4fd751c ToolWidthPicker refactored 2025-09-08 13:54:23 +10:00
Attila Cseh
3fd265c333 slider for brush and eraser tool 2025-09-08 13:54:23 +10:00
psychedelicious
26a3a9130c Revert "build(ui): port clean translations script to js"
This reverts commit 8a00d855b4.
2025-09-08 11:20:55 +10:00
psychedelicious
3dfeaab4b2 Revert "build(ui): add package script to check and clean translatoins"
This reverts commit 9610f34dd4.
2025-09-08 11:20:55 +10:00
psychedelicious
a33707cc76 Revert "ci: add translation string check to frontend checks"
This reverts commit 98945a4560.
2025-09-08 11:20:55 +10:00
psychedelicious
21e13daf6e Revert "chore(ui): clean translations"
This reverts commit a0dceecab9.
2025-09-08 11:20:55 +10:00
psychedelicious
fa2614ee02 Revert "tidy(ui): remove python clean translations script"
This reverts commit 8a81c05caf.
2025-09-08 11:20:55 +10:00
Hosted Weblate
4be6ddb23d translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI
2025-09-05 12:28:33 +10:00
Riccardo Giovanetti
bba0e01926 translationBot(ui): update translation (Italian)
Currently translated at 98.6% (2093 of 2122 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2025-09-05 12:28:33 +10:00
psychedelicious
20d57d5ccf gh: update pr template 2025-09-05 11:27:02 +10:00
psychedelicious
d9121271a2 fix(ui): rehydration + redux migration design issue
Certain items in redux are ephemeral and omitted from persisted slices.
On rehydration, we need to inject these values back into the slice.

But there was an issue taht could prevent slice migrations from running
during rehydration.

The migrations look for the `_version` key in state and migrate the
slice accordingly.

The logic that merged in the ephemeral values accidentally _also_ merged
in the `_version` key if it didn't already exist. This happened _before_
migrations are run.

This causes problems for slices that didn't have a `_version` key and
then have one added via migration.

For example, the params slice didn't have a `_version` key until the
previous commit, which added `_version` and changed some other parts of
state in a migration.

On first load of the updated code, we have a catch-22 kinda situation:
- The persisted params slice is the old version. It needs to have both
`_version` and some other data added to it.
- We deserialize the state and then merge in ephemeral values. This
inadvertnetly also merged in the `_version` key.
- We run the slice migration. It sees there is a `_version` key and
thinks it doesn't need to run. The extra data isn't added to the slice.
The slice is parsed against its zod schema and fails because the new
data is missing.
- Because the parse failed, we treat the user's persisted data as
invalid and overwrite it with initial state, potentially causing data
loss.

The fix is to be more selective when merging in the ephemeral state
before migration - this is now done by checking which keys are on the
persist denylist and only adding those key.
2025-09-05 11:27:02 +10:00
psychedelicious
30b487c71c tidy(ui): remove unused x/y coords from params slice 2025-09-05 11:27:02 +10:00
psychedelicious
8a81c05caf tidy(ui): remove python clean translations script 2025-09-05 11:02:37 +10:00
psychedelicious
a0dceecab9 chore(ui): clean translations 2025-09-05 11:02:37 +10:00
psychedelicious
98945a4560 ci: add translation string check to frontend checks 2025-09-05 11:02:37 +10:00
psychedelicious
9610f34dd4 build(ui): add package script to check and clean translatoins 2025-09-05 11:02:37 +10:00
psychedelicious
8a00d855b4 build(ui): port clean translations script to js 2025-09-05 11:02:37 +10:00
psychedelicious
25430f04c5 chore: bump version to v6.6.0rc2 2025-09-04 16:43:41 +10:00
psychedelicious
b2b53c4481 fix(ui): set a react key on the current image viewer's components
This tells react that the component is a new instance each time we
change the image. Which, in turn, prevents a flash of the
previously-selected image during image switching and
progress-image-to-output-image-ing.
2025-09-04 16:35:40 +10:00
psychedelicious
c6696d7913 fix(ui): ensure origin is set correctly for generate tab batches
This prevents an issue in the image viewer's logic for simulating the
progress image "resolving" to a completed image
2025-09-04 16:35:40 +10:00
psychedelicious
8bcb6648f1 fix(ui): stop dragging when user clicks mmb once
This has been an issue for a long time. I suspect it wasn't noticed
until now because it's finicky to trigger - you have to click and
release very quickly, without moving the mouse at all.
2025-09-04 16:16:04 +10:00
psychedelicious
0ee360ba6c fix(ui): show fallback when no image is selected 2025-09-04 16:13:01 +10:00
psychedelicious
09bbe3eef9 fix(ui): clear gallery selection when switching boards and there are no items in the new board 2025-09-04 16:13:01 +10:00
psychedelicious
d14b7a48f5 fix(ui): clear gallery selection when last image on selected board is deleted 2025-09-04 16:13:01 +10:00
Mary Hipp
1db55b0ffa cleanup 2025-09-03 10:11:32 -04:00
Mary Hipp
3104a1baa6 remove crossOrigin for thumbnail loading 2025-09-03 10:11:32 -04:00
psychedelicious
0e523ca2c1 fix(ui): browser image caching cors race condition
Must set cross origin whenever we load an image from a URL to prevent
race conditions where browser caches an image with no CORS, then canvas
attempts to load it with CORS, resulting in browser rejecting the
request before it is made
2025-09-03 10:11:32 -04:00
psychedelicious
75daef2aba fix(ui): fix situation where progress images are super tiny
Missed a spot
2025-09-03 22:56:55 +10:00
psychedelicious
b036b18986 chore: bump version to v6.6.0rc1 2025-09-03 18:02:37 +10:00
Hosted Weblate
93535fa3c2 translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI
2025-09-03 17:57:27 +10:00
Riccardo Giovanetti
dcafb44f8a translationBot(ui): update translation (Italian)
Currently translated at 98.6% (2088 of 2117 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2025-09-03 17:57:27 +10:00
Mary Hipp
44b1d8d1fc remove video base models from image aspect/ratio logic 2025-09-03 10:22:14 +10:00
Attila Cseh
6f70a6bd10 prettier fix 2025-09-02 19:23:24 +10:00
Attila Cseh
0546aeed1d code review changes 2025-09-02 19:23:24 +10:00
Attila Cseh
8933f3f5dd LoRA weight default values turned into constant 2025-09-02 19:23:24 +10:00
Attila Cseh
29cdefe873 type conversion fixed 2025-09-02 19:23:24 +10:00
Attila Cseh
df299bb37f python source code reformatted 2025-09-02 19:23:24 +10:00
Attila Cseh
481fb42371 lint errors fixed 2025-09-02 19:23:24 +10:00
Attila Cseh
631a04b48c LoRA default weight 2025-09-02 19:23:24 +10:00
Attila Cseh
547e1941f4 code review changes 2025-09-02 19:16:26 +10:00
Attila Cseh
031d25ed63 switchable foreground/background colors 2025-09-02 19:16:26 +10:00
Riccardo Giovanetti
27f4af0eb4 translationBot(ui): update translation (Italian)
Currently translated at 98.6% (2087 of 2116 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2025-09-02 15:05:51 +10:00
psychedelicious
e0a0617093 chore(ui): bump dockview
This brings in a fix for Chrome that allowed you to drag tabs and split
the panels.

Closes #8449
2025-09-02 11:05:41 +10:00
psychedelicious
e6a763b887 fix(ui): move getItemsPerRow to frontend src dir
Not sure how but it was in repo root

Closes #8509
2025-09-02 11:02:56 +10:00
psychedelicious
3c9c49f7d9 feat(ui): add readiness checks for LoRAs
If incompatible LoRAs are added, prevent Invoking.

The logic to prevent adding incompatible LoRAs to graphs already
existed. This does not fix any generation bugs; just a visual
inconsistency where it looks like Invoke would use an incompatible LoRA.
2025-09-01 14:41:03 +10:00
Attila Cseh
26690d47b7 lint errors fixed 2025-09-01 14:34:35 +10:00
Attila Cseh
fcaff6ce09 remove LoRAs for recall use all 2025-09-01 14:34:35 +10:00
Damian
afd7296cb2 Add 'sd-2' to supported negative prompt base models
add back negative prompt support for sd2
2025-08-31 10:20:31 -04:00
psychedelicious
d6f42c76d5 fix(app): board count queries not getting categories as params 2025-08-29 11:07:52 +10:00
Mary Hipp
68f39fe907 cleanup 2025-08-28 16:38:48 -04:00
Mary Hipp
23a528545f match screen capture button to the others 2025-08-28 16:38:48 -04:00
Mary Hipp
c69d04a7f0 handle large videos 2025-08-28 15:29:47 -04:00
Mary Hipp
60f1e2d7ad do not show negative prompt for video 2025-08-28 12:59:23 -04:00
Mary Hipp
cb386bec28 do not show reference images on video tab 2025-08-28 12:59:23 -04:00
Mary Hipp
f29ceb3f12 add translations 2025-08-28 10:17:00 -04:00
Mary Hipp
4f51bc9421 add credit estimate for video generation 2025-08-28 10:17:00 -04:00
Mary Hipp
0c41abab79 add label for starting image field 2025-08-28 10:17:00 -04:00
Mary Hipp
cb457c3402 default resolution to 1080p 2025-08-28 10:17:00 -04:00
Mary Hipp
606ad73814 use first video model if none selected 2025-08-28 10:17:00 -04:00
psychedelicious
fe70bd538a fix(ui): hide unused queue actions menu item category 2025-08-28 10:17:00 -04:00
psychedelicious
b5c7316c0a chore(ui): lint 2025-08-28 10:17:00 -04:00
psychedelicious
460aec03ea fix(ui): more video translations 2025-08-28 10:17:00 -04:00
psychedelicious
6730d86a13 fix(ui): make ctx menu star label not refer to iamges 2025-08-28 10:17:00 -04:00
psychedelicious
c4bc03cb1f fix(ui): make ctx menu download tooltip not refer to iamges 2025-08-28 10:17:00 -04:00
psychedelicious
136ee28199 feat(ui): remove unimplemented context menu items for video 2025-08-28 10:17:00 -04:00
psychedelicious
2c6d266c0a fix(ui): metadata viewer translations 2025-08-28 10:17:00 -04:00
psychedelicious
f779920eaa chore(ui): lint 2025-08-28 10:17:00 -04:00
psychedelicious
01bef5d165 fix(ui): do not highlight starting frame image in red when it is not required 2025-08-28 10:17:00 -04:00
psychedelicious
72851d3e84 feat(ui): tweak video settings padding 2025-08-28 10:17:00 -04:00
psychedelicious
4ba85c62ca feat(ui): add border around starting frame image 2025-08-28 10:17:00 -04:00
psychedelicious
313aedb00a fix(ui): graph builder check for veo 2025-08-28 10:17:00 -04:00
psychedelicious
85bd324d74 tweak(ui): nav bar divider not so bright 2025-08-28 10:17:00 -04:00
psychedelicious
4a04411e74 fix(ui): tab hotkeys for video 2025-08-28 10:17:00 -04:00
psychedelicious
299a4db3bb chore(ui): lint 2025-08-28 10:17:00 -04:00
psychedelicious
390faa592c chore: ruff 2025-08-28 10:17:00 -04:00
Mary Hipp
2463aeb84a studio init action for video tab 2025-08-28 10:17:00 -04:00
Mary Hipp
ec8df163d1 launchpad cleanup 2025-08-28 10:17:00 -04:00
Mary Hipp
a198b7da78 fix view on large screens, restore auth for screen capture 2025-08-28 10:17:00 -04:00
Mary Hipp
fb11770852 rearrange image | video | asset for boards 2025-08-28 10:17:00 -04:00
Mary Hipp
6b6f3d56f7 add option for video upsell, rearrange navigation bar and gallery tabs 2025-08-28 10:17:00 -04:00
Mary Hipp
29d00eef9a hide video features if video is disabled 2025-08-28 10:17:00 -04:00
psychedelicious
6972cd708d feat(ui): delete confirmation for videos 2025-08-28 10:17:00 -04:00
psychedelicious
82893804ff feat(ui): metadata recall for videos 2025-08-28 10:17:00 -04:00
psychedelicious
47ffe365bc fix(ui): do not store whole model config in state 2025-08-28 10:17:00 -04:00
psychedelicious
f7b03b1e63 fix(ui): do not change canvas bbox on video model change 2025-08-28 10:17:00 -04:00
psychedelicious
356e38e82a feat(ui): use correct model config object in video graph builders 2025-08-28 10:17:00 -04:00
psychedelicious
5ea077bb8c feat(ui): add selector to get model config for current video model 2025-08-28 10:17:00 -04:00
psychedelicious
3c4b303555 feat(ui): simplify and consolidate video capture logic 2025-08-28 10:17:00 -04:00
psychedelicious
b8651cb1a2 fix(ui): rebase conflict 2025-08-28 10:17:00 -04:00
Mary Hipp
a6527c0ba1 lint again 2025-08-28 10:17:00 -04:00
Mary Hipp
6e40eca754 lint 2025-08-28 10:17:00 -04:00
Mary Hipp
53fab17c33 use context to track video ref so that toolbar can also save current frame 2025-08-28 10:17:00 -04:00
Mary Hipp
3876d88b3c add save frame functionality 2025-08-28 10:17:00 -04:00
Mary Hipp
82b4526691 add video_count and asset_count to boards UI 2025-08-28 10:17:00 -04:00
Mary Hipp
f56ba11394 add asset_count to BoardDTO and split it out from image count 2025-08-28 10:17:00 -04:00
Mary Hipp
32eb5190f2 add video_count to boardDTO 2025-08-28 10:17:00 -04:00
Mary Hipp
72e378789d video metadata support 2025-08-28 10:17:00 -04:00
Mary Hipp
f10ddb0cab split out video aspect/ratio into its own components 2025-08-28 10:17:00 -04:00
Mary Hipp
286127077d updates for new model type 2025-08-28 10:17:00 -04:00
Mary Hipp
36278bc044 add UI support for new model type Video 2025-08-28 10:17:00 -04:00
Mary Hipp
7a1c7ca43a add Video as new model type 2025-08-28 10:17:00 -04:00
psychedelicious
8303d567d5 docs(ui): add note about visual jank in gallery 2025-08-28 10:17:00 -04:00
psychedelicious
1fe19c1242 fix(ui): use correct placeholder for vidoes 2025-08-28 10:17:00 -04:00
psychedelicious
127a43865c fix(ui): locate in gallery, galleryview when selecting image/video 2025-08-28 10:17:00 -04:00
psychedelicious
24a48884cb chore(ui): lint 2025-08-28 10:17:00 -04:00
psychedelicious
47cee816fd chore(ui): dpdm 2025-08-28 10:17:00 -04:00
psychedelicious
90bacaddda feat(ui): video dnd 2025-08-28 10:17:00 -04:00
psychedelicious
c0cc9f421e fix(ui): generate tab graph builder 2025-08-28 10:17:00 -04:00
psychedelicious
dbb9032648 fix(ui): iterations works for video models 2025-08-28 10:17:00 -04:00
psychedelicious
b9e32e59a2 fix(ui): missing tranlsation 2025-08-28 10:17:00 -04:00
psychedelicious
545a1d8737 fix(ui): fetching imageDTO for video 2025-08-28 10:17:00 -04:00
psychedelicious
c4718403a2 tidy(ui): remove unused VideoAtPosition component 2025-08-28 10:17:00 -04:00
psychedelicious
eb308b1ff7 feat(ui): simpler layout for video player 2025-08-28 10:17:00 -04:00
Mary Hipp
a277bea804 fix video styling 2025-08-28 10:17:00 -04:00
Mary Hipp
30619c0420 add runway back as a model and allow runway and veo3 to live together in peace and harmony 2025-08-28 10:17:00 -04:00
Mary Hipp
504d8e32be add runway to backend 2025-08-28 10:17:00 -04:00
Mary Hipp
f21229cd14 update redux selection to have a list of images and/or videos, update image viewer to show either image or video depending on what is selected 2025-08-28 10:17:00 -04:00
Mary Hipp
640ec676c3 lint 2025-08-28 10:17:00 -04:00
Mary Hipp
6370412e9c tsc 2025-08-28 10:17:00 -04:00
Mary Hipp
edec2c2775 lint the dang thing 2025-08-28 10:17:00 -04:00
psychedelicious
bd38be31d8 gallery 2025-08-28 10:17:00 -04:00
psychedelicious
b938ae0a7e Revert "feat(ui): consolidated gallery (wip)"
This reverts commit 12b70bca67.
2025-08-28 10:17:00 -04:00
Mary Hipp
6e5b1ed55f add videos to change board modal 2025-08-28 10:17:00 -04:00
Mary Hipp
5970bd38c2 add resolution as a generation setting 2025-08-28 10:17:00 -04:00
Mary Hipp
e046417cf5 replace runway with veo, build out veo3 model support 2025-08-28 10:17:00 -04:00
Mary Hipp
27a2cd19bd add Veo3 model support to backend 2025-08-28 10:17:00 -04:00
psychedelicious
0df631b802 feat(ui): consolidated gallery (wip) 2025-08-28 10:17:00 -04:00
psychedelicious
5bb7cd168d feat(ui): gallery optimistic updates for video 2025-08-28 10:17:00 -04:00
psychedelicious
b4ba84ad35 fix(ui): panel names on video tab 2025-08-28 10:17:00 -04:00
Mary Hipp
d1628f51c9 stubbing out change board functionality 2025-08-28 10:17:00 -04:00
Mary Hipp
17c1304ce2 hook up starring, unstarring, and deleting single videos (no multiselect yet), adapt context menus to work for both images and videos and start on video context menu 2025-08-28 10:17:00 -04:00
Mary Hipp
cc9a85f7d0 add readiness logic to video tab 2025-08-28 10:17:00 -04:00
psychedelicious
7e2999649a feat(ui): more video stuff 2025-08-28 10:17:00 -04:00
psychedelicious
1473142f73 feat(ui): fiddle w/ video stuff 2025-08-28 10:17:00 -04:00
psychedelicious
49343546e7 feat(ui): fiddle w/ video stuff 2025-08-28 10:17:00 -04:00
psychedelicious
39d5879405 chore: ruff 2025-08-28 10:17:00 -04:00
psychedelicious
4b4ec29a09 feat(nodes): update VideoField & VideoOutput 2025-08-28 10:17:00 -04:00
psychedelicious
dc6811076f feat(ui): add dnd target for video start frame 2025-08-28 10:17:00 -04:00
Mary Hipp
0568784ee9 add duration and aspect ratio to video settings 2025-08-28 10:17:00 -04:00
Mary Hipp
895eac6bcd integrating video into gallery - thinking maybe a new category of image would make more senes 2025-08-28 10:17:00 -04:00
Mary Hipp
fe0efa9bdf add noop video router 2025-08-28 10:17:00 -04:00
Mary Hipp
acabc8bd54 add video models 2025-08-28 10:17:00 -04:00
Mary Hipp
89f999af08 combine nodes that generate and save videos 2025-08-28 10:17:00 -04:00
Mary Hipp
9ae76bef51 build out adhoc video saving graph 2025-08-28 10:17:00 -04:00
Mary Hipp
0999b43616 push up updates for VideoField 2025-08-28 10:17:00 -04:00
Mary Hipp
e6e4f58163 update VideoField 2025-08-28 10:17:00 -04:00
Mary Hipp
b371930e02 split out RunwayVideoOutput from VideoOutput 2025-08-28 10:17:00 -04:00
Mary Hipp
9b50e2303b rough rough POC of video tab 2025-08-28 10:17:00 -04:00
Mary Hipp
49d1810991 video_output support 2025-08-28 10:17:00 -04:00
psychedelicious
b1b009f7b8 chore: bump version to v6.5.1 2025-08-28 22:57:14 +10:00
psychedelicious
3431e6385c chore: uv lock 2025-08-28 22:57:14 +10:00
psychedelicious
5db1027d32 Pin sentencepiece version in pyproject.toml
Pin sentencepiece version to 0.2.0 to avoid coredump issues.
2025-08-28 22:57:14 +10:00
Hosted Weblate
579f182fe9 translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI
2025-08-28 22:51:40 +10:00
Riccardo Giovanetti
55bf41f63f translationBot(ui): update translation (Italian)
Currently translated at 98.6% (2053 of 2082 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2025-08-28 22:51:40 +10:00
psychedelicious
fc32fd2d2e fix(ui): progress image renders at physical size 2025-08-28 22:47:52 +10:00
psychedelicious
a2b6536078 fix(ui): konva caching opt-out doesn't do what i thought it would 2025-08-28 22:45:03 +10:00
Mary Hipp Rogers
144c54a6c8 Revert "video_output support"
This reverts commit 453ef1a220.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
ca40daeb97 Revert "rough rough POC of video tab"
This reverts commit e89266bfe3.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
e600cdc826 Revert "split out RunwayVideoOutput from VideoOutput"
This reverts commit 97719b0aab.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
b7c52f33dc Revert "update VideoField"
This reverts commit bd251f8cce.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
e78157fcf0 Revert "push up updates for VideoField"
This reverts commit 94ba840948.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
7d7b98249f Revert "build out adhoc video saving graph"
This reverts commit 07565d4015.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
f5bf84f304 Revert "combine nodes that generate and save videos"
This reverts commit eff9c7b92f.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
c30d5bece2 Revert "add video models"
This reverts commit 295b5a20a8.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
27845b2f1b Revert "add noop video router"
This reverts commit e9c4e12454.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
bad6eea077 Revert "integrating video into gallery - thinking maybe a new category of image would make more senes"
This reverts commit 5c93e53195.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
9c26ac5ce3 Revert "add duration and aspect ratio to video settings"
This reverts commit 4d8bcad15b.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
b7306bb5c9 Revert "feat(ui): add dnd target for video start frame"
This reverts commit 530d20c1be.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
0c115177b2 Revert "feat(nodes): update VideoField & VideoOutput"
This reverts commit 67de3f2d9b.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
5aae41b5bb Revert "chore: ruff"
This reverts commit 9380d8901c.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
7ad09a2f79 Revert "feat(ui): fiddle w/ video stuff"
This reverts commit f98bbc32dd.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
5a6d3639b7 Revert "feat(ui): fiddle w/ video stuff"
This reverts commit 79e8482b27.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
84617d3df2 Revert "feat(ui): more video stuff"
This reverts commit 963c2ec60c.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
e05f30749e Revert "add readiness logic to video tab"
This reverts commit 288ac0a293.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
88a2e27338 Revert "hook up starring, unstarring, and deleting single videos (no multiselect yet), adapt context menus to work for both images and videos and start on video context menu"
This reverts commit a918198d4f.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
15a6fd76c8 Revert "stubbing out change board functionality"
This reverts commit 67042e6dec.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
6adb46a86c Revert "fix(ui): panel names on video tab"
This reverts commit 64dfa125d2.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
e8a74eb79d Revert "feat(ui): gallery optimistic updates for video"
This reverts commit 0ec6d33086.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
dcd716c384 Revert "feat(ui): consolidated gallery (wip)"
This reverts commit 6ef1c2a5e1.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
56697635dd Revert "add Veo3 model support to backend"
This reverts commit 49d569ec59.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
5b5657e292 Revert "replace runway with veo, build out veo3 model support"
This reverts commit d95a698ebd.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
ad3dfbe1ed Revert "add resolution as a generation setting"
This reverts commit b71829a827.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
59ddc4f7b0 Revert "add videos to change board modal"
This reverts commit 45b4432833.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
4653b79f12 Revert "Revert "feat(ui): consolidated gallery (wip)""
This reverts commit 637d19c22b.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
778d6f167f Revert "gallery"
This reverts commit aa4e3adadb.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
05c71f50f1 Revert "lint the dang thing"
This reverts commit 1b0d599dc2.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
406e0be39c Revert "tsc"
This reverts commit 7828102b67.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
0d71234a12 Revert "lint"
This reverts commit b377b80446.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
e38019bb70 Revert "update redux selection to have a list of images and/or videos, update image viewer to show either image or video depending on what is selected"
This reverts commit 8df3067599.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
a879880b42 Revert "add runway to backend"
This reverts commit f631b5178f.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
71c8accbfe Revert "add runway back as a model and allow runway and veo3 to live together in peace and harmony"
This reverts commit b2026d9c00.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
154fb99daf Revert "fix video styling"
This reverts commit 3d9889e272.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
0df476ce13 Revert "feat(ui): simpler layout for video player"
This reverts commit 3a1cedbced.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
e7ad830fa9 Revert "tidy(ui): remove unused VideoAtPosition component"
This reverts commit e55d39a20b.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
e81e0a8286 Revert "fix(ui): fetching imageDTO for video"
This reverts commit fbf8aa17c8.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
d0f7e72cbb Revert "fix(ui): missing tranlsation"
This reverts commit 89efe9c2b1.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
fdead4fb8c Revert "fix(ui): iterations works for video models"
This reverts commit 24f22d539f.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
31c9945b32 Revert "fix(ui): generate tab graph builder"
This reverts commit 84dc4e4ea9.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
22de8a4b12 Revert "feat(ui): video dnd"
This reverts commit f5fdba795a.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
89cb3c3230 Revert "chore(ui): dpdm"
This reverts commit 6a7fe6668b.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
7bb99ece4e Revert "chore(ui): lint"
This reverts commit 55139bb169.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
28f040123f Revert "fix(ui): locate in gallery, galleryview when selecting image/video"
This reverts commit 26fe937d97.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
1be3a4db64 Revert "fix(ui): use correct placeholder for vidoes"
This reverts commit 7e031e9c01.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
cb44c995d2 Revert "docs(ui): add note about visual jank in gallery"
This reverts commit 2d9c82da85.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
9b9b35c315 Revert "add Video as new model type"
This reverts commit fb0a924918.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
f6edab6032 Revert "add UI support for new model type Video"
This reverts commit c6f2d127ef.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
f79665b023 Revert "updates for new model type"
This reverts commit 23cde86bc4.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
6b1bc7a87d Revert "split out video aspect/ratio into its own components"
This reverts commit 6c375b228e.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
c6f2994c84 Revert "video metadata support"
This reverts commit b16d1a943d.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
0cff67ff23 Revert "add video_count to boardDTO"
This reverts commit 1cc6893d0d.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
e957c11c9a Revert "add asset_count to BoardDTO and split it out from image count"
This reverts commit d4378d9f2a.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
4baa685c7a Revert "add video_count and asset_count to boards UI"
This reverts commit e36490c2ec.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
1bd5907a12 Revert "add save frame functionality"
This reverts commit 6a20271dba.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
2fd56e6029 Revert "use context to track video ref so that toolbar can also save current frame"
This reverts commit 1bf25fadb3.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
b0548edc8c Revert "lint"
This reverts commit 378f33bc92.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
41d781176f Revert "lint again"
This reverts commit 41e1697e79.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
8709de0b33 Revert "fix(ui): rebase conflict"
This reverts commit bc6dd12083.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
af43fe2fd4 Revert "feat(ui): simplify and consolidate video capture logic"
This reverts commit c5a76806c1.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
ebbb11c3b1 Revert "feat(ui): add selector to get model config for current video model"
This reverts commit 5cabc37a87.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
0fc8c08da3 Revert "feat(ui): use correct model config object in video graph builders"
This reverts commit 9fcba3b876.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
bfadcffe3c Revert "fix(ui): do not change canvas bbox on video model change"
This reverts commit 8eb3f40e1b.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
49c2332c13 Revert "fix(ui): do not store whole model config in state"
This reverts commit b2ed3c99d4.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
dacef158c4 Revert "feat(ui): metadata recall for videos"
This reverts commit 4c32b2a123.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
0c34d8201e Revert "feat(ui): delete confirmation for videos"
This reverts commit 505c75a5ab.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
77132075ff Revert "hide video features if video is disabled"
This reverts commit 0de5097207.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
f008d3b0b2 Revert "add option for video upsell, rearrange navigation bar and gallery tabs"
This reverts commit 4845d31857.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
4e66ccefe8 Revert "rearrange image | video | asset for boards"
This reverts commit 8a60def51f.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
5d0ed45326 Revert "fix view on large screens, restore auth for screen capture"
This reverts commit 1f526a1c27.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
379d633ac6 Revert "launchpad cleanup"
This reverts commit ab41f71a36.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
93bba1b692 Revert "studio init action for video tab"
This reverts commit 431fd83a43.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
667e175ab7 Revert "chore: ruff"
This reverts commit 3ae99df091.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
de146aa4aa Revert "chore(ui): lint"
This reverts commit 36c16d2781.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
ed9c2c8208 Revert "fix(ui): tab hotkeys for video"
This reverts commit 20813b5615.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
9d984878f3 Revert "tweak(ui): nav bar divider not so bright"
This reverts commit 269d4fe670.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
585eb8c69d Revert "fix(ui): graph builder check for veo"
This reverts commit 239fb86a46.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
c105bae127 Revert "feat(ui): add border around starting frame image"
This reverts commit 8642e8881d.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
c39f26266f Revert "feat(ui): tweak video settings padding"
This reverts commit 842d729ec8.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
47dffd123a Revert "fix(ui): do not highlight starting frame image in red when it is not required"
This reverts commit 0b05b24e9a.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
b946ec3172 Revert "chore(ui): lint"
This reverts commit 8c2e6a3988.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
024c02329d Revert "fix(ui): metadata viewer translations"
This reverts commit 2a6cfde488.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
4b43b59472 Revert "feat(ui): remove unimplemented context menu items for video"
This reverts commit a6b0581939.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
d11f115e1a Revert "fix(ui): make ctx menu download tooltip not refer to iamges"
This reverts commit e4f24c4dc4.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
92253ce854 Revert "fix(ui): make ctx menu star label not refer to iamges"
This reverts commit ec793cb636.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
0ebbfa90c9 Revert "fix(ui): more video translations"
This reverts commit 0d827d8306.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
fdfee11e37 Revert "chore(ui): lint"
This reverts commit 3971382a6d.
2025-08-28 08:32:47 -04:00
Mary Hipp Rogers
6091bf4f60 Revert "fix(ui): hide unused queue actions menu item category"
This reverts commit 07271ca468.
2025-08-28 08:32:47 -04:00
psychedelicious
07271ca468 fix(ui): hide unused queue actions menu item category 2025-08-28 08:23:58 -04:00
psychedelicious
3971382a6d chore(ui): lint 2025-08-28 08:23:58 -04:00
psychedelicious
0d827d8306 fix(ui): more video translations 2025-08-28 08:23:58 -04:00
psychedelicious
ec793cb636 fix(ui): make ctx menu star label not refer to iamges 2025-08-28 08:23:58 -04:00
psychedelicious
e4f24c4dc4 fix(ui): make ctx menu download tooltip not refer to iamges 2025-08-28 08:23:58 -04:00
psychedelicious
a6b0581939 feat(ui): remove unimplemented context menu items for video 2025-08-28 08:23:58 -04:00
psychedelicious
2a6cfde488 fix(ui): metadata viewer translations 2025-08-28 08:23:58 -04:00
psychedelicious
8c2e6a3988 chore(ui): lint 2025-08-28 08:23:58 -04:00
psychedelicious
0b05b24e9a fix(ui): do not highlight starting frame image in red when it is not required 2025-08-28 08:23:58 -04:00
psychedelicious
842d729ec8 feat(ui): tweak video settings padding 2025-08-28 08:23:58 -04:00
psychedelicious
8642e8881d feat(ui): add border around starting frame image 2025-08-28 08:23:58 -04:00
psychedelicious
239fb86a46 fix(ui): graph builder check for veo 2025-08-28 08:23:58 -04:00
psychedelicious
269d4fe670 tweak(ui): nav bar divider not so bright 2025-08-28 08:23:58 -04:00
psychedelicious
20813b5615 fix(ui): tab hotkeys for video 2025-08-28 08:23:58 -04:00
psychedelicious
36c16d2781 chore(ui): lint 2025-08-28 08:23:58 -04:00
psychedelicious
3ae99df091 chore: ruff 2025-08-28 08:23:58 -04:00
Mary Hipp
431fd83a43 studio init action for video tab 2025-08-28 08:23:58 -04:00
Mary Hipp
ab41f71a36 launchpad cleanup 2025-08-28 08:23:58 -04:00
Mary Hipp
1f526a1c27 fix view on large screens, restore auth for screen capture 2025-08-28 08:23:58 -04:00
Mary Hipp
8a60def51f rearrange image | video | asset for boards 2025-08-28 08:23:58 -04:00
Mary Hipp
4845d31857 add option for video upsell, rearrange navigation bar and gallery tabs 2025-08-28 08:23:58 -04:00
Mary Hipp
0de5097207 hide video features if video is disabled 2025-08-28 08:23:58 -04:00
psychedelicious
505c75a5ab feat(ui): delete confirmation for videos 2025-08-28 08:23:58 -04:00
psychedelicious
4c32b2a123 feat(ui): metadata recall for videos 2025-08-28 08:23:58 -04:00
psychedelicious
b2ed3c99d4 fix(ui): do not store whole model config in state 2025-08-28 08:23:58 -04:00
psychedelicious
8eb3f40e1b fix(ui): do not change canvas bbox on video model change 2025-08-28 08:23:58 -04:00
psychedelicious
9fcba3b876 feat(ui): use correct model config object in video graph builders 2025-08-28 08:23:58 -04:00
psychedelicious
5cabc37a87 feat(ui): add selector to get model config for current video model 2025-08-28 08:23:58 -04:00
psychedelicious
c5a76806c1 feat(ui): simplify and consolidate video capture logic 2025-08-28 08:23:58 -04:00
psychedelicious
bc6dd12083 fix(ui): rebase conflict 2025-08-28 08:23:58 -04:00
Mary Hipp
41e1697e79 lint again 2025-08-28 08:23:58 -04:00
Mary Hipp
378f33bc92 lint 2025-08-28 08:23:58 -04:00
Mary Hipp
1bf25fadb3 use context to track video ref so that toolbar can also save current frame 2025-08-28 08:23:58 -04:00
Mary Hipp
6a20271dba add save frame functionality 2025-08-28 08:23:58 -04:00
Mary Hipp
e36490c2ec add video_count and asset_count to boards UI 2025-08-28 08:23:58 -04:00
Mary Hipp
d4378d9f2a add asset_count to BoardDTO and split it out from image count 2025-08-28 08:23:58 -04:00
Mary Hipp
1cc6893d0d add video_count to boardDTO 2025-08-28 08:23:58 -04:00
Mary Hipp
b16d1a943d video metadata support 2025-08-28 08:23:58 -04:00
Mary Hipp
6c375b228e split out video aspect/ratio into its own components 2025-08-28 08:23:58 -04:00
Mary Hipp
23cde86bc4 updates for new model type 2025-08-28 08:23:58 -04:00
Mary Hipp
c6f2d127ef add UI support for new model type Video 2025-08-28 08:23:58 -04:00
Mary Hipp
fb0a924918 add Video as new model type 2025-08-28 08:23:58 -04:00
psychedelicious
2d9c82da85 docs(ui): add note about visual jank in gallery 2025-08-28 08:23:58 -04:00
psychedelicious
7e031e9c01 fix(ui): use correct placeholder for vidoes 2025-08-28 08:23:58 -04:00
psychedelicious
26fe937d97 fix(ui): locate in gallery, galleryview when selecting image/video 2025-08-28 08:23:58 -04:00
psychedelicious
55139bb169 chore(ui): lint 2025-08-28 08:23:58 -04:00
psychedelicious
6a7fe6668b chore(ui): dpdm 2025-08-28 08:23:58 -04:00
psychedelicious
f5fdba795a feat(ui): video dnd 2025-08-28 08:23:58 -04:00
psychedelicious
84dc4e4ea9 fix(ui): generate tab graph builder 2025-08-28 08:23:58 -04:00
psychedelicious
24f22d539f fix(ui): iterations works for video models 2025-08-28 08:23:58 -04:00
psychedelicious
89efe9c2b1 fix(ui): missing tranlsation 2025-08-28 08:23:58 -04:00
psychedelicious
fbf8aa17c8 fix(ui): fetching imageDTO for video 2025-08-28 08:23:58 -04:00
psychedelicious
e55d39a20b tidy(ui): remove unused VideoAtPosition component 2025-08-28 08:23:58 -04:00
psychedelicious
3a1cedbced feat(ui): simpler layout for video player 2025-08-28 08:23:58 -04:00
Mary Hipp
3d9889e272 fix video styling 2025-08-28 08:23:58 -04:00
Mary Hipp
b2026d9c00 add runway back as a model and allow runway and veo3 to live together in peace and harmony 2025-08-28 08:23:58 -04:00
Mary Hipp
f631b5178f add runway to backend 2025-08-28 08:23:58 -04:00
Mary Hipp
8df3067599 update redux selection to have a list of images and/or videos, update image viewer to show either image or video depending on what is selected 2025-08-28 08:23:58 -04:00
Mary Hipp
b377b80446 lint 2025-08-28 08:23:58 -04:00
Mary Hipp
7828102b67 tsc 2025-08-28 08:23:58 -04:00
Mary Hipp
1b0d599dc2 lint the dang thing 2025-08-28 08:23:58 -04:00
psychedelicious
aa4e3adadb gallery 2025-08-28 08:23:58 -04:00
psychedelicious
637d19c22b Revert "feat(ui): consolidated gallery (wip)"
This reverts commit 12b70bca67.
2025-08-28 08:23:58 -04:00
Mary Hipp
45b4432833 add videos to change board modal 2025-08-28 08:23:58 -04:00
Mary Hipp
b71829a827 add resolution as a generation setting 2025-08-28 08:23:58 -04:00
Mary Hipp
d95a698ebd replace runway with veo, build out veo3 model support 2025-08-28 08:23:58 -04:00
Mary Hipp
49d569ec59 add Veo3 model support to backend 2025-08-28 08:23:58 -04:00
psychedelicious
6ef1c2a5e1 feat(ui): consolidated gallery (wip) 2025-08-28 08:23:58 -04:00
psychedelicious
0ec6d33086 feat(ui): gallery optimistic updates for video 2025-08-28 08:23:58 -04:00
psychedelicious
64dfa125d2 fix(ui): panel names on video tab 2025-08-28 08:23:58 -04:00
Mary Hipp
67042e6dec stubbing out change board functionality 2025-08-28 08:23:58 -04:00
Mary Hipp
a918198d4f hook up starring, unstarring, and deleting single videos (no multiselect yet), adapt context menus to work for both images and videos and start on video context menu 2025-08-28 08:23:58 -04:00
Mary Hipp
288ac0a293 add readiness logic to video tab 2025-08-28 08:23:58 -04:00
psychedelicious
963c2ec60c feat(ui): more video stuff 2025-08-28 08:23:58 -04:00
psychedelicious
79e8482b27 feat(ui): fiddle w/ video stuff 2025-08-28 08:23:58 -04:00
psychedelicious
f98bbc32dd feat(ui): fiddle w/ video stuff 2025-08-28 08:23:58 -04:00
psychedelicious
9380d8901c chore: ruff 2025-08-28 08:23:58 -04:00
psychedelicious
67de3f2d9b feat(nodes): update VideoField & VideoOutput 2025-08-28 08:23:58 -04:00
psychedelicious
530d20c1be feat(ui): add dnd target for video start frame 2025-08-28 08:23:58 -04:00
Mary Hipp
4d8bcad15b add duration and aspect ratio to video settings 2025-08-28 08:23:58 -04:00
Mary Hipp
5c93e53195 integrating video into gallery - thinking maybe a new category of image would make more senes 2025-08-28 08:23:58 -04:00
Mary Hipp
e9c4e12454 add noop video router 2025-08-28 08:23:58 -04:00
Mary Hipp
295b5a20a8 add video models 2025-08-28 08:23:58 -04:00
Mary Hipp
eff9c7b92f combine nodes that generate and save videos 2025-08-28 08:23:58 -04:00
Mary Hipp
07565d4015 build out adhoc video saving graph 2025-08-28 08:23:58 -04:00
Mary Hipp
94ba840948 push up updates for VideoField 2025-08-28 08:23:58 -04:00
Mary Hipp
bd251f8cce update VideoField 2025-08-28 08:23:58 -04:00
Mary Hipp
97719b0aab split out RunwayVideoOutput from VideoOutput 2025-08-28 08:23:58 -04:00
Mary Hipp
e89266bfe3 rough rough POC of video tab 2025-08-28 08:23:58 -04:00
Mary Hipp
453ef1a220 video_output support 2025-08-28 08:23:58 -04:00
psychedelicious
faf8f0f291 chore: bump version to v6.5.0 2025-08-28 13:32:37 +10:00
psychedelicious
5d36499982 chore: update whatsnew 2025-08-28 13:32:37 +10:00
Linos
151d67a0cc translationBot(ui): update translation (Vietnamese)
Currently translated at 100.0% (2082 of 2082 strings)

Co-authored-by: Linos <linos.coding@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/vi/
Translation: InvokeAI/Web UI
2025-08-28 13:02:16 +10:00
Riccardo Giovanetti
72431ff197 translationBot(ui): update translation (Italian)
Currently translated at 98.6% (2053 of 2082 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2025-08-28 13:02:16 +10:00
psychedelicious
0de1feed76 chore(ui): lint 2025-08-28 12:59:35 +10:00
psychedelicious
7ffb626dbe feat(ui): add image load errors to logging 2025-08-28 12:59:35 +10:00
psychedelicious
79753289b1 feat(ui): log image failed to load errors at error level 2025-08-28 12:59:35 +10:00
psychedelicious
bac4c05fd9 feat(ui): log "destroying module" at debug level 2025-08-28 12:59:35 +10:00
psychedelicious
8a3b5d2c6f fix(ui): do not cache canvas entities when they have no w/h 2025-08-28 12:59:35 +10:00
psychedelicious
309578c19a fix(ui): progress image gets stuck on viewer when generating on canvas 2025-08-28 12:55:36 +10:00
Mary Hipp
fd58e1d0f2 update copy for API models without w/h controls 2025-08-27 09:24:22 -04:00
psychedelicious
04ffb979ce fix(ui): deny the pull of the square 2025-08-27 08:56:15 -04:00
psychedelicious
35c00d5a83 chore(ui): lint 2025-08-27 08:56:15 -04:00
psychedelicious
c2b49d58f5 fix(ui): gemini 2.5 unsupported gen mode error message 2025-08-27 08:56:15 -04:00
psychedelicious
6ff6b40a35 feat(ui): support unknown output image dimensions on canvas
Gemini 2.5 Flash makes no guarantees about output image sizes. Our
existing logic always rendered staged images on Canvas at the bbox dims
- not the image's physical dimensions. When Gemini returns an image that
doesn't match the bbox, it would get squished.

To rectify this, the canvas staging area renderer is updated to render
its images using their physical dimensions, as opposed to their
configured dimensions (i.e. bbox).

A flag on CanvasObjectImage enables this rendering behaviour.

Then, when saving the image as a layer from staging area, we use the
physical dimensions.

When the bbox and physical dimensions do not match, the bbox is not
touched, so it won't exactly encompass the staged image. No point in
resizing the bbox if the dimensions don't match - the next image could
be a different size, and the sizes might not be valid (it's an external
resource, after all).
2025-08-27 08:56:15 -04:00
psychedelicious
1f1beda567 fix(ui): remove gemini aspect ratio checking in graph builder 2025-08-27 08:56:15 -04:00
psychedelicious
91d62eb242 fix(ui): update ref image type when switching to gemini 2025-08-27 08:56:15 -04:00
psychedelicious
013e02d08b feat(ui): show w/h, scaled bbox settings only when relevant 2025-08-27 08:56:15 -04:00
psychedelicious
115053972c feat(ui): handle api model determination in a clearer way w/ list of base models; use it in dimensions component 2025-08-27 08:56:15 -04:00
psychedelicious
bcab754ac2 docs(ui): add note about reactflow types 2025-08-27 08:56:15 -04:00
psychedelicious
f1a542aca2 docs(ui): add note about extraneous coordiantes in paramsSlice 2025-08-27 08:56:15 -04:00
psychedelicious
0701cc63a1 feat(ui): hide width/height sliders for api models
These models only support aspect ratio inputs; not pixel dimensions
2025-08-27 08:56:15 -04:00
psychedelicious
9337710b45 chore(ui): lint 2025-08-27 08:56:15 -04:00
psychedelicious
592ef5a9ee feat(ui): improved support model handling when switching models
- Disable LoRAs instead of deleting them when base model changes
- Update toast message to indicate that we may have _updated_ a model
(prev just sayed cleared or disabled)
- Do not change ref image models if the new base model doesn't support
them. For example, changing from SDXL to Imagen does not update the ref
image model or alert the user, because Imagen does not support ref
images. Switching from Imagen to FLUX does update the ref image model
and alert the user. Just a bit less noisy.
2025-08-27 08:56:15 -04:00
psychedelicious
5fe39a3ae9 fix(ui): add gemini 2.5 to ref image supporting models 2025-08-27 08:56:15 -04:00
psychedelicious
1888c586ca feat(ui): do not prevent invoking when ref images are added but model does not support ref images 2025-08-27 08:56:15 -04:00
psychedelicious
88922a467e feat(ui): hide ref images UI when selected models does not support ref images 2025-08-27 08:56:15 -04:00
psychedelicious
84115e598c fix(ui): lock height slider when using api model 2025-08-27 08:56:15 -04:00
Mary Hipp
370fc67777 UI support for gemini 2.5 API model 2025-08-27 08:56:15 -04:00
Mary Hipp
fa810e1d02 add gemini 2.5 to base model 2025-08-27 08:56:15 -04:00
Attila Cseh
ec5043aa83 useNodeFieldElementExists turned private 2025-08-26 11:39:16 +10:00
Attila Cseh
9a2a0cef74 node field dnd logic updatedto prevent duplicates 2025-08-26 11:39:16 +10:00
Attila Cseh
c205c1d19e current board removed from options 2025-08-26 11:33:39 +10:00
Attila Cseh
ae1a815453 change board - sorting order of boards alphabetical 2025-08-26 11:33:39 +10:00
psychedelicious
687bc281e5 chore: prep for v6.5.0rc1 (#8479)
## Summary

Bump version

## Related Issues / Discussions

n/a

## QA Instructions

n/a

## Merge Plan

This is already released.

## Checklist

- [x] _The PR has a short but descriptive title, suitable for a
changelog_
- [ ] _Tests added / updated (if applicable)_
- [ ] _Documentation added / updated (if applicable)_
- [ ] _Updated `What's New` copy (if doing a release after this PR)_
2025-08-26 11:25:01 +10:00
psychedelicious
567316d753 chore: bump version to v6.5.0rc1 2025-08-25 18:10:18 +10:00
psychedelicious
53ac7c9d2c feat(ui): bbox aspect ratio lock is always inverted by shift 2025-08-25 17:59:20 +10:00
Riccardo Giovanetti
90be2a0cdf translationBot(ui): update translation (Italian)
Currently translated at 98.6% (2050 of 2079 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2025-08-25 17:57:54 +10:00
Attila Cseh
c7fb8f69ae code review fixes 2025-08-25 17:53:59 +10:00
Attila Cseh
7fecb8e88b formatting fixed 2025-08-25 17:53:59 +10:00
Attila Cseh
ee6a2a6603 respect direction of selection in Gallery 2025-08-25 17:53:59 +10:00
Attila Cseh
2496ac19c4 remove input field from form 2025-08-25 16:33:09 +10:00
psychedelicious
e34ed199c9 feat(ui): respect aspect ratio when resizing bbox on canvas 2025-08-25 15:30:01 +10:00
psychedelicious
569533ef80 fix(ui): toggle bbox visiblity translation 2025-08-25 14:51:34 +10:00
psychedelicious
dfac73f9f0 fix(ui): disable color picker while middle-mouse panning canvas 2025-08-25 14:47:42 +10:00
psychedelicious
f4219d5db3 chore: uv lock 2025-08-23 14:17:56 +10:00
psychedelicious
04d1958e93 feat(app): vendor in invisible-watermark
Fixes errors like `AttributeError: module 'cv2.ximgproc' has no
attribute 'thinning'` which occur because there is a conflict between
our own `opencv-contrib-python` dependency and the `invisible-watermark`
library's `opencv-python`.
2025-08-23 14:17:56 +10:00
psychedelicious
47d7d93e78 fix(ui): float input precision
Determine the "base" step for floats. If no `multipleOf` is provided,
the "base" step is `undefined`, meaning the float can have any number of
decimal places.

The UI library does its own step constrains though and is rounding to 3
decimal places. Probably need to update the logic in the UI library to
have truly arbitrary precision for float fields.
2025-08-22 13:35:59 +10:00
psychedelicious
0e17950949 fix(ui): race condition when setting hf token and downloading model
I ran into a race condition where I set a HF token and it was valid, but
somehow this error toast still appeared. The conditional feel through to
an assertion that we never expected to get to, which crashed the UI.

Handled the unexpected case gracefully now.
2025-08-22 13:30:38 +10:00
psychedelicious
b0cfdc94b5 feat(ui): do not sample alpha in Canvas color picker
Closes #7897
2025-08-21 21:38:03 +10:00
psychedelicious
bb153b55d3 docs: update quick start 2025-08-21 21:26:09 +10:00
psychedelicious
93ef637d59 docs: update latest release links 2025-08-21 21:26:09 +10:00
Attila Cseh
c5689ca1a7 code review changes 2025-08-21 19:42:38 +10:00
Attila Cseh
008e421ad4 shuffle button on workflows 2025-08-21 19:42:38 +10:00
psychedelicious
28a77ab06c Revert "experiment: add non-lfs-tracked file to lfs-tracked dir"
This reverts commit 4f4b7ddfb0.
2025-08-21 15:49:20 +10:00
psychedelicious
be48d3c12d ci: give workflow perms to label/comment on pr 2025-08-21 15:49:20 +10:00
psychedelicious
518b21a49a experiment: add non-lfs-tracked file to lfs-tracked dir 2025-08-21 15:49:20 +10:00
psychedelicious
68825ca9eb ci: add workflow to catch incorrect usage of git-lfs 2025-08-21 15:49:20 +10:00
psychedelicious
73c5f0b479 chore: bump version to v6.4.0 2025-08-19 12:19:02 +10:00
psychedelicious
7b4e04cd7c git: move test LoRA to LFS 2025-08-19 11:56:59 +10:00
Linos
ae4368fabe translationBot(ui): update translation (Vietnamese)
Currently translated at 100.0% (2073 of 2073 strings)

Co-authored-by: Linos <linos.coding@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/vi/
Translation: InvokeAI/Web UI
2025-08-19 10:28:35 +10:00
psychedelicious
df8e39a9e1 chore: bump version to v6.4.0rc2 2025-08-19 00:01:48 +10:00
psychedelicious
45b43de571 fix(ui): prevent node drag when editing title
Closes #8435
2025-08-18 23:20:28 +10:00
psychedelicious
6d18a72a05 fix(ui): fit to bbox when bbox is not aligned to 64px grid 2025-08-18 23:17:45 +10:00
Kent Keirsey
af58a75e97 Support PEFT Loras with Base_Model.model prefix (#8433)
* Support PEFT Loras with Base_Model.model prefix

* update tests

* ruff

* fix python complaints

* update kes

* format keys

* remove unneeded test
2025-08-18 09:14:46 -04:00
psychedelicious
fd4c3bd27a refactor: estimate working vae memory during encode/decode
- Move the estimation logic to utility functions
- Estimate memory _within_ the encode and decode methods, ensuring we
_always_ estimate working memory when running a VAE
2025-08-18 21:43:14 +10:00
psychedelicious
1f8a60ded2 fix(ui): export NumericalParameterConfig type 2025-08-18 21:38:17 +10:00
psychedelicious
b1b677997d chore: bump version to v6.4.0rc1 2025-08-18 21:34:09 +10:00
psychedelicious
f17b43d736 chore(ui): update whatsnew 2025-08-18 21:34:09 +10:00
psychedelicious
c009a50489 feat(ui): reduce storage persist debounce to 300ms
matches pre-server-backed-state-persistence value
2025-08-18 21:34:09 +10:00
psychedelicious
97a16c455c fix(ui): update board totals when generation completes 2025-08-18 21:34:09 +10:00
psychedelicious
a8a07598c8 chore: ruff 2025-08-18 21:14:00 +10:00
psychedelicious
23206e22e8 tests: skip excessively flaky MPS-specific tests in CI 2025-08-18 21:14:00 +10:00
psychedelicious
f4aba52b90 feat(ui): use flushSync for locateInGallery to ensure panel api calls finish before selecting image 2025-08-18 19:55:06 +10:00
psychedelicious
d17c273939 feat(ui): add locate in gallery button to current image buttons toolbar 2025-08-18 19:55:06 +10:00
psychedelicious
aeb5e7d50a feat(ui): hide locate in gallery from context when unable to actually locate
e.g. when on a tab that doesn't have a gallery, or the image is
intermediate
2025-08-18 19:55:06 +10:00
psychedelicious
580ad30832 feat(ui): use bold icon for locate in gallery 2025-08-18 19:55:06 +10:00
psychedelicious
6390f7d734 fix(ui): more reliable scrollIntoView/"Locate in Gallery"
Three changes needed to make scrollIntoView and "Locate in Gallery" work
reliably.

1. Use setTimeout to work around race condition with scrollIntoView in
gallery.

It was possible to call scrollIntoView before react-virtuoso was ready.
I think react-virtuoso was initialized but hadn't rendered/measured its
items yet, so when we scroll to e.g. index 742, the items have a zero
height, so it doesn't actually scroll down. Then the items render.

Setting a timeout here defers the scroll until after the next event loop
cycle, by which time we expect react-virutoso to be ready.

2. Ensure the scollIntoView effect in gallery triggers any time the
selection is touched by making its dependency the array of selected
images, not just the last selected image name.

The "locate in gallery" functionality works by selecting an image.
There's a reactive effect in the gallery that runs when the last
selected image changes and scrolls it into view.

But if you already have an image selected, selecting it again will not
change the image name bc it is a string primitive. The useEffect ignores
the selection.

So, if you clicked "locate in gallery" on an image that was already
selected, it wouldn't be scrolled into view - even if you had already
scrolled away from it.

To work around this, the effect now uses the whole selection array as
its dependency. Whenever the selection changes, we get a new array,
which triggers the effect.

3. Gallery slice had some checks to avoid creating a new array of
selected image names in state when the selected images didn't change.

For example, if image "abc" was selected, and we selected "abc" again,
instead of creating a new array with the same "abc" image, we bailed
early. IIRC this optimization addressed a rerender issue long ago.

This optimization needs to be removed in order for fix #2 above to work.
We now _want_ a new array whenever selection is set - even if it didn't
actually change.
2025-08-18 19:55:06 +10:00
psychedelicious
5ddbfefb6a feat(ui): add trace logging to scrollIntoView 2025-08-18 19:55:06 +10:00
psychedelicious
bbf5ed7956 fix(ui): use is_intermediate to determine if image is gallery image 2025-08-18 19:55:06 +10:00
Attila Cseh
19cd6eed08 locate in gallery image context menu 2025-08-18 19:55:06 +10:00
Attila Cseh
9c1eb263a8 new entity added above the currently selected one 2025-08-18 18:46:40 +10:00
Attila Cseh
75755189a7 prettier fixes 2025-08-18 18:46:40 +10:00
Attila Cseh
a9ab72d27d new layers created on the top of the existing layers 2025-08-18 18:46:40 +10:00
Attila Cseh
678eb34995 duplicate layer appear above original one 2025-08-18 18:46:40 +10:00
Attila Cseh
ef7050f560 merged layers order retained 2025-08-18 18:46:40 +10:00
Attila Cseh
9787d9de74 prettier fix 2025-08-18 18:30:08 +10:00
Attila Cseh
bb4a50bab2 confirmation before downloading starter bundle 2025-08-18 18:30:08 +10:00
Attila Cseh
f3554b4e1b prettier fixed 2025-08-14 21:10:21 +10:00
Attila Cseh
9dcb025241 build error fixed 2025-08-14 21:10:21 +10:00
Attila Cseh
ecf646066a CLIP skip value clamped 2025-08-14 21:10:21 +10:00
Attila Cseh
3fd10b68cd recall CLIP skip 2025-08-14 21:10:21 +10:00
Attila Cseh
6e32c7993c CLIP Skip zod schema created 2025-08-14 21:10:21 +10:00
Riccardo Giovanetti
8329533848 translationBot(ui): update translation (Italian)
Currently translated at 98.5% (2041 of 2071 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.6% (2039 of 2067 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2025-08-14 12:14:27 +10:00
psychedelicious
fc7157b029 fix(ui): do not add pos style prompt to metadata 2025-08-14 10:56:24 +10:00
psychedelicious
a1897f7490 chore(ui): lint 2025-08-14 10:56:24 +10:00
psychedelicious
a89b3efd14 feat(ui): remove SDXL style prompt from linear UI
This feature added a lot of unexpected complexity in graph building /
metadata recall and is unintuitive user experience. 99% of the time, the
style prompt should be exactly the main prompt.

You can still use style prompts in workflows, but in an effort to reduce
complexity in the linear UI, we are removing this rarely-used feature.
2025-08-14 10:56:24 +10:00
jiangmencity
5259693ed1 chore: fix some comments
Signed-off-by: jiangmencity <jiangmen@52it.net>
2025-08-14 09:32:54 +10:00
Tikal
d77c24206d Update NODES.md 2025-08-14 09:18:47 +10:00
psychedelicious
c5069557f3 fix(mm): fail when model exists at path instead of finding unused new path
When installing a model, the previous, graceful logic would increment a
suffix on the destination path until found a free path for the model.

But because model file installation and record creation are not in a
transaction, we could end up moving the file successfully and fail to
create the record:
- User attempts to install an already-installed model
- Attempt to move the downloaded model from download tempdir to
destination path
- The path already exists
- Add `_1` or similar to the path until we find a path that is free
- Move the model
- Create the model record
- FK constraint violation bc we already have a model w/ that name, but
the model file has already been moved into the invokeai dir.

Closes #8416
2025-08-13 10:40:06 +10:00
psychedelicious
9b220f61bd translations(ui): add translation for gallery settings 2025-08-12 23:34:24 +10:00
psychedelicious
7fc3af12cc translations(ui): add translation for select your model in launchpad 2025-08-12 23:34:24 +10:00
psychedelicious
e2721b46b6 translations(ui): add atranslations for add/remove negative promtp 2025-08-12 23:34:24 +10:00
psychedelicious
17118a04bd feat(ui): dynamic dockview tab title translations
Requires a ui slice migration and reset of users's layout settings to
get the right titles into dockview params state, which is persisted.
2025-08-12 23:34:24 +10:00
psychedelicious
24788e3c83 fix(ui): input field error styling specificity 2025-08-12 23:30:34 +10:00
psychedelicious
056387c981 feat(ui): allow recall of prompt and seed on upscaling tab 2025-08-12 16:21:51 +10:00
psychedelicious
8a43d90273 fix(ui): positive prompt in upscale metadata 2025-08-12 16:21:51 +10:00
psychedelicious
4f9b9760db feat(ui): debounce persistence instead of throttle 2025-08-12 16:16:11 +10:00
psychedelicious
fdaddafa56 fix(mm): only add suffix to model paths when path is file 2025-08-12 15:31:43 +10:00
psychedelicious
23d59abbd7 chore: ruff 2025-08-12 10:51:05 +10:00
psychedelicious
cf7fa5bce8 perf(backend): clear torch cache after encoding each image in kontext extension
Slightly reduces VRAM allocations.
2025-08-12 10:51:05 +10:00
psychedelicious
39e41998bb feat(ui): use latent-space kontext ref image concat in flux graph
Prevents a large spike in VRAM when preparing to denoise w/ multiple ref
images.

There doesn't appear to be any different in image quality / ref
adherence when concatenating in latent space vs image space, though
images _are_ different.
2025-08-12 10:51:05 +10:00
psychedelicious
c6eff71b74 fix(backend): bug in kontext canvas dimension tracking when concating in latent space
We weren't tracking the canvas dimensions properly which coudl result in
FLUX not "seeing" ref images after the first very well
2025-08-12 10:51:05 +10:00
psychedelicious
6ea4c47757 chore: ruff 2025-08-12 10:51:05 +10:00
psychedelicious
91f91aa835 feat(mm): prepare kontext latents before loading transformer
If the transformer fills up VRAM, then when we VAE encode kontext
latents, we'll need to first offload the transformer (partially, if
partial loading is enabled).

No need to do this - we can encode kontext latents before loading the
transformer to reduce model thrashing.
2025-08-12 10:51:05 +10:00
psychedelicious
ea7868d076 Revert "experiment(mm): investigate vae working memory calculations"
This reverts commit bc9ed57d5cd134dc7c9117395e91d22a3c4aa6de.
2025-08-12 10:51:05 +10:00
psychedelicious
7d86f00d82 feat(mm): implement working memory estimation for VAE encode for all models
Tell the model manager that we need some extra working memory for VAE
encoding operations to prevent OOMs.

See previous commit for investigation and determination of the magic
numbers used.

This safety measure is especially relevant now that we have FLUX Kontext
and may be encoding rather large ref images. Without the working memory
estimation we can OOM as we prepare for denoising.

See #8405 for an example of this issue on a very low VRAM system. It's
possible we can have the same issue on any GPU, though - just a matter
of hitting the right combination of models loaded.
2025-08-12 10:51:05 +10:00
psychedelicious
7785061e7d experiment(mm): investigate vae working memory calculations
This commit includes a task delegated to Claude to investigate our VAE
working memory calculations and investigation results.

See VAE_INVESTIGATION.md for motivation and detail. Everything else is
its output.

Result data includes empirical measurements for all supported model
architectures at a variety of resolutions and fp16/fp32 precision.
Testing conducted on a 4090.

The summarized conclusion is that our working memory estimations for
decoding are spot-on, but decoding also needs some extra working memory.
Empirical measurements suggest ~45% the amount needed for encoding.

A followup commit will implement working memory estimations for VAE
encoding with the goal of preventing unexpected OOMs during encode.
2025-08-12 10:51:05 +10:00
psychedelicious
3370052e54 fix(ui): restore deduping logic in node field element selectors
This is required for some publishing functionality
2025-08-11 22:50:05 +10:00
Attila Cseh
325dacd29c same field cannot be added to form multiple times in workflow editor 2025-08-11 22:50:05 +10:00
psychedelicious
f4981a6ba9 tidy(ui): minor cleanup 2025-08-11 22:37:46 +10:00
Attila Cseh
8c159942eb add to form icon included 2025-08-11 22:37:46 +10:00
Attila Cseh
deb4dc64af error nodes outlined in red 2025-08-11 22:37:46 +10:00
psychedelicious
1a11437b6f feat(ui): add hidden bbox hotkey to alert
If you accidentally hit the hotkey and hide the bbox it could be
difficult to figure out how to un-hide it without the hotkey called out
in the alert.
2025-08-11 22:30:45 +10:00
Attila Cseh
04572c94ad setting bbox visibility moved into render method 2025-08-11 22:30:45 +10:00
Attila Cseh
1e9e78089e Add toggle for bbox with hotkey 2025-08-11 22:30:45 +10:00
Heathen711
e65f93663d bugfix(container-builder) Use the mnt space instead of root space for docker images 2025-08-06 12:36:07 -04:00
psychedelicious
2a796fe25e chore: bump version to v6.3.0 2025-08-05 10:35:22 +10:00
psychedelicious
61ff9ee3a7 feat(ui): add button to ref image to recall size & optimize for model
This is useful for FLUX Kontext, where you typically want the generation
size to at least roughly match the first ref image size.
2025-08-05 10:28:44 +10:00
psychedelicious
111408c046 feat(mm): add flux krea to starter models 2025-08-05 10:25:14 +10:00
psychedelicious
d7619d465e feat(mm): change anime upscaling model to one that doesn't trigger picklescan 2025-08-05 10:25:14 +10:00
Kent Keirsey
8ad4f6e56d updates & fix 2025-08-05 10:10:52 +10:00
Cursor Agent
bf4899526f Add 'shift+s' hotkey for fitting bbox to canvas
Co-authored-by: kent <kent@invoke.ai>
2025-08-05 10:10:52 +10:00
psychedelicious
6435d265c6 fix(ui): overflow w/ long board names 2025-08-05 10:06:55 +10:00
Linos
3163ef454d translationBot(ui): update translation (Vietnamese)
Currently translated at 100.0% (2065 of 2065 strings)

Co-authored-by: Linos <linos.coding@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/vi/
Translation: InvokeAI/Web UI
2025-08-05 10:04:20 +10:00
Riccardo Giovanetti
7ea636df70 translationBot(ui): update translation (Italian)
Currently translated at 98.6% (2037 of 2065 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.6% (2037 of 2065 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.5% (2036 of 2065 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.6% (2014 of 2042 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2025-08-05 10:04:20 +10:00
Hosted Weblate
1869824803 translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

translationBot(ui): update translation files

Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI
2025-08-05 10:04:20 +10:00
psychedelicious
66fc8af8a6 fix(ui): reset session button actions
- Do not reset dimensions when resetting generation settings (they are
model-dependent, and we don't change model-dependent settings w/ that
butotn)
- Do not reset bbox when resetting canvas layers
- Show reset canvas layers button only on canvas tab
- Show reset generation settings button only on canvas or generate tab
2025-08-05 10:01:22 +10:00
psychedelicious
48cb6b12f0 fix(ui): add style ref launchpad using wrong dnd config
I don't think this actually caused problems bc the two DND targets were
very similar, but it was wrong.
2025-08-05 09:57:11 +10:00
psychedelicious
68e30a9864 feat(ui): prevent creating new canvases while staging
Disable these items while staging:
- New Canvas From Image context menu
- Edit image hook & launchpad button
- Generate from Text launchpad button (only while on canvas tab)
- Use a Layout Image launchpad button
2025-08-05 09:57:11 +10:00
psychedelicious
f65dc2c081 chore(ui): typegen 2025-08-05 09:54:00 +10:00
psychedelicious
0cd77443a7 feat(app): add setting to disable picklescan
When unsafe_disable_picklescan is enabled, instead of erroring on
detections or scan failures, a warning is logged.

A warning is also logged on app startup when this setting is enabled.

The setting is disabled by default and there is no change in behaviour
when disabled.
2025-08-05 09:54:00 +10:00
Mary Hipp
185ed86424 fix graph building 2025-08-04 12:32:27 -04:00
Mary Hipp
fed817ab83 add image concatenation to flux kontext graph if more than one refernece image 2025-08-04 11:27:02 -04:00
Mary Hipp
e0b45db69a remove check in readiness for multiple reg images 2025-08-04 11:27:02 -04:00
psychedelicious
2beac1fb04 chore: bump version to v6.3.0rc2 2025-08-04 23:55:04 +10:00
psychedelicious
e522de33f8 refactor(nodes): roll back latent-space resizing of kontext images 2025-08-04 23:03:12 +10:00
psychedelicious
d591b50c25 feat(ui): use image-space concatenation in FLUX graphs 2025-08-04 23:03:12 +10:00
psychedelicious
b365aad6d8 chore(ui): typegen 2025-08-04 23:03:12 +10:00
psychedelicious
65ad392361 feat(nodes): add node to prep images for FLUX Kontext 2025-08-04 23:03:12 +10:00
psychedelicious
56d75e1c77 feat(backend): use VAE mean encoding for Kontext reference images
Use distribution mean without sampling noise for more stable and
consistent reference image encoding, matching ComfyUI implementation
2025-08-04 23:03:12 +10:00
psychedelicious
df77a12efe refactor(backend): use torchvision transforms for Kontext image preprocessing
Replace numpy-based normalization with torchvision transforms for
consistency with other image processing in the codebase
2025-08-04 23:03:12 +10:00
psychedelicious
faf662d12e refactor(backend): use BICUBIC resampling for Kontext images
Switch from LANCZOS to BICUBIC for smoother image resizing to reduce
artifacts in reference image processing
2025-08-04 23:03:12 +10:00
psychedelicious
44a7dfd486 fix(backend): use consistent idx_offset=1 for all Kontext images
Changes from per-image index offsets to a consistent value of 1 for
all reference images, matching the ComfyUI implementation
2025-08-04 23:03:12 +10:00
psychedelicious
bb15e5cf06 feat(backend): add spatial tiling for multiple Kontext reference images
Implements intelligent spatial tiling that arranges multiple reference
images in a virtual canvas, choosing between horizontal and vertical
placement to maintain a square-like aspect ratio
2025-08-04 23:03:12 +10:00
psychedelicious
1a1c846be3 feat(backend): include reference images in negative CFG pass for Kontext
Maintains consistency between positive and negative passes to prevent
CFG artifacts when using Kontext reference images
2025-08-04 23:03:12 +10:00
psychedelicious
93c896a370 fix(backend): use img_cond_seq to check for Kontext slicing
Was incorrectly checking img_input_ids instead of img_cond_seq
2025-08-04 23:03:12 +10:00
psychedelicious
053d7c8c8e feat(ui): support disabling roarr output styling via localstorage 2025-07-31 23:02:45 +10:00
psychedelicious
5296263954 feat(ui): add missing translations 2025-07-31 22:51:33 +10:00
psychedelicious
a36b70c01c fix(ui): add image name data attr to gallery placeholder image elements
This fixes an issue where gallery's auto-scroll-into-view for selected
images didn't work, and users instead saw a "Unable to find image..."
debug log message in JS console.
2025-07-31 22:48:42 +10:00
psychedelicious
854a2a5a7a chore: bump version to v6.3.0rc1 2025-07-31 14:17:18 +10:00
psychedelicious
f9c64b0609 chore(ui): update whats new 2025-07-31 14:17:18 +10:00
psychedelicious
5889fa536a feat(ui): add migration path for client state from IndexedDB to server-backed storage 2025-07-31 14:09:45 +10:00
Linos
0e71ba892f translationBot(ui): update translation (Vietnamese)
Currently translated at 100.0% (2044 of 2044 strings)

Co-authored-by: Linos <linos.coding@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/vi/
Translation: InvokeAI/Web UI
2025-07-31 13:59:21 +10:00
Riccardo Giovanetti
d766a21223 translationBot(ui): update translation (Italian)
Currently translated at 98.6% (2016 of 2044 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2025-07-31 13:59:21 +10:00
psychedelicious
5c8c54eab8 chore: ruff 2025-07-31 06:38:48 +10:00
psychedelicious
f296f4525c tidy(ui): disable logging middleware 2025-07-31 06:38:48 +10:00
psychedelicious
7c9ba4cb52 refactor(ui): add persistence gate logic to prevent race conditions with slow rehydration 2025-07-31 06:38:48 +10:00
psychedelicious
6784fd5b43 refactor(ui): use new routes for _all_ client state persistence (no override/custom drivers) 2025-07-31 06:38:48 +10:00
psychedelicious
11d68cc646 chore(ui): typegen 2025-07-31 06:38:48 +10:00
psychedelicious
ea8c877025 refactor(app): move client state persistence to own route, add queue_id 2025-07-31 06:38:48 +10:00
psychedelicious
7a3c2332dd feat(ui): add visual indicator when input field is added to form 2025-07-31 06:33:22 +10:00
psychedelicious
3835fd2f72 feat(ui): zhoosh image comparison ui 2025-07-30 07:20:47 -04:00
psychedelicious
6f8746040c docs(ui): update comments in readiness re: flux kontext via bfl api 2025-07-30 12:26:48 +10:00
psychedelicious
35e3940a09 feat(ui): update warning when using multiple ref images on BFL API kontext
It only supports 1 image.
2025-07-30 12:26:48 +10:00
psychedelicious
415616d83f feat(ui): support multiple kontext ref images in studio 2025-07-30 12:26:48 +10:00
psychedelicious
afb67efef9 chore(ui): typegen 2025-07-30 12:26:48 +10:00
psychedelicious
1ed1fefa60 feat(nodes): support multiple kontext ref images
Images are concatenated in latent space.
2025-07-30 12:26:48 +10:00
Ar7ific1al
fa94a05c77 Update CanvasStateApiModule.ts
Add temporary grid snap with ctrl, optional small step with ctrl+shift, while grid snap is off
2025-07-30 12:16:42 +10:00
psychedelicious
7a23d8266f feat(ui): simpler storage driver impl 2025-07-30 05:53:20 +10:00
psychedelicious
a44de079dd perf(ui): instantiate logger for storage error handler once 2025-07-30 05:53:20 +10:00
psychedelicious
c3c1a3edd8 chore(ui): typegen 2025-07-30 05:53:20 +10:00
psychedelicious
ea26b5b147 feat(app): client state persistence endpoints accept stringified data 2025-07-30 05:53:20 +10:00
Eugene Brodsky
4226b741b1 fix(docker) rocm 6.3 based image (#8152)
1. Fix the run script to properly read the GPU_DRIVER
2. Cloned and adjusted the ROCM dockerbuild for docker
3. Adjust the docker-compose.yml to use the cloned dockerbuild
2025-07-29 10:16:42 -04:00
Eugene Brodsky
1424b7c254 Merge branch 'main' into bugfix/heathen711/rocm-docker 2025-07-29 10:12:13 -04:00
psychedelicious
933fb2294c fix(ui): zod rejects any board id besides "none"
Turns out the string autocomplete TS hack does not translate to zod.
Widen the zod schema to any string, but use the hack for the TS type.
2025-07-29 08:45:16 -04:00
psychedelicious
5a181ee0fd build(ui): export loading component 2025-07-29 08:43:03 -04:00
psychedelicious
3b0d59e459 tests(app): update mm tests to test updated behaviour 2025-07-29 16:08:15 +10:00
psychedelicious
fec296e41d fix(app): move (not copy) models from install tmpdir to destination
It's not clear why we were copying downloaded models to the destination
dir instead of moving them. I cannot find a reason for it, and I am able
to install single-file and diffusers models just fine with the change.

This fixes an issue where model installation requires 2x the model's
size (bc we were copying the model over).
2025-07-29 16:08:15 +10:00
Heathen711
ae4e38c6d0 Merge branch 'main' into bugfix/heathen711/rocm-docker 2025-07-28 21:24:34 -07:00
psychedelicious
a9f3f1a4b2 fix(app): handle model files with periods in their name
Previously, we used pathlib's `with_suffix()` method to change add a
suffix (e.g. ".safetensors") to a model when installing it.

The intention is to add a suffix to the model's name - but that method
actually replaces everything after the first period.

This can cause different models to be installed under the same name!

For example, the FLUX models all end up with the same name:
- "FLUX.1 schnell.safetensors" -> "FLUX.safetensors"
- "FLUX.1 dev.safetensors" -> "FLUX.safetensors"

The fix is easy - append the suffix using string formatting instead of
using pathlib.

This issue has existed for a long time, but was exacerbated in
075345bffd in which I updated the names of
our starter models, adding ".1" to the FLUX model names. Whoops!
2025-07-29 14:15:59 +10:00
psychedelicious
8a73df4fe1 fix(ui): progress image does not hide on viewer with autoswitch disabled 2025-07-29 12:53:45 +10:00
psychedelicious
ea2e1ea8f0 fix(ui): queue count badge renders when left panel collapsed 2025-07-29 12:51:23 +10:00
psychedelicious
e8aa91931d fix(ui): connect metadata to output node for ext api nodes 2025-07-29 06:46:17 +10:00
psychedelicious
8d22a314a6 docs(ui): add some comments for race condition handling 2025-07-29 06:34:08 +10:00
psychedelicious
57ce2b8aa7 chore(ui): lint 2025-07-29 06:34:08 +10:00
psychedelicious
6b810cb3fb fix(ui): race condition w/ queue counts 2025-07-29 06:34:08 +10:00
psychedelicious
4f3a5dcc43 tidy(ui): remove unused progress related logic and components 2025-07-29 06:34:08 +10:00
psychedelicious
c3ae14cf73 fix(ui): ignore events for already-completed queue items 2025-07-29 06:34:08 +10:00
psychedelicious
b9c44b92d5 fix(ui): clear progress images from viewer at the right time 2025-07-29 06:34:08 +10:00
psychedelicious
5a68b4ddbc build(ui): skip logging ctx plugin when running tests 2025-07-29 06:31:30 +10:00
psychedelicious
18a722839b chore(ui): update knip conifg 2025-07-29 06:31:30 +10:00
psychedelicious
7370cb9be6 build(ui): add vite plugin to add relative file path to logger context 2025-07-29 06:31:30 +10:00
Kent Keirsey
cc4df52f82 feat: server-side client state persistence (#8314)
## Summary

Move client state persistence from browser to server.

- Add new client state persistence service to handle reading and writing
client state to db & associated router. The API mirrors that of
LocalStorage/IndexedDB where the set/get methods both operate on _keys_.
For example, when we persist the canvas state, we send only the new
canvas state to the backend - not the whole app state.
- The data is very flexibly-typed as a pydantic `JsonValue`. The client
is expected to handle all data parsing/validation (it must do this
anyways, and does this today).
- Change persistence from debounced to throttled at 2 seconds. Maybe
less is OK? Trying to not hammer the server.
- Add new persistence storage driver in client and use it in
redux-remember. It does its best to avoid extraneous persist requests,
caching the last data it persisted and noop-ing if there are no changes.
- Storage driver tracks pending persist actions using ref counts (bc
each slice is persisted independently). If there user navigates away
from the page during a persist request, it will give them the "you may
lose something if you navigate away" alert.
- This "lose something" alert message is not customizable (browser
security reasons).
- The alert is triggered only when the user closes the tape while a
persist network request is mid-flight. It's possible that the user makes
a change and closes the page before we start persisting. In this case,
they will lose the last 2 seconds of data.
- I tried making triggering the alert when a persist was waiting to
start, and it felt off.
- Maybe the alert isn't even necessary. Again you'd lose 2s of data at
most, probably a non issue. IMO after trying it, a subtle indicator
somewhere on the page is probably less confusing/intrusive.
- Fix an issue where the `redux-remember` enhancer was added _last_ in
the enhancer chain, which prevented us detecting when a persist has
succeeded. This required a small change to the `unserialze` utility
(used during rehydration) to ensure slices enhanced with `redux-undo`
are set up correctly as they are rehydrated.
- Restructure the redux store code to avoid circular dependencies. I
couldn't figure out how to do this without just smooshing it all into
the main `store.ts` file. Oh well.

Implications:
- Because client state is now on the server, different browsers will
have the same studio state. For example, if I start working on something
in Firefox, if I switch to Chrome, I have the same client state.
- Incognito windows won't do anything bc client state is server-side.
- It takes a bit longer for persistence to happen thanks to the
debounce, but there's now an indicator that tells you your stuff isn't
saved yet.
- Resetting the browser won't fix an issue with your studio state. You
must use `Reset Web UI` to fix it (or otherwise hit the appropriate
endpoint). It may be possible to end up in a Catch-22 where you can't
click the button and get stuck w/ a borked studio - I think to think
through this a bit more, might not be an issue.
- It probably takes a bit longer to start up, since we need to retrieve
client state over network instead of directly with browser APIs.

Other notes:
- We could explore adding an "incognito" mode, enabled via
`invokeai.yaml` setting or maybe in the UI. This would temporarily
disable persistence. Actually, I don't think this really makes sense, bc
all the images would be saved to disk.
- The studio state is stored in a single row in the DB. Currently, a
static row ID is used to force the studio state to be a singleton. It is
_possible_ to support multiple saved states. Might be a solve for app
workspaces.

## Related Issues / Discussions

n/a

## QA Instructions

Try it out. It's pretty straightforward. Error states are the main
things to test - for example, network blips. The new server-side
persistence driver is the only real functional change - everything else
is just kinda shuffling things around to support it.

## Merge Plan

n/a

## Checklist

- [x] _The PR has a short but descriptive title, suitable for a
changelog_
- [ ] _Tests added / updated (if applicable)_
- [ ] _Documentation added / updated (if applicable)_
- [ ] _Updated `What's New` copy (if doing a release after this PR)_
2025-07-25 12:08:47 -04:00
Kent Keirsey
1cb4ef05a4 add newline 2025-07-25 11:08:54 -04:00
Kent Keirsey
7da141101c Merge branch 'main' into psyche/feat/app/client-state-persistence 2025-07-25 11:07:17 -04:00
psychedelicious
2571e199c5 tidy(ui): remove unused props 2025-07-25 11:06:18 -04:00
psychedelicious
79e93f905e fix(ui): add separate wrapper components for notes and current image nodes that do not need invocation node context 2025-07-25 11:06:18 -04:00
psychedelicious
f562e4f835 fix(ui): ensure all node context provider wraps all calls to useInvocationNodeContext 2025-07-25 11:06:18 -04:00
psychedelicious
47e220aaf3 perf(ui): imperatively get nodes and edges in autolayout hook 2025-07-25 11:06:18 -04:00
psychedelicious
9365154bfe chore: bump version to v6.2.0 2025-07-25 11:06:18 -04:00
psychedelicious
afc6911c96 chore: bump version to v6.3.0a1 2025-07-25 19:07:08 +10:00
psychedelicious
afa1ee7ffd tidy(ui): enable devmode redux checks 2025-07-25 19:04:21 +10:00
psychedelicious
5a102f6b53 chore(ui): lint 2025-07-25 19:04:21 +10:00
psychedelicious
af345a33f3 fix(ui): infinite loop when setting tile controlnet model 2025-07-25 19:04:21 +10:00
psychedelicious
038b110a82 fix(ui): do not store whole model configs in state 2025-07-25 19:04:21 +10:00
psychedelicious
f3cd49d46e refactor(ui): just manually validate async stuff 2025-07-25 19:04:21 +10:00
psychedelicious
ca7d7c9d93 refactor(ui): work around zod async validation issue 2025-07-25 19:04:21 +10:00
psychedelicious
1addeb4b59 fix(ui): check initial retrieval and set as last persisted 2025-07-25 19:04:21 +10:00
psychedelicious
6ea4884b0c chore(ui): bump zod to latest
Checking if it fixes an issue w/ async validators
2025-07-25 19:04:21 +10:00
psychedelicious
aed9b1013e refactor(ui): use zod for all redux state 2025-07-25 19:04:21 +10:00
psychedelicious
6962536b4a refactor(ui): use zod for all redux state (wip)
needed for confidence w/ state rehydration logic
2025-07-25 19:04:21 +10:00
psychedelicious
7e59d040aa feat(ui): iterate on storage api 2025-07-25 19:04:20 +10:00
psychedelicious
e7c67da2c2 refactor(ui): restructure persistence driver creation to support custom drivers 2025-07-25 19:04:20 +10:00
psychedelicious
c44571bc36 revert(ui): temp changes to main.tsx for testing 2025-07-25 19:04:20 +10:00
psychedelicious
ca257650d4 revert(ui): temp disable eslint rule 2025-07-25 19:04:20 +10:00
psychedelicious
6a9962d2bb git: update gitignore 2025-07-25 19:04:20 +10:00
psychedelicious
9492569a2c wip 2025-07-25 19:04:20 +10:00
psychedelicious
61e711620d chore: ruff 2025-07-25 19:04:20 +10:00
psychedelicious
3cf82505bb tests(app): service mocks 2025-07-25 19:04:20 +10:00
psychedelicious
53bcbc58f5 chore(ui): lint 2025-07-25 19:04:20 +10:00
psychedelicious
42f3990f7a refactor(ui): iterate on persistence 2025-07-25 19:04:20 +10:00
psychedelicious
456205da17 refactor(ui): iterate on persistence 2025-07-25 19:04:20 +10:00
psychedelicious
ca0684700e refactor(ui): alternate approach to slice configs 2025-07-25 19:04:19 +10:00
psychedelicious
6a702821ef chore(ui): typegen 2025-07-25 19:04:19 +10:00
psychedelicious
682d271f6f feat(api): make client state key query not body 2025-07-25 19:04:19 +10:00
psychedelicious
e872c253b1 refactor(ui): cleaner slice definitions 2025-07-25 19:04:19 +10:00
psychedelicious
28633c9983 feat: server-side client state persistence 2025-07-25 19:04:19 +10:00
psychedelicious
70ac58e64a tidy(ui): remove unused props 2025-07-25 18:51:21 +10:00
psychedelicious
e653837236 fix(ui): add separate wrapper components for notes and current image nodes that do not need invocation node context 2025-07-25 18:51:21 +10:00
psychedelicious
2bbfcc2f13 fix(ui): ensure all node context provider wraps all calls to useInvocationNodeContext 2025-07-25 18:51:21 +10:00
psychedelicious
d6e0e439c5 perf(ui): imperatively get nodes and edges in autolayout hook 2025-07-25 18:50:59 +10:00
psychedelicious
26aab60f81 chore: bump version to v6.2.0 2025-07-25 18:41:00 +10:00
Riccardo Giovanetti
7bea2fa11f translationBot(ui): update translation (Italian)
Currently translated at 98.6% (2016 of 2044 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.6% (2015 of 2043 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2025-07-25 17:15:01 +10:00
psychedelicious
169d58ea4c feat(ui): restore clear queue button
It is accessible in two places:
- The queue actions hamburger menu.
- On the queue tab.

If the clear queue app feature is disabled, it is not shown in either of
those places.
2025-07-23 23:38:53 +10:00
psychedelicious
b53d2250f7 feat(ui): reduce snap tolerance to make it easier to break the snap 2025-07-23 23:05:40 +10:00
psychedelicious
242eea8295 fix(ui): incorrect zoom direction w/ small scroll amounts 2025-07-23 23:05:40 +10:00
psychedelicious
4dabe09e0d tests(ui): remove test for no-longer-valid behaviour 2025-07-23 23:03:02 +10:00
psychedelicious
07fa0d3b77 fix(ui): do not attempt toggle when target panel isn't registered 2025-07-23 23:03:02 +10:00
psychedelicious
e97f82292f tests(ui): add tests for disposable handling 2025-07-23 23:03:02 +10:00
psychedelicious
005bab9035 fix(ui): tab disposables not being added correctly 2025-07-23 23:03:02 +10:00
psychedelicious
409173919c tests(ui): add tests for toggleViewer functionality 2025-07-23 23:03:02 +10:00
psychedelicious
7915180047 feat(ui): restore viewer toggle hotkey 2025-07-23 23:03:02 +10:00
Riccardo Giovanetti
4349b8387d translationBot(ui): update translation (Italian)
Currently translated at 97.9% (2000 of 2042 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2025-07-23 12:26:48 +10:00
Kent Keirsey
f95b686bdc reposition export button 2025-07-23 11:55:11 +10:00
Mary Hipp
72afb9c3fd fix iterations for all API models 2025-07-22 13:27:35 -04:00
Mary Hipp
f004fc31f1 update whats new 2025-07-22 12:24:10 -04:00
psychedelicious
2aa163b3a2 feat(ui): add default inpaint mask layer on canvas reset 2025-07-22 10:26:57 +10:00
psychedelicious
f40900c173 chore: bump version to v6.1.0 2025-07-22 08:24:31 +10:00
psychedelicious
2c1f2b2873 tidy(ui): move star hotkey into own hook & use reactive state for focus 2025-07-22 08:11:57 +10:00
Kent Keirsey
8418e34480 lint 2025-07-22 08:11:57 +10:00
Kent Keirsey
b548ac0ccf Add Star/Unstar Hotkey and fix hotkey translations 2025-07-22 08:11:57 +10:00
Linos
2af2b8b6c4 translationBot(ui): update translation (Vietnamese)
Currently translated at 100.0% (2003 of 2003 strings)

Co-authored-by: Linos <linos.coding@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/vi/
Translation: InvokeAI/Web UI
2025-07-22 07:58:19 +10:00
Hosted Weblate
058dc06748 translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI
2025-07-22 07:58:19 +10:00
Riccardo Giovanetti
8acb1c0088 translationBot(ui): update translation (Italian)
Currently translated at 98.7% (1978 of 2003 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.7% (1978 of 2003 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.6% (1968 of 1994 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2025-07-22 07:58:19 +10:00
Hosted Weblate
683732a37c translationBot(ui): update translation files
Updated by "Cleanup translation files" hook in Weblate.

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/
Translation: InvokeAI/Web UI
2025-07-22 07:58:19 +10:00
Riku
b990eacca0 translationBot(ui): update translation (German)
Currently translated at 62.1% (1251 of 2012 strings)

Co-authored-by: Riku <riku.block@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/
Translation: InvokeAI/Web UI
2025-07-22 07:58:19 +10:00
RyoKoba
5f7e920deb translationBot(ui): update translation (Japanese)
Currently translated at 99.8% (2007 of 2011 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 99.8% (2007 of 2011 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 99.8% (2007 of 2011 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 99.8% (2007 of 2011 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 99.8% (2007 of 2011 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 92.0% (1851 of 2011 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 92.0% (1851 of 2011 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 92.0% (1851 of 2011 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 87.4% (1744 of 1995 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 87.4% (1744 of 1995 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 81.0% (1616 of 1995 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 81.0% (1616 of 1995 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 81.0% (1616 of 1995 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 81.0% (1616 of 1995 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 81.0% (1616 of 1995 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 81.0% (1616 of 1995 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 81.0% (1616 of 1995 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 81.0% (1616 of 1995 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 81.0% (1616 of 1995 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 75.6% (1510 of 1995 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 75.6% (1510 of 1995 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 75.6% (1510 of 1995 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 75.6% (1510 of 1995 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 75.6% (1510 of 1995 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 75.6% (1510 of 1995 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 75.6% (1510 of 1995 strings)

translationBot(ui): update translation (Japanese)

Currently translated at 75.6% (1510 of 1995 strings)

Co-authored-by: RyoKoba <kobayashi_ryo@cyberagent.co.jp>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ja/
Translation: InvokeAI/Web UI
2025-07-22 07:58:19 +10:00
Riccardo Giovanetti
55dfdc0a9c translationBot(ui): update translation (Italian)
Currently translated at 97.9% (1953 of 1994 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.7% (1986 of 2011 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.7% (1970 of 1995 strings)

translationBot(ui): update translation (Italian)

Currently translated at 97.8% (1910 of 1952 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2025-07-22 07:58:19 +10:00
Linos
10d6d19e17 translationBot(ui): update translation (Vietnamese)
Currently translated at 100.0% (2012 of 2012 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 100.0% (2012 of 2012 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 99.7% (2006 of 2012 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 99.7% (2006 of 2012 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 99.5% (2002 of 2012 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 99.5% (2002 of 2012 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 97.8% (1968 of 2012 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 97.8% (1968 of 2012 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 97.8% (1968 of 2012 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 97.8% (1968 of 2012 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 96.4% (1940 of 2012 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 96.4% (1940 of 2012 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 100.0% (1921 of 1921 strings)

translationBot(ui): update translation (Vietnamese)

Currently translated at 100.0% (1917 of 1917 strings)

Co-authored-by: Linos <linos.coding@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/vi/
Translation: InvokeAI/Web UI
2025-07-22 07:58:19 +10:00
skunkworxdark
15542b954d Fix nodes ui: Make nodes dot background to be the same as the snap to grid size and position
Fix nodes ui:  Make nodes dot background to be the same as the snap to grid size and position
Update to Flow.tsx

Changes the size and offset of the dots background to be the same size as the snap to grid, and also fix the background dot pattern alignment.

Currently, the snapGrid is 25x25, and the default background dot gap is 20x20, these do not align.  This is fixed by making the gap property of the background the same as the snapGrid.

Additionally, there is a bug in the rectFlow background code that incorrectly sets the offset to be the centre of the dot pattern with the default offset of 0.  To work around this issue, setting the background offset property to the snapGrid size will realign the dot pattern correctly. 

I have logged a bug for the rectFlow background issue in its repo. 
https://github.com/xyflow/xyflow/issues/5405
2025-07-22 07:46:52 +10:00
skunkworxdark
6430d830c1 Update nodes auto layout spacing for snap to grid size
Update workflowSettingsSlice.ts

Change the default settings for auto layout nodeSpacing and layerSpacing  to 30 instead of 32.    This will make the x position of auto layed nodes land on the snap to grid positions. 

Because the node width (320) + 30 = 350 which is divisible by the snap to grid size of 25.
2025-07-22 07:40:58 +10:00
Kent Keirsey
c3f6389291 fix ruff and remove unused API route 2025-07-22 07:33:48 +10:00
Kent Keirsey
070eef3eff remove whitespace 2025-07-22 07:33:48 +10:00
Kent Keirsey
b14d841d57 Extract util and fix model image logic 2025-07-22 07:33:48 +10:00
Kent Keirsey
dd35ab026a update logic and remove bad test 2025-07-22 07:33:48 +10:00
Cursor Agent
7fc06db8ad Add LoRA model metadata extraction from JSON and PNG files
Co-authored-by: kent <kent@invoke.ai>
2025-07-22 07:33:48 +10:00
psychedelicious
9d1f09c0f3 fix(ui): return wrapped history in redux-remember unserialize
We intermittently get an error like this:
```
TypeError: Cannot read properties of undefined (reading 'length')
```

This error is caused by a `redux-undo`-enhanced slice being rehydrated
without the extra stuff it adds to the slice to make it undoable (e.g.
an array of `past` states, the `present` state, array of `future`
states, and some other metadata).

`redux-undo` may need to check the length of the past/future arrays as
part of its internal functionality. These keys don't exist so we get the
error. I'm not sure _why_ they don't exist - my understanding of
`redux-undo` is that it should be checking and wrapping the state w/ the
history stuff automatically. Seems to be related to `redux-remember` -
may be a race condition.

The solution is to ensure we wrap rehydrated state for undoable slices
as we rehydrate them. I discovered the solution while troubleshooting
#8314 when the changes therein somehow triggered the issue to start
occuring every time instead of rarely.
2025-07-22 07:00:57 +10:00
skunkworxdark
cacfb183a6 Add auto layout controls to node editor (#8239)
* Add auto layout controls using elkjs to node editor

Introduces auto layout functionality for the node editor using elkjs, including a new UI popover for layout options (placement strategy, layering, spacing, direction). Adds related state and actions to workflowSettingsSlice, updates translations, and ensures elkjs is included in optimized dependencies.

* feat(nodes): Improve workflow auto-layout controls and accuracy

- The auto-layout settings panel is updated to use `Select` dropdowns and `NumberInput`
- The layout algorithm now uses the actual rendered dimensions of nodes from the DOM, falling back to estimates only when necessary. This results in a much more accurate and predictable layout.
- The ELKjs library integration is refactored to fix some warnings

* Update useAutoLayout.ts

prettier

* feat(nodes): Improve workflow auto-layout controls and accuracy

- The auto-layout settings panel is updated to use `Select` dropdowns and `NumberInput`
- The layout algorithm now uses the actual rendered dimensions of nodes from the DOM, falling back to estimates only when necessary. This results in a much more accurate and predictable layout.
- The ELKjs library integration is refactored to fix some warnings

* Update useAutoLayout.ts

prettier

* build(ui): import elkjs directly

* updated to use  dagrejs for autolayout

updated to use dagrejs - it has less layout options but is already included

but this is still WIP as some nodes don't report the height correctly. I am still investigating this...

* Update useAutoLayout.ts

update to fix layout issues

* minor updates

- pretty useAutoLayout.ts
- add missing type import in ViewportControls.tsx
- update pnpm-lock.yaml with elkjs removed

* Update ViewportControls.tsx

pnpm fix

* Fix Frontend check + single node selection fix

Fix Frontend check -  remove unused export from workflowSettingsSlice.ts
Update so that if you have a single node selected, it will auto layout all nodes, as this is a common thing to have a single node selected and means that you don't have to unselect it.

* feat(ui): misc improvements for autolayout

- Split popover into own component
- Add util functions to get node w/h
- Use magic wand icon for button
- Fix sizing of input components
- Use CompositeNumberInput instead of base chakra number input
- Add zod schemas for string values and use them in the component to
ensure state integrity

* chore(ui): lint

---------

Co-authored-by: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
2025-07-21 14:44:29 +10:00
psychedelicious
564f4f7a60 feat(ui): better icon for invert mask button 2025-07-21 13:47:02 +10:00
Kent Keirsey
113a118fcf fix potential for null data 2025-07-21 13:47:02 +10:00
Kent Keirsey
1f930cdaf2 fix 2025-07-21 13:47:02 +10:00
Kent Keirsey
c490e0ce08 feat(ui):invert mask 2025-07-21 13:47:02 +10:00
Kent Keirsey
7640ee307c feat(ui):Adjust-bbox-to-masks 2025-07-21 13:26:49 +10:00
psychedelicious
1f5f70f898 feat(ui): clean up picker compact view default state handling
- Name it `pickerCompactViewStates` bc its not exclusive to model
picker, it is used for all pickers
- Rename redux action to model an event
- Move selector to right file
- Use selector to derive state for individual picker
2025-07-21 13:18:09 +10:00
Mary Hipp
1430858112 cleanup 2025-07-21 13:18:09 +10:00
Mary Hipp
48c27ec117 persist model picker compact/expanded state 2025-07-21 13:18:09 +10:00
psychedelicious
af7737e804 fix(ui): context menu on staging area images
There was a subtle issue where the progress image wasn't ever cleared,
preventing the context menu from working on staging area preview images.

The staging area preview images were displaying the last progress image
_on top of_ the result image. Because the image elements were so small,
you wouldn't notice that you were looking at a low-res progress image.
Right clicking a progress image gets you no menu.

If you refresh the page or switch tabs, this would fix itself, because
those actions clear out the progress images. The result image would then
be the topmost element, and the context menu works.

Fixing this without introducing a flash of empty space as the progress
image was hidden required a bit of refactoring. We have to wait for the
result image element to load before clearing out the progress.

Result - progress images appear to "resolve" to result images in the
staging area without any blips or jank, and the context menu works after
that happens.
2025-07-21 13:15:34 +10:00
psychedelicious
3eca0d2ba0 fix(ui): staging area left/right hotkeys 2025-07-18 08:08:15 -04:00
psychedelicious
307259f096 fix(ui): ensure staging area always has the right state and session association 2025-07-18 08:08:15 -04:00
psychedelicious
bed01941a5 fix(ui): ensure we clean up when session id changes 2025-07-18 08:08:15 -04:00
psychedelicious
89fa43a3b6 docs(ui): update StagingAreaApi docstrings 2025-07-18 08:08:15 -04:00
psychedelicious
d8fcb08abf repo: update ignores 2025-07-18 08:08:15 -04:00
psychedelicious
c61bcd9f50 tests(ui): add test suite for StagingAreaApi 2025-07-18 08:08:15 -04:00
psychedelicious
3fb0fcbbfb tidy(ui): move staging area components to correct dir 2025-07-18 08:08:15 -04:00
psychedelicious
db9af5083f tidy(ui): move launchpad components to ui dir 2025-07-18 08:08:15 -04:00
psychedelicious
720f1bb65c chore(ui): rename context2.tsx -> context.tsx 2025-07-18 08:08:15 -04:00
psychedelicious
7dfb318ba2 chore(ui): lint 2025-07-18 08:08:15 -04:00
psychedelicious
9b024da2b4 refactor(ui): move staging area logic out side react
Was running into difficultlies reasoning about the logic and couldn't
write tests because it was all in react.

Moved logic outside react, updated context, make it testable.
2025-07-18 08:08:15 -04:00
psychedelicious
15ca3b727a wip 2025-07-18 08:08:15 -04:00
psychedelicious
74ca604ae0 fix(ui): unstyled error boundary 2025-07-18 08:08:15 -04:00
psychedelicious
6934b05c85 fix(ui): use invocation context provider in inspector panel 2025-07-18 08:08:15 -04:00
psychedelicious
1a47a5317c chore(ui): update dockview to latest
Remove extraneous fix now that the disableDnd issue is resolved upstream
2025-07-18 08:08:15 -04:00
psychedelicious
bc3ef21c64 chore(ui): bump version to v6.1.0rc2 2025-07-18 08:08:15 -04:00
psychedelicious
e329f5ad43 fix(ui): negative style prompt not recorded in metadata 2025-07-18 06:41:21 +10:00
psychedelicious
e6ad91bf89 chore(ui): update prettier config 2025-07-17 22:04:57 +10:00
psychedelicious
2f586416a5 chore(ui): remove unused pkgs 2025-07-17 22:04:57 +10:00
psychedelicious
33b56f421c chore(ui): lint 2025-07-17 22:04:57 +10:00
psychedelicious
e58ee4c492 chore(ui): upgrade zod 2025-07-17 22:04:57 +10:00
psychedelicious
49691aa07e chore(ui): upgrade rollup vis 2025-07-17 22:04:57 +10:00
psychedelicious
56570f235f chore(ui): actually upgrade storybook 2025-07-17 22:04:57 +10:00
psychedelicious
a2d95cf5b6 chore(ui): upgrade minor bump packages 2025-07-17 22:04:57 +10:00
psychedelicious
704dbfd04a chore(ui): upgrade storybook 2025-07-17 22:04:57 +10:00
psychedelicious
5d9e078043 chore(ui): finish eslint v9 migration 2025-07-17 22:04:57 +10:00
psychedelicious
875cde13ae chore(ui): migrate to eslint v9 (wip) 2025-07-17 22:04:57 +10:00
psychedelicious
77655aed86 chore(ui): update eslint config 2025-07-17 22:04:57 +10:00
psychedelicious
0628b92d63 chore: bump version to v6.1.0rc1 2025-07-17 19:30:38 +10:00
psychedelicious
9e526d00c2 chore(ui): lint 2025-07-17 15:36:24 +10:00
psychedelicious
1a24396be8 feat(ui): styling when nodes have error 2025-07-17 15:36:24 +10:00
psychedelicious
d97e73a565 chore(ui): lint 2025-07-17 15:36:24 +10:00
psychedelicious
55b14c8aaf perf(ui): optimize redux selectors for workflow editor
- Build selectors for each node in a react context so components can
re-use the same selectors
- Cache the selectors in the context
2025-07-17 15:36:24 +10:00
Heathen711
1cdd4b5980 bugfix(docs) link syntax 2025-07-17 04:26:06 +00:00
psychedelicious
79f65e57eb fix(ui): remove unnecessary coalescing operator 2025-07-17 14:21:02 +10:00
Kent Keirsey
b4c8950278 address comments 2025-07-17 14:21:02 +10:00
Kent Keirsey
400b2e9a55 unlint. 2025-07-17 14:21:02 +10:00
Kent Keirsey
3a687c583a lint 2025-07-17 14:21:02 +10:00
Kent Keirsey
833950078d commit tile size controls 2025-07-17 14:21:02 +10:00
Kent Keirsey
e698dcb148 unlint. 2025-07-17 14:21:02 +10:00
Kent Keirsey
218386e077 lint 2025-07-17 14:21:02 +10:00
Kent Keirsey
4426be9e64 commit tile size controls 2025-07-17 14:21:02 +10:00
Heathen711
89ceecc870 bugfix(docker) Ensure the correct extra install. 2025-07-17 04:19:22 +00:00
psychedelicious
86f4cf7857 feat(ui): related embedding styling/tidy 2025-07-17 14:12:29 +10:00
Kent Keirsey
49ae66d94a Added related model support 2025-07-17 14:12:29 +10:00
Cursor Agent
c10865c7ef Reorder embedding options in PromptTriggerSelect component
Co-authored-by: kent <kent@invoke.ai>
2025-07-17 14:12:29 +10:00
Heathen711
687cccdb99 cleanup(docker) 2025-07-17 04:00:42 +00:00
psychedelicious
f3478a189a fix(ui): able to drag empty space in tab bar and detach panels 2025-07-17 13:58:32 +10:00
Heathen711
c84f8465b8 bugfix(pyproject) Convert from dependency groups to extras and update docks to use UV's built in torch support 2025-07-17 03:58:26 +00:00
psychedelicious
43db29176a chore(ui): lint 2025-07-17 13:52:24 +10:00
psychedelicious
f38922929c docs(ui): comments in modelsLoaded 2025-07-17 13:52:24 +10:00
psychedelicious
7d02c58f86 fix(ui): move <ParamTileControlNetModel /> to <UpscaleTabAdvancedSettingsAccordion /> 2025-07-17 13:52:24 +10:00
Kent Keirsey
6edce8be87 Add scaling in 2025-07-17 13:52:24 +10:00
Kent Keirsey
31f63e38bd lint 2025-07-17 13:52:24 +10:00
Kent Keirsey
78a68ac3a7 Updated 2025-07-17 13:52:24 +10:00
Kent Keirsey
8cd3bcd1c0 Updates 2025-07-17 13:52:24 +10:00
Cursor Agent
264cc5ef46 Add tile ControlNet model selection to upscale settings
Co-authored-by: kent <kent@invoke.ai>
2025-07-17 13:52:24 +10:00
Heathen711
4b5c481b7a Merge remote-tracking branch 'origin' into bugfix/heathen711/rocm-docker 2025-07-17 01:03:03 +00:00
JPPhoto
8bfbea5ed3 Updated __init__.py 2025-07-17 06:33:56 +10:00
JPPhoto
f06a66da07 Updated schema.ts 2025-07-17 06:33:56 +10:00
Jonathan
337cae9b22 Update __init__.py
Added FluxConditioningField, FluxConditioningCollectionOutput, and FluxConditioningCollectionOutput,
2025-07-17 06:33:56 +10:00
Jonathan
bf926bb7d5 Update primitives.py
Added FluxConditioningCollectionOutput
2025-07-17 06:33:56 +10:00
psychedelicious
18ad9a6af3 feat(ui): canvas/viewer panel tabs show progress 2025-07-17 06:20:05 +10:00
psychedelicious
b6ed31c222 feat(ui): clicking invoke switches to viewer tab instead of canvas when save all images to gallery is enabled 2025-07-17 06:20:05 +10:00
psychedelicious
200beb5af5 feat(ui): make save all images to gallery option also bypass canvas 2025-07-17 06:20:05 +10:00
psychedelicious
f82a948bdd refactor(ui): canvas autoswitch logic
Simplify the canvas auto-switch logic to not rely on the preview images
loading. This fixes an issue where offscreen preview images didn't get
auto-switched to. Images are now loaded directly.
2025-07-17 06:20:05 +10:00
psychedelicious
dd03e3ddcd refactor(ui): simplify canvas session logic 2025-07-17 06:20:05 +10:00
psychedelicious
7561b73e8f fix(ui): uppercase file extensions blocked for image upload
Closes #8284
2025-07-17 00:48:36 +10:00
psychedelicious
caa97608c7 fix(ui): aspect ratios out of order 2025-07-16 23:27:37 +10:00
Mary Hipp
72a6d1edc1 simplify descriptoin styling 2025-07-16 09:19:33 -04:00
Mary Hipp
b8bf89c2f1 add fallback image and make sure description text is legible for model picker noncompact 2025-07-16 09:19:33 -04:00
psychedelicious
a1ade2b8c0 feat(ui): export apis & actions from package 2025-07-16 08:21:03 -04:00
Eugene Brodsky
4bdcae1f8f fix(docker): switch to pnpm10.x 2025-07-15 13:03:15 -04:00
Jonathan
4b22c84407 Update dev-environment.md
Document the latest changes required to build Invoke 6.0.
2025-07-15 15:21:01 +10:00
Eugene Brodsky
c9daf1db30 (fix) remove timeout from image prompt expansion (#8281) 2025-07-14 11:19:20 -04:00
psychedelicious
06d3cfbe97 gh: update bug report template
- Add require drop down for install method
- Make browser version optional
- Link to latest release
- Update verbiage for sys info section
2025-07-14 12:18:52 +10:00
psychedelicious
71e4901313 fix(ui): ignore disalbed ref images in readiness checks 2025-07-14 10:51:51 +10:00
Heathen711
2caa1b166d Merge remote-tracking branch 'origin' into bugfix/heathen711/rocm-docker 2025-07-13 00:55:39 +00:00
psychedelicious
82fb897b62 chore(ui): lint 2025-07-12 14:56:57 +10:00
psychedelicious
192b00d969 chore: bump version to v6.0.2 2025-07-12 14:56:57 +10:00
psychedelicious
7bb25ef1b4 fix(ui): gallery dnd 2025-07-12 14:56:57 +10:00
psychedelicious
62f52c74a8 fix(ui): linked negative style prompt not passed in
Closes #8256
2025-07-12 10:22:17 +10:00
psychedelicious
97439c1daa fix(ui): native context menu shown on right click on short fat images
Closes #8254
2025-07-12 10:22:17 +10:00
psychedelicious
b23bff1b53 fix(ui): center staging area images 2025-07-12 10:22:17 +10:00
psychedelicious
d9a1efbabf fix(ui): staging area images may be slightly too large 2025-07-12 10:22:17 +10:00
psychedelicious
d4e903ee2d chore: bump version to v6.0.1 2025-07-12 10:22:17 +10:00
Kevin Turner
bb3e5d16d8 feat(Model Manager): refuse to download a file when there's insufficient space 2025-07-12 10:14:25 +10:00
psychedelicious
e62d3f01a8 feat(app): better error message for failed model probe
- Old: No valid config found
- New: Unable to determine model type
2025-07-11 23:35:43 +10:00
psychedelicious
757ecdbf82 build(ui): downgrade idb-keyval
We have increased error rates after updating this package. Let's try
downgrading to see if that fixes the issue.
2025-07-11 15:00:10 +10:00
psychedelicious
694c85b041 fix(ui): language file filenames
Need to replace the underscores w/ dashes - this was missed in #8246.
2025-07-11 14:21:41 +10:00
psychedelicious
988d7ba24c chore: bump version to v6.0.1rc1 2025-07-11 09:05:24 +10:00
psychedelicious
ac981879ef fix(ui): runtime errors related to calling reduce on array iterator
Fix an issue in certain browsers/builds causing a runtime error.

A zod enum has a .options property, which is an array of all the options
for the enum. This is handy for when you need to derive something from a
zod schema.

In this case, we represented the possible focus regions in the zod enum,
then derived a mapping of region names to set of target HTML elements.
Why isn't important, but suffice to say, we were using the .options
property for this.

But actually, we were using .options.values(), then calling .reduce() on
that. An array's .values() method returns an _array iterator_. Array
iterators do not have .reduce() methods!

Except, apparently in some environments they do - it depends on the JS
engine and whether or not polyfills for iterator helpers were included
in the build.

Turns out my dev environment - and most user browsers - do provide
.reduce(), so we didn't catch this error. It took a large deployment and
error monitoring to catch it.

I've refactored the code to totally avoid deriving data from zod in this
way.
2025-07-11 08:25:47 +10:00
psychedelicious
fc71849c24 feat(app): expose a cursor, not a connection in db util 2025-07-11 08:20:06 +10:00
psychedelicious
a19aa3b032 feat(app): db abstraction to prevent threading conflicts
- Add a context manager to the SqliteDatabase class which abstracts away
creating a transaction, committing it on success and rolling back on
error.
- Use it everywhere. The context manager should be exited before
returning results. No business logic changes should be present.
2025-07-11 08:20:06 +10:00
Heathen711
1b6ebede7b Revert "cleanup(github actions)"
This reverts commit 017d38eee2.
2025-07-10 21:10:56 +00:00
Heathen711
017d38eee2 cleanup(github actions) 2025-07-10 21:04:48 +00:00
Heathen711
78eb6b0338 cleanup(docker) 2025-07-10 21:03:57 +00:00
Heathen711
3e8e0f6ddf Merge remote-tracking branch 'origin' into bugfix/heathen711/rocm-docker 2025-07-10 20:14:27 +00:00
Heathen711
8213f62d3b bugfix(docker) render group controls the devices, but it needs to match the host's render group ID 2025-07-09 20:20:59 +00:00
Heathen711
233740a40e Merge remote-tracking branch 'origin' into bugfix/heathen711/rocm-docker 2025-07-09 03:27:42 +00:00
Heathen711
8c5fcfd0fd cleanup(docker) remove no cache argument 2025-07-05 15:25:26 +00:00
Heathen711
6d7b231196 Merge remote-tracking branch 'origin' into bugfix/heathen711/rocm-docker 2025-07-05 15:22:35 +00:00
Heathen711
31ca314b02 Missed files 2025-07-05 15:21:46 +00:00
Heathen711
0db304f1ee bugfix(uv) Lock torchvision and ensure the docker uses the same rocm version 2025-07-05 03:35:11 +00:00
Heathen711
a3cb3e03f4 bugfix(ci) Clean up more space for typegen check 2025-07-03 21:22:11 +00:00
Heathen711
641a6cfdb7 bugfix(docker) Remove the need for UV index as that is now baked into the uv.lock 2025-07-03 21:15:03 +00:00
Heathen711
f27471cea7 bugfix(docker): Use uv.lock for docker, and update to newer index urls. 2025-07-03 20:08:28 +00:00
Heathen711
47508b8d6c bugfix(docker) combined the dockerfiles and reduced image size 2025-07-03 06:01:51 +00:00
Heathen711
28e0242907 Fix tagging & remove force reinstall 2025-07-03 01:56:46 +00:00
Heathen711
96523ca01f fix(docker) Add cloned dockerbuild 2025-06-29 22:07:11 +00:00
Heathen711
c10a6fdab1 fix(docker) rocm 2.4.6 based image 2025-06-29 22:02:40 +00:00
1700 changed files with 157429 additions and 29868 deletions

1
.gitattributes vendored
View File

@@ -4,3 +4,4 @@
* text=auto
docker/** text eol=lf
tests/test_model_probe/stripped_models/** filter=lfs diff=lfs merge=lfs -text
tests/model_identification/stripped_models/** filter=lfs diff=lfs merge=lfs -text

39
.github/CODEOWNERS vendored
View File

@@ -1,31 +1,32 @@
# continuous integration
/.github/workflows/ @lstein @blessedcoolant @hipsterusername @ebr @jazzhaiku @psychedelicious
/.github/workflows/ @lstein @blessedcoolant
# documentation
/docs/ @lstein @blessedcoolant @hipsterusername @psychedelicious
/mkdocs.yml @lstein @blessedcoolant @hipsterusername @psychedelicious
# documentation - anyone with write privileges can review
/docs/
/mkdocs.yml
# nodes
/invokeai/app/ @blessedcoolant @psychedelicious @hipsterusername @jazzhaiku
/invokeai/app/ @blessedcoolant @lstein @dunkeroni @JPPhoto
# installation and configuration
/pyproject.toml @lstein @blessedcoolant @psychedelicious @hipsterusername
/docker/ @lstein @blessedcoolant @psychedelicious @hipsterusername @ebr
/scripts/ @ebr @lstein @psychedelicious @hipsterusername
/installer/ @lstein @ebr @psychedelicious @hipsterusername
/invokeai/assets @lstein @ebr @psychedelicious @hipsterusername
/invokeai/configs @lstein @psychedelicious @hipsterusername
/invokeai/version @lstein @blessedcoolant @psychedelicious @hipsterusername
/pyproject.toml @lstein @blessedcoolant
/docker/ @lstein @blessedcoolant
/scripts/ @lstein @blessedcoolant
/installer/ @lstein @blessedcoolant
/invokeai/assets @lstein @blessedcoolant
/invokeai/configs @lstein @blessedcoolant
/invokeai/version @lstein @blessedcoolant
# web ui
/invokeai/frontend @blessedcoolant @psychedelicious @lstein @maryhipp @hipsterusername
/invokeai/frontend @blessedcoolant @lstein @dunkeroni
# generation, model management, postprocessing
/invokeai/backend @lstein @blessedcoolant @hipsterusername @jazzhaiku @psychedelicious @maryhipp
/invokeai/backend @lstein @blessedcoolant @dunkeroni @JPPhoto @Pfannkuchensack
# front ends
/invokeai/frontend/CLI @lstein @psychedelicious @hipsterusername
/invokeai/frontend/install @lstein @ebr @psychedelicious @hipsterusername
/invokeai/frontend/merge @lstein @blessedcoolant @psychedelicious @hipsterusername
/invokeai/frontend/training @lstein @blessedcoolant @psychedelicious @hipsterusername
/invokeai/frontend/web @psychedelicious @blessedcoolant @maryhipp @hipsterusername
/invokeai/frontend/CLI @lstein
/invokeai/frontend/install @lstein
/invokeai/frontend/merge @lstein @blessedcoolant
/invokeai/frontend/training @lstein @blessedcoolant
/invokeai/frontend/web @blessedcoolant @lstein @dunkeroni @Pfannkuchensack

View File

@@ -21,6 +21,20 @@ body:
- label: I have searched the existing issues
required: true
- type: dropdown
id: install_method
attributes:
label: Install method
description: How did you install Invoke?
multiple: false
options:
- "Invoke's Launcher"
- 'Stability Matrix'
- 'Pinokio'
- 'Manual'
validations:
required: true
- type: markdown
attributes:
value: __Describe your environment__
@@ -76,8 +90,8 @@ body:
attributes:
label: Version number
description: |
The version of Invoke you have installed. If it is not the latest version, please update and try again to confirm the issue still exists. If you are testing main, please include the commit hash instead.
placeholder: ex. 3.6.1
The version of Invoke you have installed. If it is not the [latest version](https://github.com/invoke-ai/InvokeAI/releases/latest), please update and try again to confirm the issue still exists. If you are testing main, please include the commit hash instead.
placeholder: ex. v6.0.2
validations:
required: true
@@ -85,17 +99,17 @@ body:
id: browser-version
attributes:
label: Browser
description: Your web browser and version.
description: Your web browser and version, if you do not use the Launcher's provided GUI.
placeholder: ex. Firefox 123.0b3
validations:
required: true
required: false
- type: textarea
id: python-deps
attributes:
label: Python dependencies
label: System Information
description: |
If the problem occurred during image generation, click the gear icon at the bottom left corner, click "About", click the copy button and then paste here.
Click the gear icon at the bottom left corner, then click "About". Click the copy button and then paste here.
validations:
required: false

View File

@@ -18,5 +18,6 @@
- [ ] _The PR has a short but descriptive title, suitable for a changelog_
- [ ] _Tests added / updated (if applicable)_
- [ ] _❗Changes to a redux slice have a corresponding migration_
- [ ] _Documentation added / updated (if applicable)_
- [ ] _Updated `What's New` copy (if doing a release after this PR)_

View File

@@ -45,13 +45,23 @@ jobs:
steps:
- name: Free up more disk space on the runner
# https://github.com/actions/runner-images/issues/2840#issuecomment-1284059930
# the /mnt dir has 70GBs of free space
# /dev/sda1 74G 28K 70G 1% /mnt
# According to some online posts the /mnt is not always there, so checking before setting docker to use it
run: |
echo "----- Free space before cleanup"
df -h
sudo rm -rf /usr/share/dotnet
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
sudo swapoff /mnt/swapfile
sudo rm -rf /mnt/swapfile
if [ -f /mnt/swapfile ]; then
sudo swapoff /mnt/swapfile
sudo rm -rf /mnt/swapfile
fi
if [ -d /mnt ]; then
sudo chmod -R 777 /mnt
echo '{"data-root": "/mnt/docker-root"}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
fi
echo "----- Free space after cleanup"
df -h

View File

@@ -23,6 +23,7 @@ jobs:
close-issue-message: "Due to inactivity, this issue was automatically closed. If you are still experiencing the issue, please recreate the issue."
days-before-pr-stale: -1
days-before-pr-close: -1
only-labels: "bug"
exempt-issue-labels: "Active Issue"
repo-token: ${{ secrets.GITHUB_TOKEN }}
operations-per-run: 500

30
.github/workflows/lfs-checks.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
# Checks that large files and LFS-tracked files are properly checked in with pointer format.
# Uses https://github.com/ppremk/lfs-warning to detect LFS issues.
name: 'lfs checks'
on:
push:
branches:
- 'main'
pull_request:
types:
- 'ready_for_review'
- 'opened'
- 'synchronize'
merge_group:
workflow_dispatch:
jobs:
lfs-check:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
# Required to label and comment on the PRs
pull-requests: write
steps:
- name: checkout
uses: actions/checkout@v4
- name: check lfs files
uses: ppremk/lfs-warning@v3.3

View File

@@ -22,12 +22,12 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: setup python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.10'
python-version: '3.12'
cache: pip
cache-dependency-path: pyproject.toml

View File

@@ -39,6 +39,20 @@ jobs:
- name: checkout
uses: actions/checkout@v4
- name: Free up more disk space on the runner
# https://github.com/actions/runner-images/issues/2840#issuecomment-1284059930
run: |
echo "----- Free space before cleanup"
df -h
sudo rm -rf /usr/share/dotnet
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
if [ -f /mnt/swapfile ]; then
sudo swapoff /mnt/swapfile
sudo rm -rf /mnt/swapfile
fi
echo "----- Free space after cleanup"
df -h
- name: check for changed files
if: ${{ inputs.always_run != true }}
id: changed-files

5
.gitignore vendored
View File

@@ -190,3 +190,8 @@ installer/update.bat
installer/update.sh
installer/InvokeAI-Installer/
.aider*
.claude/
# Weblate configuration file
weblate.ini

View File

@@ -16,20 +16,20 @@ help:
@echo "frontend-build Build the frontend in order to run on localhost:9090"
@echo "frontend-dev Run the frontend in developer mode on localhost:5173"
@echo "frontend-typegen Generate types for the frontend from the OpenAPI schema"
@echo "wheel Build the wheel for the current version"
@echo "frontend-prettier Format the frontend using lint:prettier"
@echo "wheel Build the wheel for the current version"
@echo "tag-release Tag the GitHub repository with the current version (use at release time only!)"
@echo "openapi Generate the OpenAPI schema for the app, outputting to stdout"
@echo "docs Serve the mkdocs site with live reload"
# Runs ruff, fixing any safely-fixable errors and formatting
ruff:
ruff check . --fix
ruff format .
cd invokeai && uv tool run ruff@0.11.2 format
# Runs ruff, fixing all errors it can fix and formatting
ruff-unsafe:
ruff check . --fix --unsafe-fixes
ruff format .
ruff format
# Runs mypy, using the config in pyproject.toml
mypy:
@@ -64,6 +64,13 @@ frontend-dev:
frontend-typegen:
cd invokeai/frontend/web && python ../../../scripts/generate_openapi_schema.py | pnpm typegen
frontend-lint:
cd invokeai/frontend/web/src && \
pnpm lint:tsc && \
pnpm lint:dpdm && \
pnpm lint:eslint --fix && \
pnpm lint:prettier --write
# Tag the release
wheel:
cd scripts && ./build_wheel.sh
@@ -79,4 +86,4 @@ openapi:
# Serve the mkdocs site w/ live reload
.PHONY: docs
docs:
mkdocs serve
mkdocs serve

View File

@@ -4,38 +4,33 @@
# Invoke - Professional Creative AI Tools for Visual Media
#### To learn more about Invoke, or implement our Business solutions, visit [invoke.com]
[![discord badge]][discord link] [![latest release badge]][latest release link] [![github stars badge]][github stars link] [![github forks badge]][github forks link] [![CI checks on main badge]][CI checks on main link] [![latest commit to main badge]][latest commit to main link] [![github open issues badge]][github open issues link] [![github open prs badge]][github open prs link] [![translation status badge]][translation status link]
</div>
Invoke is a leading creative engine built to empower professionals and enthusiasts alike. Generate and create stunning visual media using the latest AI-driven technologies. Invoke offers an industry leading web-based UI, and serves as the foundation for multiple commercial products.
Invoke is available in two editions:
| **Community Edition** | **Professional Edition** |
|----------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
| **For users looking for a locally installed, self-hosted and self-managed service** | **For users or teams looking for a cloud-hosted, fully managed service** |
| - Free to use under a commercially-friendly license | - Monthly subscription fee with three different plan levels |
| - Download and install on compatible hardware | - Offers additional benefits, including multi-user support, improved model training, and more |
| - Includes all core studio features: generate, refine, iterate on images, and build workflows | - Hosted in the cloud for easy, secure model access and scalability |
| Quick Start -> [Installation and Updates][installation docs] | More Information -> [www.invoke.com/pricing](https://www.invoke.com/pricing) |
- Free to use under a commercially-friendly license
- Download and install on compatible hardware
- Generate, refine, iterate on images, and build workflows
![Highlighted Features - Canvas and Workflows](https://github.com/invoke-ai/InvokeAI/assets/31807370/708f7a82-084f-4860-bfbe-e2588c53548d)
---
> ## 📣 Are you a new or returning InvokeAI user?
> Take our first annual [User's Survey](https://forms.gle/rCE5KuQ7Wfrd1UnS7)
---
# Documentation
| **Quick Links** |
|----------------------------------------------------------------------------------------------------------------------------|
| [Installation and Updates][installation docs] - [Documentation and Tutorials][docs home] - [Bug Reports][github issues] - [Contributing][contributing docs] |
| **Quick Links** |
| ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Installation and Updates][installation docs] - [Documentation and Tutorials][docs home] - [Bug Reports][github issues] - [Contributing][contributing docs] |
# Installation
To get started with Invoke, [Download the Installer](https://www.invoke.com/downloads).
For detailed step by step instructions, or for instructions on manual/docker installations, visit our documentation on [Installation and Updates][installation docs]
To get started with Invoke, [Download the Launcher](https://github.com/invoke-ai/launcher/releases/latest).
## Troubleshooting, FAQ and Support
@@ -57,7 +52,7 @@ The Unified Canvas is a fully integrated canvas implementation with support for
### Workflows & Nodes
Invoke offers a fully featured workflow management solution, enabling users to combine the power of node-based workflows with the easy of a UI. This allows for customizable generation pipelines to be developed and shared by users looking to create specific workflows to support their production use-cases.
Invoke offers a fully featured workflow management solution, enabling users to combine the power of node-based workflows with the ease of a UI. This allows for customizable generation pipelines to be developed and shared by users looking to create specific workflows to support their production use-cases.
### Board & Gallery Management
@@ -90,7 +85,6 @@ Original portions of the software are Copyright © 2024 by respective contributo
[features docs]: https://invoke-ai.github.io/InvokeAI/features/database/
[faq]: https://invoke-ai.github.io/InvokeAI/faq/
[contributors]: https://invoke-ai.github.io/InvokeAI/contributing/contributors/
[invoke.com]: https://www.invoke.com/about
[github issues]: https://github.com/invoke-ai/InvokeAI/issues
[docs home]: https://invoke-ai.github.io/InvokeAI
[installation docs]: https://invoke-ai.github.io/InvokeAI/installation/

View File

@@ -0,0 +1,169 @@
# User Isolation Implementation Summary
This document describes the implementation of user isolation features in the InvokeAI session queue and processing system to address issues identified in the enhancement request.
## Issues Addressed
### 1. Cross-User Image/Preview Visibility
**Problem:** When two users are logged in simultaneously and one initiates a generation, the generation preview shows up in both users' browsers and the generated image gets saved to both users' image boards.
**Solution:** Implemented socket-level event filtering based on user authentication:
#### Backend Changes (`invokeai/app/api/sockets.py`):
- Added socket authentication middleware in `_handle_connect()` method
- Extracts JWT token from socket auth data or HTTP headers
- Verifies token using existing `verify_token()` function
- Stores `user_id` and `is_admin` in socket session for later use
- Modified `_handle_queue_event()` to filter events by user:
- For `QueueItemEventBase` events, only emit to:
- The user who owns the queue item (`user_id` matches)
- Admin users (`is_admin` is True)
- For general queue events, emit to all subscribers
#### Event System Changes (`invokeai/app/services/events/events_common.py`):
- Added `user_id` field to `QueueItemEventBase` class
- Updated all event builders to include `user_id` from queue items:
- `InvocationStartedEvent.build()`
- `InvocationProgressEvent.build()`
- `InvocationCompleteEvent.build()`
- `InvocationErrorEvent.build()`
- `QueueItemStatusChangedEvent.build()`
### 2. Batch Field Values Privacy
**Problem:** Users can see batch field values from generation processes launched by other users.
**Solution:** Implemented field value sanitization at the API level:
#### API Router Changes (`invokeai/app/api/routers/session_queue.py`):
- Created `sanitize_queue_item_for_user()` helper function
- Clears `field_values` for non-admin users viewing other users' items
- Admins and item owners can see all field values
- Updated endpoints to require authentication and sanitize responses:
- `list_all_queue_items()` - Added `CurrentUser` dependency
- `get_queue_items_by_item_ids()` - Added `CurrentUser` dependency
- `get_queue_item()` - Added `CurrentUser` dependency
### 3. Queue Updates Across Browser Windows
**Problem:** When the job queue tab is open in multiple browsers and a generation is begun in one browser window, the queue does not update in the other window.
**Status:** This issue is likely resolved by the socket authentication and event filtering changes. The existing socket subscription mechanism (`subscribe_queue` event) already supports multiple connections per user. Testing is required to confirm this works correctly with the new authentication flow.
### 4. User Information Display
**Problem:** Queue table lacks user identification, making it difficult to know who launched which job.
**Solution:** Added user information to queue items and UI:
#### Database Layer (`invokeai/app/services/session_queue/session_queue_sqlite.py`):
- Updated SQL queries to JOIN with `users` table
- Modified methods to fetch user information:
- `get_queue_item()` - Now selects `display_name` and `email` from users table
- `dequeue()` - Includes user info
- `get_next()` - Includes user info
- `get_current()` - Includes user info
- `list_all_queue_items()` - Includes user info
#### Data Model Changes (`invokeai/app/services/session_queue/session_queue_common.py`):
- Added optional fields to `SessionQueueItem`:
- `user_display_name: Optional[str]` - Display name from users table
- `user_email: Optional[str]` - Email from users table
- Note: `user_id` field already existed from Migration 25
#### Frontend UI Changes:
- **Constants** (`constants.ts`): Added `user: '8rem'` column width
- **Header** (`QueueListHeader.tsx`): Added "User" column header
- **Item Component** (`QueueItemComponent.tsx`):
- Added logic to display user information (display_name → email → user_id)
- Added user column to queue item row
- Added tooltip with full username on hover
- Added "Hidden for privacy" message when field_values are null for non-owned items
- **Localization** (`en.json`): Added translations:
- `"user": "User"`
- `"fieldValuesHidden": "Hidden for privacy"`
## Security Considerations
### Token Verification
- Tokens are verified using the existing `verify_token()` function from `invokeai.app.services.auth.token_service`
- Invalid or missing tokens default to "system" user with non-admin privileges
- Socket connections without valid tokens are still accepted for backward compatibility but have limited access
### Data Privacy
- Field values are only visible to:
- The user who created the queue item
- Admin users
- Non-admin users viewing other users' queue items see "Hidden for privacy" instead of field values
### Admin Privileges
- Admin users can see all queue events and field values across all users
- Admin status is determined from the JWT token's `is_admin` field
## Migration Notes
No database migration is required. The changes leverage:
- Existing `user_id` column in `session_queue` table (added in Migration 25)
- Existing `users` table (added in Migration 25)
- SQL LEFT JOINs to fetch user information (gracefully handles missing user records)
## Testing Requirements
### Backend Testing
1. **Socket Authentication:**
- Verify valid tokens are accepted and user context is stored
- Verify invalid tokens default to system user
- Verify expired tokens are rejected
2. **Event Filtering:**
- User A should only receive events for their own queue items
- Admin users should receive all events
- Non-admin users should not receive events from other users
3. **Field Value Sanitization:**
- Non-admin users should see null field_values for other users' items
- Admins should see all field values
- Users should see their own field values
### Frontend Testing
1. **UI Display:**
- User column should display in queue list
- Display name should be shown when available
- Email should be shown as fallback when display name is missing
- User ID should be shown when both display name and email are missing
- Tooltip should show full username on hover
2. **Field Values Display:**
- "Hidden for privacy" message should appear when viewing other users' items
- Own items should show field values normally
3. **Multi-Browser Testing:**
- Open queue tab in two browsers with different users
- Start generation in one browser
- Verify other browser doesn't see the preview/progress
- Verify admin user can see all generations
### Integration Testing
1. Multi-user scenarios with simultaneous generations
2. Queue updates across multiple browser windows
3. Admin vs. non-admin privilege differentiation
4. Socket reconnection handling
## Known Limitations
1. **TypeScript Types:**
- The OpenAPI schema needs to be regenerated to include new fields
- Run: `cd invokeai/frontend/web && python ../../../scripts/generate_openapi_schema.py | pnpm typegen`
2. **Backward Compatibility:**
- System user ("system") entries will not have display name or email
- Existing queue items from before Migration 25 will have user_id="system"
3. **Socket.IO Session Storage:**
- Socket.IO's in-memory session storage may not persist across server restarts
- Consider implementing persistent session storage if needed for production
## Future Enhancements
1. Add user filtering to queue list (show only my items vs. all items)
2. Add permission system for queue management operations (cancel, retry, delete)
3. Implement queue item ownership transfer for administrative purposes
4. Add audit logging for queue operations with user attribution
5. Consider implementing user-specific queue limits or quotas

View File

@@ -22,6 +22,10 @@
## GPU_DRIVER can be set to either `cuda` or `rocm` to enable GPU support in the container accordingly.
# GPU_DRIVER=cuda #| rocm
## If you are using ROCM, you will need to ensure that the render group within the container and the host system use the same group ID.
## To obtain the group ID of the render group on the host system, run `getent group render` and grab the number.
# RENDER_GROUP_ID=
## CONTAINER_UID can be set to the UID of the user on the host system that should own the files in the container.
## It is usually not necessary to change this. Use `id -u` on the host system to find the UID.
# CONTAINER_UID=1000

View File

@@ -5,8 +5,7 @@
FROM docker.io/node:22-slim AS web-builder
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack use pnpm@8.x
RUN corepack enable
RUN corepack use pnpm@10.x && corepack enable
WORKDIR /build
COPY invokeai/frontend/web/ ./
@@ -44,7 +43,6 @@ ENV \
UV_MANAGED_PYTHON=1 \
UV_LINK_MODE=copy \
UV_PROJECT_ENVIRONMENT=/opt/venv \
UV_INDEX="https://download.pytorch.org/whl/cu124" \
INVOKEAI_ROOT=/invokeai \
INVOKEAI_HOST=0.0.0.0 \
INVOKEAI_PORT=9090 \
@@ -75,19 +73,17 @@ RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
# this is just to get the package manager to recognize that the project exists, without making changes to the docker layer
--mount=type=bind,source=invokeai/version,target=invokeai/version \
if [ "$TARGETPLATFORM" = "linux/arm64" ] || [ "$GPU_DRIVER" = "cpu" ]; then UV_INDEX="https://download.pytorch.org/whl/cpu"; \
elif [ "$GPU_DRIVER" = "rocm" ]; then UV_INDEX="https://download.pytorch.org/whl/rocm6.2"; \
fi && \
uv sync --frozen
# build patchmatch
RUN cd /usr/lib/$(uname -p)-linux-gnu/pkgconfig/ && ln -sf opencv4.pc opencv.pc
RUN python -c "from patchmatch import patch_match"
ulimit -n 30000 && \
uv sync --extra $GPU_DRIVER --frozen
# Link amdgpu.ids for ROCm builds
# contributed by https://github.com/Rubonnek
RUN mkdir -p "/opt/amdgpu/share/libdrm" &&\
ln -s "/usr/share/libdrm/amdgpu.ids" "/opt/amdgpu/share/libdrm/amdgpu.ids"
ln -s "/usr/share/libdrm/amdgpu.ids" "/opt/amdgpu/share/libdrm/amdgpu.ids" && groupadd render
# build patchmatch
RUN cd /usr/lib/$(uname -p)-linux-gnu/pkgconfig/ && ln -sf opencv4.pc opencv.pc
RUN python -c "from patchmatch import patch_match"
RUN mkdir -p ${INVOKEAI_ROOT} && chown -R ${CONTAINER_UID}:${CONTAINER_GID} ${INVOKEAI_ROOT}
@@ -106,8 +102,6 @@ COPY invokeai ${INVOKEAI_SRC}/invokeai
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
--mount=type=bind,source=uv.lock,target=uv.lock \
if [ "$TARGETPLATFORM" = "linux/arm64" ] || [ "$GPU_DRIVER" = "cpu" ]; then UV_INDEX="https://download.pytorch.org/whl/cpu"; \
elif [ "$GPU_DRIVER" = "rocm" ]; then UV_INDEX="https://download.pytorch.org/whl/rocm6.2"; \
fi && \
uv pip install -e .
ulimit -n 30000 && \
uv pip install -e .[$GPU_DRIVER]

136
docker/Dockerfile-rocm-full Normal file
View File

@@ -0,0 +1,136 @@
# syntax=docker/dockerfile:1.4
#### Web UI ------------------------------------
FROM docker.io/node:22-slim AS web-builder
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack use pnpm@8.x
RUN corepack enable
WORKDIR /build
COPY invokeai/frontend/web/ ./
RUN --mount=type=cache,target=/pnpm/store \
pnpm install --frozen-lockfile
RUN npx vite build
## Backend ---------------------------------------
FROM library/ubuntu:24.04
ARG DEBIAN_FRONTEND=noninteractive
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
RUN --mount=type=cache,target=/var/cache/apt \
--mount=type=cache,target=/var/lib/apt \
apt update && apt install -y --no-install-recommends \
ca-certificates \
git \
gosu \
libglib2.0-0 \
libgl1 \
libglx-mesa0 \
build-essential \
libopencv-dev \
libstdc++-10-dev \
wget
ENV \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
VIRTUAL_ENV=/opt/venv \
INVOKEAI_SRC=/opt/invokeai \
PYTHON_VERSION=3.12 \
UV_PYTHON=3.12 \
UV_COMPILE_BYTECODE=1 \
UV_MANAGED_PYTHON=1 \
UV_LINK_MODE=copy \
UV_PROJECT_ENVIRONMENT=/opt/venv \
INVOKEAI_ROOT=/invokeai \
INVOKEAI_HOST=0.0.0.0 \
INVOKEAI_PORT=9090 \
PATH="/opt/venv/bin:$PATH" \
CONTAINER_UID=${CONTAINER_UID:-1000} \
CONTAINER_GID=${CONTAINER_GID:-1000}
ARG GPU_DRIVER=cuda
# Install `uv` for package management
COPY --from=ghcr.io/astral-sh/uv:0.6.9 /uv /uvx /bin/
# Install python & allow non-root user to use it by traversing the /root dir without read permissions
RUN --mount=type=cache,target=/root/.cache/uv \
uv python install ${PYTHON_VERSION} && \
# chmod --recursive a+rX /root/.local/share/uv/python
chmod 711 /root
WORKDIR ${INVOKEAI_SRC}
# Install project's dependencies as a separate layer so they aren't rebuilt every commit.
# bind-mount instead of copy to defer adding sources to the image until next layer.
#
# NOTE: there are no pytorch builds for arm64 + cuda, only cpu
# x86_64/CUDA is the default
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
--mount=type=bind,source=uv.lock,target=uv.lock \
# this is just to get the package manager to recognize that the project exists, without making changes to the docker layer
--mount=type=bind,source=invokeai/version,target=invokeai/version \
ulimit -n 30000 && \
uv sync --extra $GPU_DRIVER --frozen
RUN --mount=type=cache,target=/var/cache/apt \
--mount=type=cache,target=/var/lib/apt \
if [ "$GPU_DRIVER" = "rocm" ]; then \
wget -O /tmp/amdgpu-install.deb \
https://repo.radeon.com/amdgpu-install/6.3.4/ubuntu/noble/amdgpu-install_6.3.60304-1_all.deb && \
apt install -y /tmp/amdgpu-install.deb && \
apt update && \
amdgpu-install --usecase=rocm -y && \
apt-get autoclean && \
apt clean && \
rm -rf /tmp/* /var/tmp/* && \
usermod -a -G render ubuntu && \
usermod -a -G video ubuntu && \
echo "\\n/opt/rocm/lib\\n/opt/rocm/lib64" >> /etc/ld.so.conf.d/rocm.conf && \
ldconfig && \
update-alternatives --auto rocm; \
fi
## Heathen711: Leaving this for review input, will remove before merge
# RUN --mount=type=cache,target=/var/cache/apt \
# --mount=type=cache,target=/var/lib/apt \
# if [ "$GPU_DRIVER" = "rocm" ]; then \
# groupadd render && \
# usermod -a -G render ubuntu && \
# usermod -a -G video ubuntu; \
# fi
## Link amdgpu.ids for ROCm builds
## contributed by https://github.com/Rubonnek
# RUN mkdir -p "/opt/amdgpu/share/libdrm" &&\
# ln -s "/usr/share/libdrm/amdgpu.ids" "/opt/amdgpu/share/libdrm/amdgpu.ids"
# build patchmatch
RUN cd /usr/lib/$(uname -p)-linux-gnu/pkgconfig/ && ln -sf opencv4.pc opencv.pc
RUN python -c "from patchmatch import patch_match"
RUN mkdir -p ${INVOKEAI_ROOT} && chown -R ${CONTAINER_UID}:${CONTAINER_GID} ${INVOKEAI_ROOT}
COPY docker/docker-entrypoint.sh ./
ENTRYPOINT ["/opt/invokeai/docker-entrypoint.sh"]
CMD ["invokeai-web"]
# --link requires buldkit w/ dockerfile syntax 1.4, does not work with podman
COPY --link --from=web-builder /build/dist ${INVOKEAI_SRC}/invokeai/frontend/web/dist
# add sources last to minimize image changes on code changes
COPY invokeai ${INVOKEAI_SRC}/invokeai
# this should not increase image size because we've already installed dependencies
# in a previous layer
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
--mount=type=bind,source=uv.lock,target=uv.lock \
ulimit -n 30000 && \
uv pip install -e .[$GPU_DRIVER]

View File

@@ -47,8 +47,9 @@ services:
invokeai-rocm:
<<: *invokeai
devices:
- /dev/kfd:/dev/kfd
- /dev/dri:/dev/dri
environment:
- AMD_VISIBLE_DEVICES=all
- RENDER_GROUP_ID=${RENDER_GROUP_ID}
runtime: amd
profiles:
- rocm

View File

@@ -21,6 +21,17 @@ _=$(id ${USER} 2>&1) || useradd -u ${USER_ID} ${USER}
# ensure the UID is correct
usermod -u ${USER_ID} ${USER} 1>/dev/null
## ROCM specific configuration
# render group within the container must match the host render group
# otherwise the container will not be able to access the host GPU.
if [[ -v "RENDER_GROUP_ID" ]] && [[ ! -z "${RENDER_GROUP_ID}" ]]; then
# ensure the render group exists
groupmod -g ${RENDER_GROUP_ID} render
usermod -a -G render ${USER}
usermod -a -G video ${USER}
fi
### Set the $PUBLIC_KEY env var to enable SSH access.
# We do not install openssh-server in the image by default to avoid bloat.
# but it is useful to have the full SSH server e.g. on Runpod.

View File

@@ -13,7 +13,7 @@ run() {
# parse .env file for build args
build_args=$(awk '$1 ~ /=[^$]/ && $0 !~ /^#/ {print "--build-arg " $0 " "}' .env) &&
profile="$(awk -F '=' '/GPU_DRIVER/ {print $2}' .env)"
profile="$(awk -F '=' '/GPU_DRIVER=/ {print $2}' .env)"
# default to 'cuda' profile
[[ -z "$profile" ]] && profile="cuda"
@@ -30,7 +30,7 @@ run() {
printf "%s\n" "starting service $service_name"
docker compose --profile "$profile" up -d "$service_name"
docker compose logs -f
docker compose --profile "$profile" logs -f
}
run

View File

@@ -16,7 +16,9 @@ The launcher uses GitHub as the source of truth for available releases.
## General Prep
Make a developer call-out for PRs to merge. Merge and test things out. Bump the version by editing `invokeai/version/invokeai_version.py`.
Make a developer call-out for PRs to merge. Merge and test things
out. Create a branch with a name like user/chore/vX.X.X-prep and bump the version by editing
`invokeai/version/invokeai_version.py` and commit locally.
## Release Workflow
@@ -26,14 +28,14 @@ It is triggered on **tag push**, when the tag matches `v*`.
### Triggering the Workflow
Ensure all commits that should be in the release are merged, and you have pulled them locally.
Double-check that you have checked out the commit that will represent the release (typically the latest commit on `main`).
Ensure all commits that should be in the release are merged into this branch, and that you have pulled them locally.
Run `make tag-release` to tag the current commit and kick off the workflow. You will be prompted to provide a message - use the version specifier.
If this version's tag already exists for some reason (maybe you had to make a last minute change), the script will overwrite it.
Push the commit to trigger the workflow.
> In case you cannot use the Make target, the release may also be dispatched [manually] via GH.
### Workflow Jobs and Process
@@ -89,7 +91,7 @@ The publish jobs will not run if any of the previous jobs fail.
They use [GitHub environments], which are configured as [trusted publishers] on PyPI.
Both jobs require a @hipsterusername or @psychedelicious to approve them from the workflow's **Summary** tab.
Both jobs require a @lstein or @blessedcoolant to approve them from the workflow's **Summary** tab.
- Click the **Review deployments** button
- Select the environment (either `testpypi` or `pypi` - typically you select both)
@@ -101,7 +103,7 @@ Both jobs require a @hipsterusername or @psychedelicious to approve them from th
Check the [python infrastructure status page] for incidents.
If there are no incidents, contact @hipsterusername or @lstein, who have owner access to GH and PyPI, to see if access has expired or something like that.
If there are no incidents, contact @lstein or @blessedcoolant, who have owner access to GH and PyPI, to see if access has expired or something like that.
#### `publish-testpypi` Job

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,295 @@
# Hotkeys System
This document describes the technical implementation of the customizable hotkeys system in InvokeAI.
> **Note:** For user-facing documentation on how to use customizable hotkeys, see [Hotkeys Feature Documentation](../features/hotkeys.md).
## Overview
The hotkeys system allows users to customize keyboard shortcuts throughout the application. All hotkeys are:
- Centrally defined and managed
- Customizable by users
- Persisted across sessions
- Type-safe and validated
## Architecture
The customizable hotkeys feature is built on top of the existing hotkey system with the following components:
### 1. Hotkeys State Slice (`hotkeysSlice.ts`)
Location: `invokeai/frontend/web/src/features/system/store/hotkeysSlice.ts`
**Responsibilities:**
- Stores custom hotkey mappings in Redux state
- Persisted to IndexedDB using `redux-remember`
- Provides actions to change, reset individual, or reset all hotkeys
**State Shape:**
```typescript
{
_version: 1,
customHotkeys: {
'app.invoke': ['mod+enter'],
'canvas.undo': ['mod+z'],
// ...
}
}
```
**Actions:**
- `hotkeyChanged(id, hotkeys)` - Update a single hotkey
- `hotkeyReset(id)` - Reset a single hotkey to default
- `allHotkeysReset()` - Reset all hotkeys to defaults
### 2. useHotkeyData Hook (`useHotkeyData.ts`)
Location: `invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts`
**Responsibilities:**
- Defines all default hotkeys
- Merges default hotkeys with custom hotkeys from the store
- Returns the effective hotkeys that should be used throughout the app
- Provides platform-specific key translations (Ctrl/Cmd, Alt/Option)
**Key Functions:**
- `useHotkeyData()` - Returns all hotkeys organized by category
- `useRegisteredHotkeys()` - Hook to register a hotkey in a component
### 3. HotkeyEditor Component (`HotkeyEditor.tsx`)
Location: `invokeai/frontend/web/src/features/system/components/HotkeysModal/HotkeyEditor.tsx`
**Features:**
- Inline editor with input field
- Modifier buttons (Mod, Ctrl, Shift, Alt) for quick insertion
- Live preview of hotkey combinations
- Validation with visual feedback
- Help tooltip with syntax examples
- Save/cancel/reset buttons
**Smart Features:**
- Automatic `+` insertion between modifiers
- Cursor position preservation
- Validation prevents invalid combinations (e.g., modifier-only keys)
### 4. HotkeysModal Component (`HotkeysModal.tsx`)
Location: `invokeai/frontend/web/src/features/system/components/HotkeysModal/HotkeysModal.tsx`
**Features:**
- View Mode / Edit Mode toggle
- Search functionality
- Category-based organization
- Shows HotkeyEditor components when in edit mode
- "Reset All to Default" button in edit mode
## Data Flow
```
┌─────────────────────────────────────────────────────────────┐
│ 1. User opens Hotkeys Modal │
│ 2. User clicks "Edit Mode" button │
│ 3. User clicks edit icon next to a hotkey │
│ 4. User enters new hotkey(s) using editor │
│ 5. User clicks save or presses Enter │
│ 6. Custom hotkey stored via hotkeyChanged() action │
│ 7. Redux state persisted to IndexedDB (redux-remember) │
│ 8. useHotkeyData() hook picks up the change │
│ 9. All components using useRegisteredHotkeys() get update │
└─────────────────────────────────────────────────────────────┘
```
## Hotkey Format
Hotkeys use the format from `react-hotkeys-hook` library:
- **Modifiers:** `mod`, `ctrl`, `shift`, `alt`, `meta`
- **Keys:** Letters, numbers, function keys, special keys
- **Separator:** `+` between keys in a combination
- **Multiple hotkeys:** Comma-separated (e.g., `mod+a, ctrl+b`)
**Examples:**
- `mod+enter` - Mod key + Enter
- `shift+x` - Shift + X
- `ctrl+shift+a` - Control + Shift + A
- `f1, f2` - F1 or F2 (alternatives)
## Developer Guide
### Using Hotkeys in Components
To use a hotkey in a component:
```tsx
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
const MyComponent = () => {
const handleAction = useCallback(() => {
// Your action here
}, []);
// This automatically uses custom hotkeys if configured
useRegisteredHotkeys({
id: 'myAction',
category: 'app', // or 'canvas', 'viewer', 'gallery', 'workflows'
callback: handleAction,
options: { enabled: true, preventDefault: true },
dependencies: [handleAction]
});
// ...
};
```
**Options:**
- `enabled` - Whether the hotkey is active
- `preventDefault` - Prevent default browser behavior
- `enableOnFormTags` - Allow hotkey in form elements (default: false)
### Adding New Hotkeys
To add a new hotkey to the system:
#### 1. Add Translation Strings
In `invokeai/frontend/web/public/locales/en.json`:
```json
{
"hotkeys": {
"app": {
"myAction": {
"title": "My Action",
"desc": "Description of what this hotkey does"
}
}
}
}
```
#### 2. Register the Hotkey
In `invokeai/frontend/web/src/features/system/components/HotkeysModal/useHotkeyData.ts`:
```typescript
// Inside the appropriate category builder function
addHotkey('app', 'myAction', ['mod+k']); // Default binding
```
#### 3. Use the Hotkey
In your component:
```typescript
useRegisteredHotkeys({
id: 'myAction',
category: 'app',
callback: handleMyAction,
options: { enabled: true },
dependencies: [handleMyAction]
});
```
### Hotkey Categories
Current categories:
- **app** - Global application hotkeys
- **canvas** - Canvas/drawing operations
- **viewer** - Image viewer operations
- **gallery** - Gallery/image grid operations
- **workflows** - Node workflow editor
To add a new category, update `useHotkeyData.ts` and add translations.
## Testing
Tests are located in `invokeai/frontend/web/src/features/system/store/hotkeysSlice.test.ts`.
**Test Coverage:**
- Adding custom hotkeys
- Updating existing custom hotkeys
- Resetting individual hotkeys
- Resetting all hotkeys
- State persistence and migration
Run tests with:
```bash
cd invokeai/frontend/web
pnpm test:no-watch
```
## Persistence
Custom hotkeys are persisted using the same mechanism as other app settings:
- Stored in Redux state under the `hotkeys` slice
- Persisted to IndexedDB via `redux-remember`
- Automatically loaded when the app starts
- Survives page refreshes and browser restarts
- Includes migration support for state schema changes
**State Location:**
- IndexedDB database: `invoke`
- Store key: `hotkeys`
## Dependencies
- **react-hotkeys-hook** (v4.5.0) - Core hotkey handling
- **@reduxjs/toolkit** - State management
- **redux-remember** - Persistence
- **zod** - State validation
## Best Practices
1. **Use `mod` instead of `ctrl`** - Automatically maps to Cmd on Mac, Ctrl elsewhere
2. **Provide descriptive translations** - Help users understand what each hotkey does
3. **Avoid conflicts** - Check existing hotkeys before adding new ones
4. **Use preventDefault** - Prevent browser default behavior when appropriate
5. **Check enabled state** - Only activate hotkeys when the action is available
6. **Use dependencies correctly** - Ensure callbacks are stable with useCallback
## Common Patterns
### Conditional Hotkeys
```typescript
useRegisteredHotkeys({
id: 'save',
category: 'app',
callback: handleSave,
options: {
enabled: hasUnsavedChanges && !isLoading, // Only when valid
preventDefault: true
},
dependencies: [hasUnsavedChanges, isLoading, handleSave]
});
```
### Multiple Hotkeys for Same Action
```typescript
// In useHotkeyData.ts
addHotkey('canvas', 'redo', ['mod+shift+z', 'mod+y']); // Two alternatives
```
### Focus-Scoped Hotkeys
```typescript
import { useFocusRegion } from 'common/hooks/focus';
const MyComponent = () => {
const focusRegionRef = useFocusRegion('myRegion');
// Hotkey only works when this region has focus
useRegisteredHotkeys({
id: 'myAction',
category: 'app',
callback: handleAction,
options: { enabled: true }
});
return <div ref={focusRegionRef}>...</div>;
};
```

View File

@@ -265,7 +265,7 @@ If the key is unrecognized, this call raises an
#### exists(key) -> AnyModelConfig
Returns True if a model with the given key exists in the databsae.
Returns True if a model with the given key exists in the database.
#### search_by_path(path) -> AnyModelConfig
@@ -718,7 +718,7 @@ When downloading remote models is implemented, additional
configuration information, such as list of trigger terms, will be
retrieved from the HuggingFace and Civitai model repositories.
The probed values can be overriden by providing a dictionary in the
The probed values can be overridden by providing a dictionary in the
optional `config` argument passed to `import_model()`. You may provide
overriding values for any of the model's configuration
attributes. Here is an example of setting the
@@ -841,7 +841,7 @@ variable.
#### installer.start(invoker)
The `start` method is called by the API intialization routines when
The `start` method is called by the API initialization routines when
the API starts up. Its effect is to call `sync_to_config()` to
synchronize the model record store database with what's currently on
disk.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,64 @@
# Pull Request Merge Policy
This document outlines the process for reviewing and merging pull requests (PRs) into the InvokeAI repository.
## Review Process
### 1. Assignment
One of the repository maintainers will assign collaborators to review a pull request. The assigned reviewer(s) will be responsible for conducting the code review.
### 2. Review and Iteration
The assignee is responsible for:
- Reviewing the PR thoroughly
- Providing constructive feedback
- Iterating with the PR author until the assignee is satisfied that the PR is fit to merge
- Ensuring the PR meets code quality standards, follows project conventions, and doesn't introduce bugs or regressions
### 3. Approval and Notification
Once the assignee is satisfied with the PR:
- The assignee approves the PR
- The assignee alerts one of the maintainers that the PR is ready for merge using the **#request-reviews Discord channel**
### 4. Final Merge
One of the maintainers is responsible for:
- Performing a final check of the PR
- Merging the PR into the appropriate branch
**Important:** Collaborators are strongly discouraged from merging PRs on their own, except in case of emergency (e.g., critical bug fix and no maintainer is available).
### 5. Release Policy
Once a feature release candidate is published, no feature PRs are to
be merged into main. Only bugfixes are allowed until the final
release.
## Best Practices
### Clean Commit History
To encourage a clean development log, PR authors are encouraged to use `git rebase -i` to suppress trivial commit messages (e.g., `ruff` and `prettier` formatting fixes) after the PR is accepted but before it is merged.
### Merge Strategy
The maintainer will perform either a **3-way merge** or **squash merge** when merging a PR into the `main` branch. This approach helps avoid rebase conflict hell and maintains a cleaner project history.
### Attribution
The PR author should reference any papers, source code or
documentation that they used while creating the code both in the PR
and as comments in the code itself. If there are any licensing
restrictions, these should be linked to and/or reproduced in the repo
root.
## Summary
This policy ensures that:
- All PRs receive proper review from assigned collaborators
- Maintainers have final oversight before code enters the main branch
- The commit history remains clean and meaningful
- Merge conflicts are minimized through appropriate merge strategies

View File

@@ -0,0 +1,375 @@
# Recall Parameters API - LoRAs, ControlNets, and IP Adapters with Images
## Overview
The Recall Parameters API supports recalling LoRAs, ControlNets (including T2I Adapters and Control LoRAs), and IP Adapters along with their associated weights and settings. Control Layers and IP Adapters can now include image references from the `INVOKEAI_ROOT/outputs/images` directory for fully functional control and image prompt functionality.
## Key Features
**LoRAs**: Fully functional - adds to UI, queries model configs, applies weights
**Control Layers**: Full support with optional images from outputs/images
**IP Adapters**: Full support with optional reference images from outputs/images
**Model Name Resolution**: Automatic lookup from human-readable names to internal keys
**Image Validation**: Backend validates that image files exist before sending
## Endpoints
### POST `/api/v1/recall/{queue_id}`
Updates recallable parameters for the frontend, including LoRAs, control adapters, and IP adapters with optional images.
**Path Parameters:**
- `queue_id` (string): The queue ID to associate parameters with (typically "default")
**Request Body:**
All fields are optional. Include only the parameters you want to update.
```typescript
{
// Standard parameters
positive_prompt?: string;
negative_prompt?: string;
model?: string; // Model name or key
steps?: number;
cfg_scale?: number;
width?: number;
height?: number;
seed?: number;
// ... other standard parameters
// LoRAs
loras?: Array<{
model_name: string; // LoRA model name
weight?: number; // Default: 0.75, Range: -10 to 10
is_enabled?: boolean; // Default: true
}>;
// Control Layers (ControlNet, T2I Adapter, Control LoRA)
control_layers?: Array<{
model_name: string; // Control adapter model name
image_name?: string; // Optional image filename from outputs/images
weight?: number; // Default: 1.0, Range: -1 to 2
begin_step_percent?: number; // Default: 0.0, Range: 0 to 1
end_step_percent?: number; // Default: 1.0, Range: 0 to 1
control_mode?: "balanced" | "more_prompt" | "more_control"; // ControlNet only
}>;
// IP Adapters
ip_adapters?: Array<{
model_name: string; // IP Adapter model name
image_name?: string; // Optional reference image filename from outputs/images
weight?: number; // Default: 1.0, Range: -1 to 2
begin_step_percent?: number; // Default: 0.0, Range: 0 to 1
end_step_percent?: number; // Default: 1.0, Range: 0 to 1
method?: "full" | "style" | "composition"; // Default: "full"
influence?: "Lowest" | "Low" | "Medium" | "High" | "Highest"; // Flux Redux only; default: "highest"
}>;
}
```
## Model Name Resolution
The backend automatically resolves model names to their internal keys:
1. **Main Models**: Resolved from the name to the model key
2. **LoRAs**: Searched in the LoRA model database
3. **Control Adapters**: Tried in order - ControlNet → T2I Adapter → Control LoRA
4. **IP Adapters**: Searched in the IP Adapter model database
Models that cannot be resolved are skipped with a warning in the logs.
## Image File Handling
### Image Path Resolution
When you specify an `image_name`, the backend:
1. Constructs the full path: `{INVOKEAI_ROOT}/outputs/images/{image_name}`
2. Validates that the file exists
3. Includes the image reference in the event sent to the frontend
4. Logs whether the image was found or not
### Image Naming
Images should be referenced by their filename as it appears in the outputs/images directory:
- ✅ Correct: `"image_name": "example.png"`
- ✅ Correct: `"image_name": "my_control_image_20240110.jpg"`
- ❌ Incorrect: `"image_name": "outputs/images/example.png"` (use relative filename only)
- ❌ Incorrect: `"image_name": "/full/path/to/example.png"` (use relative filename only)
## Frontend Behavior
### LoRAs
- **Fully Supported**: LoRAs are immediately added to the LoRA list in the UI
- Existing LoRAs are cleared before adding new ones
- Each LoRA's model config is fetched and applied with the specified weight
- LoRAs appear in the LoRA selector panel
### Control Layers with Images
- **Fully Supported**: Control layers now support images from outputs/images
- Configuration includes model, weights, step percentages, and image reference
- Image availability is logged in frontend console
- Images can be used to create actual control layers through the UI
### IP Adapters with Images
- **Fully Supported**: IP Adapters now support reference images from outputs/images
- Configuration includes model, weights, step percentages, method, and image reference
- Image availability is logged in frontend console
- Images can be used to create actual reference image layers through the UI
## Examples
### 1. Add LoRAs Only
```bash
curl -X POST http://localhost:9090/api/v1/recall/default \
-H "Content-Type: application/json" \
-d '{
"loras": [
{
"model_name": "add-detail-xl",
"weight": 0.8,
"is_enabled": true
},
{
"model_name": "sd_xl_offset_example-lora_1.0",
"weight": 0.5,
"is_enabled": true
}
]
}'
```
### 2. Configure Control Layers with Image
Replace `my_control_image.png` with an actual image filename from your outputs/images directory.
```bash
curl -X POST http://localhost:9090/api/v1/recall/default \
-H "Content-Type: application/json" \
-d '{
"control_layers": [
{
"model_name": "controlnet-canny-sdxl-1.0",
"image_name": "my_control_image.png",
"weight": 0.75,
"begin_step_percent": 0.0,
"end_step_percent": 0.8,
"control_mode": "balanced"
}
]
}'
```
### 3. Configure IP Adapters with Reference Image
Replace `reference_face.png` with an actual image filename from your outputs/images directory.
```bash
curl -X POST http://localhost:9090/api/v1/recall/default \
-H "Content-Type: application/json" \
-d '{
"ip_adapters": [
{
"model_name": "ip-adapter-plus-face_sd15",
"image_name": "reference_face.png",
"weight": 0.7,
"begin_step_percent": 0.0,
"end_step_percent": 1.0,
"method": "composition"
}
]
}'
```
### 4. Complete Configuration with All Features
```bash
curl -X POST http://localhost:9090/api/v1/recall/default \
-H "Content-Type: application/json" \
-d '{
"positive_prompt": "masterpiece, detailed photo with specific style",
"negative_prompt": "blurry, low quality",
"model": "FLUX Schnell",
"steps": 25,
"cfg_scale": 8.0,
"width": 1024,
"height": 768,
"seed": 42,
"loras": [
{
"model_name": "add-detail-xl",
"weight": 0.6,
"is_enabled": true
}
],
"control_layers": [
{
"model_name": "controlnet-depth-sdxl-1.0",
"image_name": "depth_map.png",
"weight": 1.0,
"begin_step_percent": 0.0,
"end_step_percent": 0.7
}
],
"ip_adapters": [
{
"model_name": "ip-adapter-plus-face_sd15",
"image_name": "style_reference.png",
"weight": 0.5,
"begin_step_percent": 0.0,
"end_step_percent": 1.0,
"method": "style"
}
]
}'
```
## Response Format
```json
{
"status": "success",
"queue_id": "default",
"updated_count": 15,
"parameters": {
"positive_prompt": "...",
"steps": 25,
"loras": [
{
"model_key": "abc123...",
"weight": 0.6,
"is_enabled": true
}
],
"control_layers": [
{
"model_key": "controlnet-xyz...",
"weight": 1.0,
"image": {
"image_name": "depth_map.png"
}
}
],
"ip_adapters": [
{
"model_key": "ip-adapter-xyz...",
"weight": 0.5,
"image": {
"image_name": "style_reference.png"
}
}
]
}
}
```
## WebSocket Events
When parameters are updated, a `recall_parameters_updated` event is emitted via WebSocket to the queue room. The frontend automatically:
1. Applies standard parameters (prompts, steps, dimensions, etc.)
2. Loads and adds LoRAs to the LoRA list
3. Logs control layer and IP adapter configurations with image information
4. Makes image references available for manual canvas/reference image creation
## Logging
### Backend Logs
Backend logs show:
- Model name → key resolution (success/failure)
- Image file validation (found/not found)
- Parameter storage confirmation
- Event emission status
Example log messages:
```
INFO: Resolved ControlNet model name 'controlnet-canny-sdxl-1.0' to key 'controlnet-xyz...'
INFO: Found image file: depth_map.png
INFO: Updated 12 recall parameters for queue default
INFO: Resolved 1 LoRA(s)
INFO: Resolved 1 control layer(s)
INFO: Resolved 1 IP adapter(s)
```
### Frontend Logs
Frontend logs (check browser console):
- Set `localStorage.ROARR_FILTER = 'debug'` to see all debug messages
- Look for messages from the `events` namespace
- LoRA loading, model resolution, and parameter application are logged
Example log messages:
```
INFO: Applied 5 recall parameters to store
INFO: Received 1 control layer(s) with image support
INFO: Control layer 1: controlnet-xyz... (weight: 0.75, image: depth_map.png)
DEBUG: Control layer 1 image available at: outputs/images/depth_map.png
INFO: Received 1 IP adapter(s) with image support
INFO: IP adapter 1: ip-adapter-xyz... (weight: 0.7, image: style_reference.png)
DEBUG: IP adapter 1 image available at: outputs/images/style_reference.png
```
## Limitations
1. **Canvas Integration**: Control layers and IP adapters with images are currently logged but not automatically added to canvas layers
- Users can view the configuration and manually create canvas layers with the provided images
- Future enhancement: Auto-create canvas layers with stored images
2. **Model Availability**: Models must be installed in InvokeAI before they can be recalled
3. **Image Availability**: Images must exist in the outputs/images directory
- Missing images are logged as warnings but don't fail the request
- Other parameters are still applied even if images are missing
4. **Image URLs**: Only local filenames from outputs/images are supported
- Remote image URLs are not currently supported
## Testing
Use the provided test script:
```bash
./test_recall_loras_controlnets.sh
```
This will test:
- LoRA addition with multiple models
- Control layer configuration with image references
- IP adapter configuration with image references
- Combined parameter updates with all features
Note: Update the image names in the test script to match actual images in your outputs/images directory.
## Troubleshooting
### Images Not Found
If you see "Image file not found" in the logs:
1. Verify the image filename matches exactly (case-sensitive)
2. Ensure the image is in `{INVOKEAI_ROOT}/outputs/images/`
3. Check that the filename doesn't include the `outputs/images/` prefix
### Models Not Found
If you see "Could not find model" messages:
1. Verify the model name matches exactly (case-sensitive)
2. Ensure the model is installed in InvokeAI
3. Check the model name using the models browser in the UI
### Event Not Received
If the frontend doesn't receive the event:
1. Check browser console for connection errors
2. Verify the queue_id matches the frontend's queue (usually "default")
3. Check backend logs for event emission errors
## Future Enhancements
Potential improvements:
1. Auto-create canvas layers with provided control layer images
2. Auto-create reference image layers with provided IP adapter images
3. Support for image URLs
4. Batch operations for multiple queue IDs
5. Image upload capability (accept base64 or file upload)

View File

@@ -0,0 +1,208 @@
# Recall Parameters API
## Overview
A new REST API endpoint has been added to the InvokeAI backend that allows programmatic updates to recallable parameters from another process. This enables external applications or scripts to modify frontend parameters like prompts, models, and step counts via HTTP requests.
When parameters are updated via the API, the backend automatically broadcasts a WebSocket event to all connected frontend clients subscribed to that queue, causing them to update immediately.
## How It Works
1. **API Request**: External application sends a POST request with parameters to update
2. **Storage**: Parameters are stored in client state persistence, associated with a queue ID
3. **Broadcast**: A WebSocket event (`recall_parameters_updated`) is emitted to all frontend clients listening to that queue
4. **Frontend Update**: Connected frontend clients receive the event and can process the updated parameters
5. **Immediate Display**: The frontend UI updates automatically with the new values
This means if you have the InvokeAI frontend open in a browser, updating parameters via the API will instantly reflect on the screen without any manual action needed.
## Endpoint
**Base URL**: `http://localhost:9090/api/v1/recall/{queue_id}`
## POST - Update Recall Parameters
Updates recallable parameters for a given queue ID.
### Request
```http
POST /api/v1/recall/{queue_id}
Content-Type: application/json
{
"positive_prompt": "a beautiful landscape",
"negative_prompt": "blurry, low quality",
"model": "sd-1.5",
"steps": 20,
"cfg_scale": 7.5,
"width": 512,
"height": 512,
"seed": 12345
}
```
The queue id is usually "default".
### Parameters
All parameters are optional. Only provide the parameters you want to update:
| Parameter | Type | Description |
|-----------|------|-------------|
| `positive_prompt` | string | Positive prompt text |
| `negative_prompt` | string | Negative prompt text |
| `model` | string | Main model name/identifier |
| `refiner_model` | string | Refiner model name/identifier |
| `vae_model` | string | VAE model name/identifier |
| `scheduler` | string | Scheduler name |
| `steps` | integer | Number of generation steps (≥1) |
| `refiner_steps` | integer | Number of refiner steps (≥0) |
| `cfg_scale` | number | CFG scale for guidance |
| `cfg_rescale_multiplier` | number | CFG rescale multiplier |
| `refiner_cfg_scale` | number | Refiner CFG scale |
| `guidance` | number | Guidance scale |
| `width` | integer | Image width in pixels (≥64) |
| `height` | integer | Image height in pixels (≥64) |
| `seed` | integer | Random seed (≥0) |
| `denoise_strength` | number | Denoising strength (0-1) |
| `refiner_denoise_start` | number | Refiner denoising start (0-1) |
| `clip_skip` | integer | CLIP skip layers (≥0) |
| `seamless_x` | boolean | Enable seamless X tiling |
| `seamless_y` | boolean | Enable seamless Y tiling |
| `refiner_positive_aesthetic_score` | number | Refiner positive aesthetic score |
| `refiner_negative_aesthetic_score` | number | Refiner negative aesthetic score |
### Response
```json
{
"status": "success",
"queue_id": "queue_123",
"updated_count": 7,
"parameters": {
"positive_prompt": "a beautiful landscape",
"negative_prompt": "blurry, low quality",
"model": "sd-1.5",
"steps": 20,
"cfg_scale": 7.5,
"width": 512,
"height": 512,
"seed": 12345
}
}
```
## GET - Retrieve Recall Parameters
Retrieves metadata about stored recall parameters.
### Request
```http
GET /api/v1/recall/{queue_id}
```
### Response
```json
{
"status": "success",
"queue_id": "queue_123",
"note": "Use the frontend to access stored recall parameters, or set specific parameters using POST"
}
```
## Usage Examples
### Using cURL
```bash
# Update prompts and model
curl -X POST http://localhost:9090/api/v1/recall/default \
-H "Content-Type: application/json" \
-d '{
"positive_prompt": "a cyberpunk city at night",
"negative_prompt": "dark, unclear",
"model": "sd-1.5",
"steps": 30
}'
# Update just the seed
curl -X POST http://localhost:9090/api/v1/recall/default \
-H "Content-Type: application/json" \
-d '{"seed": 99999}'
```
### Using Python
```python
import requests
import json
# Configuration
API_URL = "http://localhost:9090/api/v1/recall/default"
# Update multiple parameters
params = {
"positive_prompt": "a serene forest",
"negative_prompt": "people, buildings",
"steps": 25,
"cfg_scale": 7.0,
"seed": 42
}
response = requests.post(API_URL, json=params)
result = response.json()
print(f"Status: {result['status']}")
print(f"Updated {result['updated_count']} parameters")
print(json.dumps(result['parameters'], indent=2))
```
### Using Node.js/JavaScript
```javascript
const API_URL = 'http://localhost:9090/api/v1/recall/default';
const params = {
positive_prompt: 'a beautiful sunset',
negative_prompt: 'blurry',
steps: 20,
width: 768,
height: 768,
seed: 12345
};
fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(params)
})
.then(res => res.json())
.then(data => console.log(data));
```
## Implementation Details
- Parameters are stored in the client state persistence service, using keys prefixed with `recall_`
- The parameters are associated with a `queue_id`, allowing multiple concurrent sessions to maintain separate parameter sets
- Only non-null parameters are processed and stored
- The endpoint provides validation for numeric ranges (e.g., steps ≥ 1, dimensions ≥ 64)
- All parameter values are JSON-serialized for storage
- When parameter values are changed, the backend generates a web sockets event that the frontend listens to.
## Integration with Frontend
The stored parameters can be accessed by the frontend through the
existing client state API or by implementing hooks that read from the
recall parameter storage. This allows external applications to
pre-populate generation parameters before the user initiates image
generation.
## Error Handling
- **400 Bad Request**: Invalid parameters or parameter values
- **500 Internal Server Error**: Server-side error storing or retrieving parameters
Errors include detailed messages explaining what went wrong.

View File

@@ -16,7 +16,7 @@ We thank [all contributors](https://github.com/invoke-ai/InvokeAI/graphs/contrib
- @psychedelicious (Spencer Mabrito) - Web Team Leader
- @joshistoast (Josh Corbett) - Web Development
- @cheerio (Mary Rogers) - Lead Engineer & Web App Development
- @ebr (Eugene Brodsky) - Cloud/DevOps/Sofware engineer; your friendly neighbourhood cluster-autoscaler
- @ebr (Eugene Brodsky) - Cloud/DevOps/Software engineer; your friendly neighbourhood cluster-autoscaler
- @sunija - Standalone version
- @brandon (Brandon Rising) - Platform, Infrastructure, Backend Systems
- @ryanjdick (Ryan Dick) - Machine Learning & Training

View File

@@ -41,7 +41,7 @@ If you just want to use Invoke, you should use the [launcher][launcher link].
With the modifications made, the install command should look something like this:
```sh
uv pip install -e ".[dev,test,docs,xformers]" --python 3.12 --python-preference only-managed --index=https://download.pytorch.org/whl/cu126 --reinstall
uv pip install -e ".[dev,test,docs,xformers]" --python 3.12 --python-preference only-managed --index=https://download.pytorch.org/whl/cu128 --reinstall
```
6. At this point, you should have Invoke installed, a venv set up and activated, and the server running. But you will see a warning in the terminal that no UI was found. If you go to the URL for the server, you won't get a UI.
@@ -50,11 +50,11 @@ If you just want to use Invoke, you should use the [launcher][launcher link].
If you only want to edit the docs, you can stop here and skip to the **Documentation** section below.
7. Install the frontend dev toolchain:
7. Install the frontend dev toolchain, paying attention to versions:
- [`nodejs`](https://nodejs.org/) (v20+)
- [`nodejs`](https://nodejs.org/) (tested on LTS, v22)
- [`pnpm`](https://pnpm.io/8.x/installation) (must be v8 - not v9!)
- [`pnpm`](https://pnpm.io/installation) (tested on v10)
8. Do a production build of the frontend:

View File

@@ -0,0 +1,35 @@
# Canvas Text Tool
## Overview
The canvas text workflow is split between a Konva module that owns tool state and a React overlay that handles text entry.
- `invokeai/frontend/web/src/features/controlLayers/konva/CanvasTool/CanvasTextToolModule.ts`
- Owns the tool, cursor preview, and text session state (including the cursor "T" marker).
- Manages dynamic cursor contrast, starts sessions on pointer down, and commits sessions by rasterizing the active text block into a new raster layer.
- `invokeai/frontend/web/src/features/controlLayers/components/Text/CanvasTextOverlay.tsx`
- Renders the on-canvas editor as a `contentEditable` overlay positioned in canvas space.
- Syncs keyboard input, suppresses app hotkeys, and forwards commits/cancels to the Konva module.
- `invokeai/frontend/web/src/features/controlLayers/components/Text/TextToolOptions.tsx`
- Provides the font dropdown, size slider/input, formatting toggles, and alignment buttons that appear when the Text tool is active.
## Rasterization pipeline
`renderTextToCanvas()` (`invokeai/frontend/web/src/features/controlLayers/text/textRenderer.ts`) converts the editor contents into a transparent canvas. The Text tool module configures the renderer with the active font stack, weight, styling flags, alignment, and the active canvas color. The resulting canvas is encoded to a PNG data URL and stored in a new raster layer (`image` object) with a transparent background.
Layer placement preserves the original click location:
- The session stores the anchor coordinate (where the user clicked) and current alignment.
- `calculateLayerPosition()` calculates the top-left position for the raster layer after applying the configured padding and alignment offsets.
- New layers are inserted directly above the currently-selected raster layer (when present) and selected automatically.
## Font stacks
Font definitions live in `invokeai/frontend/web/src/features/controlLayers/text/textConstants.ts` as ten deterministic stacks (sans, serif, mono, rounded, script, humanist, slab serif, display, narrow, UI serif). Each stack lists system-safe fallbacks so the editor can choose the first available font per platform.
To add or adjust fonts:
1. Update `TEXT_FONT_STACKS` with the new `id`, `label`, and CSS `font-family` stack.
2. If you add a new stack, extend the `TEXT_FONT_IDS` tuple and update the `canvasTextSlice` schema default (`TEXT_DEFAULT_FONT_ID`).
3. Provide translation strings for any new labels in `public/locales/*`.
4. The editor and renderer will automatically pick up the new stack via `getFontStackById()`.

View File

@@ -297,7 +297,7 @@ Migration logic is in [migrations.ts].
<!-- links -->
[pydantic]: https://github.com/pydantic/pydantic 'pydantic'
[zod]: https://github.com/colinhacks/zod 'zod/v4'
[zod]: https://github.com/colinhacks/zod 'zod'
[openapi-types]: https://github.com/kogosoftwarellc/open-api/tree/main/packages/openapi-types 'openapi-types'
[reactflow]: https://github.com/xyflow/xyflow 'reactflow'
[reactflow-concepts]: https://reactflow.dev/learn/concepts/terms-and-definitions

View File

@@ -18,7 +18,7 @@ If youd like to add a Node, please see our [nodes contribution guide](../node
Helping support other users in [Discord](https://discord.gg/ZmtBAhwWhy) and on Github are valuable forms of contribution that we greatly appreciate.
We receive many issues and requests for help from users. We're limited in bandwidth relative to our the user base, so providing answers to questions or helping identify causes of issues is very helpful. By doing this, you enable us to spend time on the highest priority work.
We receive many issues and requests for help from users. We're limited in bandwidth relative to our user base, so providing answers to questions or helping identify causes of issues is very helpful. By doing this, you enable us to spend time on the highest priority work.
## Documentation

View File

@@ -0,0 +1,19 @@
# Text Tool
## Font selection
The Text tool uses a set of predefined font stacks. When you choose a font, the app resolves the first available font on your system from that stack and uses it for both the editor overlay and the rasterized result. This provides consistent styling across platforms while still falling back to safe system fonts if a preferred font is missing.
## Size and spacing
- **Size** controls the font size in pixels.
- **Spacing** controls the line height multiplier (Dense, Normal, Spacious). This affects the distance between lines while editing the text.
## Uncommitted state
While text is uncommitted, it remains editable on-canvas. Access to other tools is blocked. Switching to other tabs (Generate, Upascaling, Workflows etc.) discards the text. The uncommitted box can be moved and rotated:
- **Move:** Hold Ctrl (Windows/Linux) or Command (macOS) and drag to move the text box.
- **Rotate:** Drag the rotation handle above the box. Hold **Shift** while rotating to snap to 15 degree increments.
The text is committed to a raster layer when you press **Enter**. Press **Esc** to discard the current text session.

80
docs/features/hotkeys.md Normal file
View File

@@ -0,0 +1,80 @@
# Customizable Hotkeys
InvokeAI allows you to customize all keyboard shortcuts (hotkeys) to match your workflow preferences.
## Features
- **View All Hotkeys**: See all available keyboard shortcuts in one place
- **Customize Any Hotkey**: Change any shortcut to your preference
- **Multiple Bindings**: Assign multiple key combinations to the same action
- **Smart Validation**: Built-in validation prevents invalid combinations
- **Persistent Settings**: Your custom hotkeys are saved and restored across sessions
- **Easy Reset**: Reset individual hotkeys or all hotkeys back to defaults
## How to Use
### Opening the Hotkeys Modal
Press `Shift+?` or click the keyboard icon in the application to open the Hotkeys Modal.
### Viewing Hotkeys
In **View Mode** (default), you can:
- Browse all available hotkeys organized by category (App, Canvas, Gallery, Workflows, etc.)
- Search for specific hotkeys using the search bar
- See the current key combination for each action
### Customizing Hotkeys
1. Click the **Edit Mode** button at the bottom of the Hotkeys Modal
2. Find the hotkey you want to change
3. Click the **pencil icon** next to it
4. The editor will appear with:
- **Input field**: Enter your new hotkey combination
- **Modifier buttons**: Quick-insert Mod, Ctrl, Shift, Alt keys
- **Help icon** (?): Shows syntax examples and valid keys
- **Live preview**: See how your hotkey will look
5. Enter your new hotkey using the format:
- `mod+a` - Mod key + A (Mod = Ctrl on Windows/Linux, Cmd on Mac)
- `ctrl+shift+k` - Multiple modifiers
- `f1` - Function keys
- `mod+enter, ctrl+enter` - Multiple alternatives (separated by comma)
6. Click the **checkmark** or press Enter to save
7. Click the **X** or press Escape to cancel
### Resetting Hotkeys
**Reset a single hotkey:**
- Click the counter-clockwise arrow icon that appears next to customized hotkeys
**Reset all hotkeys:**
- In Edit Mode, click the **Reset All to Default** button at the bottom
### Hotkey Format Reference
**Valid Modifiers:**
- `mod` - Context-aware: Ctrl (Windows/Linux) or Cmd (Mac)
- `ctrl` - Control key
- `shift` - Shift key
- `alt` - Alt key (Option on Mac)
**Valid Keys:**
- Letters: `a-z`
- Numbers: `0-9`
- Function keys: `f1-f12`
- Special keys: `enter`, `space`, `tab`, `backspace`, `delete`, `escape`
- Arrow keys: `up`, `down`, `left`, `right`
- And more...
**Examples:**
-`mod+s` - Save action
-`ctrl+shift+p` - Command palette
-`f5, mod+r` - Two alternatives for refresh
-`mod+` - Invalid (no key after modifier)
-`shift+ctrl+` - Invalid (ends with modifier)
## For Developers
For technical implementation details, architecture, and how to add new hotkeys to the system, see the [Hotkeys Developer Documentation](../contributing/HOTKEYS.md).

View File

@@ -0,0 +1,152 @@
# Orphaned Models Synchronization Feature
## Overview
This feature adds a UI for synchronizing the models directory by finding and removing orphaned model files. Orphaned models are directories that contain model files but are not referenced in the InvokeAI database.
## Implementation Summary
### Backend (Python)
#### New Service: `OrphanedModelsService`
- Location: `invokeai/app/services/orphaned_models/`
- Implements the core logic from the CLI script
- Methods:
- `find_orphaned_models()`: Scans the models directory and database to find orphaned models
- `delete_orphaned_models(paths)`: Safely deletes specified orphaned model directories
#### API Routes
Added to `invokeai/app/api/routers/model_manager.py`:
- `GET /api/v2/models/sync/orphaned`: Returns list of orphaned models with metadata
- `DELETE /api/v2/models/sync/orphaned`: Deletes selected orphaned models
#### Data Models
- `OrphanedModelInfo`: Contains path, absolute_path, files list, and size_bytes
- `DeleteOrphanedModelsRequest`: Contains list of paths to delete
- `DeleteOrphanedModelsResponse`: Contains deleted paths and errors
### Frontend (TypeScript/React)
#### New Components
1. **SyncModelsButton.tsx**
- Red button styled with `colorScheme="error"` for visual prominence
- Labeled "Sync Models"
- Opens the SyncModelsDialog when clicked
- Located next to the "+ Add Models" button
2. **SyncModelsDialog.tsx**
- Modal dialog that displays orphaned models
- Features:
- List of orphaned models with checkboxes (default: all checked)
- "Select All" / "Deselect All" toggle
- Shows file count and total size for each model
- "Delete" and "Cancel" buttons
- Loading spinner while fetching data
- Error handling with user-friendly messages
- Automatically shows toast if no orphaned models found
- Shows success/error toasts after deletion
#### API Integration
- Added `useGetOrphanedModelsQuery` and `useDeleteOrphanedModelsMutation` hooks to `services/api/endpoints/models.ts`
- Integrated with RTK Query for efficient data fetching and caching
#### Translation Strings
Added to `public/locales/en.json`:
- syncModels, noOrphanedModels, orphanedModelsFound
- orphanedModelsDescription, foundOrphanedModels (with pluralization)
- filesCount, deleteSelected, deselectAll
- Success/error messages for deletion operations
## User Experience Flow
1. User clicks the red "Sync Models" button in the Model Manager
2. System queries the backend for orphaned models
3. If no orphaned models:
- Toast message: "The models directory is synchronized. No orphaned files found."
- Dialog closes automatically
4. If orphaned models found:
- Dialog shows list with checkboxes (all selected by default)
- User can toggle individual models or use "Select All" / "Deselect All"
- Each model shows:
- Directory path
- File count
- Total size (formatted: B, KB, MB, GB)
5. User clicks "Delete {{count}} selected"
6. System deletes selected models
7. Success/error toasts appear
8. Dialog closes
## Safety Features
1. **Database Backup**: The service creates a backup before any deletion
2. **Selective Deletion**: Users choose which models to delete
3. **Path Validation**: Ensures paths are within the models directory
4. **Error Handling**: Reports which models failed to delete and why
5. **Default Selected**: All models are selected by default for convenience
6. **Confirmation Required**: User must explicitly click Delete
## Technical Details
### Directory-Based Detection
The system treats model paths as directories:
- If database has `model-id/file.safetensors`, the entire `model-id/` directory belongs to that model
- All files and subdirectories within a registered model directory are protected
- Only directories with NO registered models are flagged as orphaned
### Supported File Extensions
- .safetensors
- .ckpt
- .pt
- .pth
- .bin
- .onnx
### Skipped Directories
- .download_cache
- .convert_cache
- \_\_pycache\_\_
- .git
## Testing Recommendations
1. **Test with orphaned models**:
- Manually copy a model directory to models folder
- Verify it appears in the dialog
- Delete it and verify removal
2. **Test with no orphaned models**:
- Clean install
- Verify toast message appears
3. **Test partial selection**:
- Select only some models
- Verify only selected ones are deleted
4. **Test error scenarios**:
- Invalid paths
- Permission issues
- Verify error messages are clear
## Files Changed
### Backend
- `invokeai/app/services/orphaned_models/__init__.py` (new)
- `invokeai/app/services/orphaned_models/orphaned_models_service.py` (new)
- `invokeai/app/api/routers/model_manager.py` (modified)
### Frontend
- `invokeai/frontend/web/src/services/api/endpoints/models.ts` (modified)
- `invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManager.tsx` (modified)
- `invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/SyncModelsButton.tsx` (new)
- `invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/SyncModelsDialog.tsx` (new)
- `invokeai/frontend/web/public/locales/en.json` (modified)
## Future Enhancements
Potential improvements for future versions:
1. Show preview of what will be deleted before deletion
2. Add option to move orphaned models to archive instead of deleting
3. Show disk space that will be freed
4. Add filter/search in orphaned models list
5. Support for undo operation
6. Scheduled automatic cleanup

View File

@@ -69,34 +69,34 @@ The following commands vary depending on the version of Invoke being installed a
- If you have an Nvidia 20xx series GPU or older, use `invokeai[xformers]`.
- If you have an Nvidia 30xx series GPU or newer, or do not have an Nvidia GPU, use `invokeai`.
7. Determine the `PyPI` index URL to use for installation, if any. This is necessary to get the right version of torch installed.
7. Determine the torch backend to use for installation, if any. This is necessary to get the right version of torch installed. This is acheived by using [UV's built in torch support.](https://docs.astral.sh/uv/guides/integration/pytorch/#automatic-backend-selection)
=== "Invoke v5.12 and later"
- If you are on Windows or Linux with an Nvidia GPU, use `https://download.pytorch.org/whl/cu128`.
- If you are on Linux with no GPU, use `https://download.pytorch.org/whl/cpu`.
- If you are on Linux with an AMD GPU, use `https://download.pytorch.org/whl/rocm6.2.4`.
- **In all other cases, do not use an index.**
- If you are on Windows or Linux with an Nvidia GPU, use `--torch-backend=cu128`.
- If you are on Linux with no GPU, use `--torch-backend=cpu`.
- If you are on Linux with an AMD GPU, use `--torch-backend=rocm6.3`.
- **In all other cases, do not use a torch backend.**
=== "Invoke v5.10.0 to v5.11.0"
- If you are on Windows or Linux with an Nvidia GPU, use `https://download.pytorch.org/whl/cu126`.
- If you are on Linux with no GPU, use `https://download.pytorch.org/whl/cpu`.
- If you are on Linux with an AMD GPU, use `https://download.pytorch.org/whl/rocm6.2.4`.
- If you are on Windows or Linux with an Nvidia GPU, use `--torch-backend=cu126`.
- If you are on Linux with no GPU, use `--torch-backend=cpu`.
- If you are on Linux with an AMD GPU, use `--torch-backend=rocm6.2.4`.
- **In all other cases, do not use an index.**
=== "Invoke v5.0.0 to v5.9.1"
- If you are on Windows with an Nvidia GPU, use `https://download.pytorch.org/whl/cu124`.
- If you are on Linux with no GPU, use `https://download.pytorch.org/whl/cpu`.
- If you are on Linux with an AMD GPU, use `https://download.pytorch.org/whl/rocm6.1`.
- If you are on Windows with an Nvidia GPU, use `--torch-backend=cu124`.
- If you are on Linux with no GPU, use `--torch-backend=cpu`.
- If you are on Linux with an AMD GPU, use `--torch-backend=rocm6.1`.
- **In all other cases, do not use an index.**
=== "Invoke v4"
- If you are on Windows with an Nvidia GPU, use `https://download.pytorch.org/whl/cu124`.
- If you are on Linux with no GPU, use `https://download.pytorch.org/whl/cpu`.
- If you are on Linux with an AMD GPU, use `https://download.pytorch.org/whl/rocm5.2`.
- If you are on Windows with an Nvidia GPU, use `--torch-backend=cu124`.
- If you are on Linux with no GPU, use `--torch-backend=cpu`.
- If you are on Linux with an AMD GPU, use `--torch-backend=rocm5.2`.
- **In all other cases, do not use an index.**
8. Install the `invokeai` package. Substitute the package specifier and version.
@@ -105,10 +105,10 @@ The following commands vary depending on the version of Invoke being installed a
uv pip install <PACKAGE_SPECIFIER>==<VERSION> --python 3.12 --python-preference only-managed --force-reinstall
```
If you determined you needed to use a `PyPI` index URL in the previous step, you'll need to add `--index=<INDEX_URL>` like this:
If you determined you needed to use a torch backend in the previous step, you'll need to set the backend like this:
```sh
uv pip install <PACKAGE_SPECIFIER>==<VERSION> --python 3.12 --python-preference only-managed --index=<INDEX_URL> --force-reinstall
uv pip install <PACKAGE_SPECIFIER>==<VERSION> --python 3.12 --python-preference only-managed --torch-backend=<VERSION> --force-reinstall
```
9. Deactivate and reactivate your venv so that the invokeai-specific commands become available in the environment:

View File

@@ -70,7 +70,7 @@ Prior to installing PyPatchMatch, you need to take the following steps:
`from patchmatch import patch_match`: It should look like the following:
```py
Python 3.10.12 (main, Jun 11 2023, 05:26:28) [GCC 11.4.0] on linux
Python 3.12.3 (main, Aug 14 2025, 17:47:21) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from patchmatch import patch_match
Compiling and loading c extensions from "/home/lstein/Projects/InvokeAI/.invokeai-env/src/pypatchmatch/patchmatch".

View File

@@ -25,38 +25,65 @@ Hardware requirements vary significantly depending on model and image output siz
- Memory: At least 16GB RAM.
- Disk: 10GB for base installation plus 100GB for models.
=== "FLUX - 1024×1024"
=== "FLUX.1 - 1024×1024"
- GPU: Nvidia 20xx series or later, 10GB+ VRAM.
- Memory: At least 32GB RAM.
- Disk: 10GB for base installation plus 200GB for models.
=== "FLUX.2 Klein - 1024×1024"
- GPU: Nvidia 20xx series or later, 6GB+ VRAM for GGUF Q4 quantized models, 12GB+ for full precision.
- Memory: At least 16GB RAM.
- Disk: 10GB for base installation plus 20GB for models.
=== "Z-Image Turbo - 1024x1024"
- GPU: Nvidia 20xx series or later, 8GB+ VRAM for the Q4_K quantized model. 16GB+ needed for the Q8 or BF16 models.
- Memory: At least 16GB RAM.
- Disk: 10GB for base installation plus 35GB for models.
More detail on system requirements can be found [here](./requirements.md).
## Step 2: Download
## Step 2: Download and Set Up the Launcher
Download the most recent launcher for your operating system:
The Launcher manages your Invoke install. Follow these instructions to download and set up the Launcher.
- [Download for Windows](https://download.invoke.ai/Invoke%20Community%20Edition.exe)
- [Download for macOS](https://download.invoke.ai/Invoke%20Community%20Edition.dmg)
- [Download for Linux](https://download.invoke.ai/Invoke%20Community%20Edition.AppImage)
!!! info "Instructions for each OS"
## Step 3: Install or Update
=== "Windows"
Run the launcher you just downloaded, click **Install** and follow the instructions to get set up.
- [Download for Windows](https://github.com/invoke-ai/launcher/releases/latest/download/Invoke.Community.Edition.Setup.latest.exe)
- Run the `EXE` to install the Launcher and start it.
- A desktop shortcut will be created; use this to run the Launcher in the future.
- You can delete the `EXE` file you downloaded.
=== "macOS"
- [Download for macOS](https://github.com/invoke-ai/launcher/releases/latest/download/Invoke.Community.Edition-latest-arm64.dmg)
- Open the `DMG` and drag the app into `Applications`.
- Run the Launcher using its entry in `Applications`.
- You can delete the `DMG` file you downloaded.
=== "Linux"
- [Download for Linux](https://github.com/invoke-ai/launcher/releases/latest/download/Invoke.Community.Edition-latest.AppImage)
- You may need to edit the `AppImage` file properties and make it executable.
- Optionally move the file to a location that does not require admin privileges and add a desktop shortcut for it.
- Run the Launcher by double-clicking the `AppImage` or the shortcut you made.
## Step 3: Install Invoke
Run the Launcher you just set up if you haven't already. Click **Install** and follow the instructions to install (or update) Invoke.
If you have an existing Invoke installation, you can select it and let the launcher manage the install. You'll be able to update or launch the installation.
!!! warning "Problem running the launcher on macOS"
!!! tip "Updating"
macOS may not allow you to run the launcher. We are working to resolve this by signing the launcher executable. Until that is done, you can manually flag the launcher as safe:
The Launcher will check for updates for itself _and_ Invoke.
- Open the **Invoke Community Edition.dmg** file.
- Drag the launcher to **Applications**.
- Open a terminal.
- Run `xattr -d 'com.apple.quarantine' /Applications/Invoke\ Community\ Edition.app`.
You should now be able to run the launcher.
- When the Launcher detects an update is available for itself, you'll get a small popup window. Click through this and the Launcher will update itself.
- When the Launcher detects an update for Invoke, you'll see a small green alert in the Launcher. Click that and follow the instructions to update Invoke.
## Step 4: Launch

View File

@@ -6,7 +6,9 @@ Invoke runs on Windows 10+, macOS 14+ and Linux (Ubuntu 20.04+ is well-tested).
Hardware requirements vary significantly depending on model and image output size.
The requirements below are rough guidelines for best performance. GPUs with less VRAM typically still work, if a bit slower. Follow the [Low-VRAM mode guide](./features/low-vram.md) to optimize performance.
The requirements below are rough guidelines for best performance. GPUs
with less VRAM typically still work, if a bit slower. Follow the
[Low-VRAM mode guide](../features/low-vram.md) to optimize performance.
- All Apple Silicon (M1, M2, etc) Macs work, but 16GB+ memory is recommended.
- AMD GPUs are supported on Linux only. The VRAM requirements are the same as Nvidia GPUs.
@@ -25,12 +27,29 @@ The requirements below are rough guidelines for best performance. GPUs with less
- Memory: At least 16GB RAM.
- Disk: 10GB for base installation plus 100GB for models.
=== "FLUX - 1024×1024"
=== "FLUX.1 - 1024×1024"
- GPU: Nvidia 20xx series or later, 10GB+ VRAM.
- Memory: At least 32GB RAM.
- Disk: 10GB for base installation plus 200GB for models.
=== "FLUX.2 Klein 4B - 1024×1024"
- GPU: Nvidia 30xx series or later, 12GB+ VRAM (e.g. RTX 3090, RTX 4070). FP8 version works with 8GB+ VRAM.
- Memory: At least 16GB RAM.
- Disk: 10GB for base installation plus 20GB for models (Diffusers format with encoder).
=== "FLUX.2 Klein 9B - 1024×1024"
- GPU: Nvidia 40xx series, 24GB+ VRAM (e.g. RTX 4090). FP8 version works with 12GB+ VRAM.
- Memory: At least 32GB RAM.
- Disk: 10GB for base installation plus 40GB for models (Diffusers format with encoder).
=== "Z-Image Turbo - 1024x1024"
- GPU: Nvidia 20xx series or later, 8GB+ VRAM for the Q4_K quantized model. 16GB+ needed for the Q8 or BF16 models.
- Memory: At least 16GB RAM.
- Disk: 10GB for base installation plus 35GB for models.
!!! info "`tmpfs` on Linux"
If your temporary directory is mounted as a `tmpfs`, ensure it has sufficient space.
@@ -41,7 +60,7 @@ The requirements below are rough guidelines for best performance. GPUs with less
You don't need to do this if you are installing with the [Invoke Launcher](./quick_start.md).
Invoke requires python 3.10 through 3.12. If you don't already have one of these versions installed, we suggest installing 3.12, as it will be supported for longer.
Invoke requires python 3.11 through 3.12. If you don't already have one of these versions installed, we suggest installing 3.12, as it will be supported for longer.
Check that your system has an up-to-date Python installed by running `python3 --version` in the terminal (Linux, macOS) or cmd/powershell (Windows).
@@ -56,7 +75,7 @@ Check that your system has an up-to-date Python installed by running `python3 --
=== "macOS"
- Install python with [an official installer].
- If model installs fail with a certificate error, you may need to run this command (changing the python version to match what you have installed): `/Applications/Python\ 3.10/Install\ Certificates.command`
- If model installs fail with a certificate error, you may need to run this command (changing the python version to match what you have installed): `/Applications/Python\ 3.11/Install\ Certificates.command`
- If you haven't already, you will need to install the XCode CLI Tools by running `xcode-select --install` in a terminal.
=== "Linux"

View File

@@ -0,0 +1,876 @@
# InvokeAI Multi-User Administrator Guide
## Overview
This guide is for administrators managing a multi-user InvokeAI installation. It covers initial setup, user management, security best practices, and troubleshooting.
## Prerequisites
Before enabling multi-user support, ensure you have:
- InvokeAI installed and running
- Access to the server filesystem (for initial setup)
- Understanding of your deployment environment
- Backup of your existing data (recommended)
## Initial Setup
### Activating Multiuser Mode
To put InvokeAI into multiuser mode, you will need to add the option
`multiuser: true` to its configuration file. This file is located at
`INVOKEAI_ROOT/invokeai.yaml` With the InvokeAI backend halted, add
the new configuration option to the end of the file with a text editor
so that it looks like this:
```yaml
# Internal metadata - do not edit:
schema_version: 4.0.2
# Enable/disable multi-user mode
multiuser: true
```
Then restart the InvokeAI server backend from the command line or
using the launcher.
!!! note "Reverting to single-user mode"
If at any time you wish to revert to single-user mode, simply comment
out the `multiuser` line, or change "true" to "false". Then
restart the server. Because of the way that browsers cache pages,
users with open InvokeAI sessions may need to force-refresh their
browsers.
### First Administrator Account
When InvokeAI starts for the first time in multi-user mode, you'll see the **Administrator Setup** dialog.
**Setup Steps:**
1. **Email Address**: Enter a valid email address (this becomes your username)
* Example: `admin@example.com` or `admin@localhost` for testing
* Must be a valid email format
* Cannot be changed later without database access
2. **Display Name**: Enter a friendly name
* Example: "System Administrator" or your real name
* Can be changed later in your profile
* Visible to other users in shared contexts
3. **Password**: Create a strong administrator password
* **Minimum requirements:**
* At least 8 characters long
* Contains uppercase letters (A-Z)
* Contains lowercase letters (a-z)
* Contains numbers (0-9)
* **Recommended:**
* Use 12+ characters
* Include special characters (!@#$%^&*)
* Use a password manager to generate and store
* Don't reuse passwords from other services
4. **Confirm Password**: Re-enter the password
5. Click **Create Administrator Account**
!!! warning "Important"
Store these credentials securely! The
first administrator account can reset
the password to something new, but cannot
retrieve a lost one.
### Configuration
InvokeAI can run in single-user or multi-user mode, controlled by the `multiuser` configuration option in `invokeai.yaml`:
```yaml
# Enable/disable multi-user mode
multiuser: true # Enable multi-user mode (requires authentication)
# multiuser: false # Single-user mode (no authentication required)
# If the multiuser option is absent, single-user mode is used
# Database configuration
use_memory_db: false # Use persistent database
db_path: databases/invokeai.db # Database location
# Session configuration (multi-user mode only)
jwt_secret_key: "your-secret-key-here" # Auto-generated if not specified
jwt_token_expiry_hours: 24 # Default session timeout
jwt_remember_me_days: 7 # "Remember me" duration
```
**Single-User Mode** (`multiuser: false` or option absent):
- No authentication required
- All functionality enabled by default
- All boards and images visible in unified view
- Ideal for personal use or trusted environments
**Multi-User Mode** (`multiuser: true`):
- Authentication required for access
- User isolation for boards, images, and workflows
- Role-based permissions enforced
- Ideal for shared servers or team environments
!!! warning "Mode Switching Behavior"
**Switching to Single-User Mode:** If boards or images were created in multi-user mode, they will all be combined into a single unified view when switching to single-user mode.
**Switching to Multi-User Mode:** Legacy boards and images created under single-user mode will be owned by an internal user named "system." Only the Administrator will have access to these legacy assets. A utility to migrate these legacy assets to another user will be part of a future release.
### Migration from Single-User
When upgrading from a single-user installation or switching modes:
1. **Automatic Migration**: The database will automatically migrate to multi-user schema when multi-user mode is first enabled
2. **Legacy Data Ownership**: Existing data (boards, images, workflows) created in single-user mode is assigned to an internal user named "system"
3. **Administrator Access**: Only administrators will have access to legacy "system"-owned assets when in multi-user mode
4. **No Data Loss**: All existing content is preserved
**Migration Process:**
```bash
# Backup your database first
cp databases/invokeai.db databases/invokeai.db.backup
# Enable multi-user mode in invokeai.yaml
# multiuser: true
# Start InvokeAI (migration happens automatically)
invokeai-web
# Complete the administrator setup dialog
# Legacy data will be owned by "system" user
```
!!! note "Legacy Asset Migration"
A utility to migrate legacy "system"-owned assets to specific user accounts will be available in a future release. Until then, administrators can access and manage all legacy content.
## User Management
### Creating Users
**Via Web Interface (Coming Soon):**
!!! info "Web UI for User Management"
A web-based user interface that allows administrators to manage users is coming in a future release. Until then, use the command-line scripts described below.
**Via Command Line Scripts:**
InvokeAI provides several command-line scripts in the `scripts/` directory for user management:
**useradd.py** - Add a new user:
```bash
# Interactive mode (prompts for details)
python scripts/useradd.py
# Create a regular user
python scripts/useradd.py \
--email user@example.com \
--password TempPass123 \
--name "User Name"
# Create an administrator
python scripts/useradd.py \
--email admin@example.com \
--password AdminPass123 \
--name "Admin Name" \
--admin
```
**userlist.py** - List all users:
```bash
# List all users
python scripts/userlist.py
# Show detailed information
python scripts/userlist.py --verbose
```
**usermod.py** - Modify an existing user:
```bash
# Change display name
python scripts/usermod.py --email user@example.com --name "New Name"
# Promote to administrator
python scripts/usermod.py --email user@example.com --admin
# Demote from administrator
python scripts/usermod.py --email user@example.com --no-admin
# Deactivate account
python scripts/usermod.py --email user@example.com --deactivate
# Reactivate account
python scripts/usermod.py --email user@example.com --activate
# Change password
python scripts/usermod.py --email user@example.com --password NewPassword123
```
**userdel.py** - Delete a user:
```bash
# Delete a user (prompts for confirmation)
python scripts/userdel.py --email user@example.com
# Delete without confirmation
python scripts/userdel.py --email user@example.com --force
```
!!! tip "Script Usage"
Run any script with `--help` to see all available options:
```bash
python scripts/useradd.py --help
```
!!! warning "Command Line Management"
- These scripts directly modify the database
- Always backup your database before making changes
- Changes take effect immediately (users may need to log in again)
- Deleting a user permanently removes all their content
### Editing Users
**Via Command Line:**
Use `usermod.py` as described above to modify user properties.
!!! warning "Last Administrator"
You cannot remove admin privileges from the last remaining administrator account.
### Resetting User Passwords
**Via Web Interface (Coming Soon):**
Web-based password reset functionality for administrators is coming in a future release.
**Via Command Line:**
```bash
# Reset a user's password
python scripts/usermod.py --email user@example.com --password NewTempPassword123
```
**Security Note:** Never send passwords via email or unsecured channels. Use secure communication methods.
### Deactivating Users
**Via Command Line:**
```bash
# Deactivate a user account
python scripts/usermod.py --email user@example.com --deactivate
# Reactivate a user account
python scripts/usermod.py --email user@example.com --activate
```
**Effects:**
- User cannot log in when deactivated
- Existing sessions are immediately invalidated
- User's data is preserved
- Can be reactivated at any time
### Deleting Users
**Via Command Line:**
```bash
# Delete a user (prompts for confirmation)
python scripts/userdel.py --email user@example.com
# Delete without confirmation prompt
python scripts/userdel.py --email user@example.com --force
```
**Important:**
- ⚠️ This action is **permanent**
- User's boards, images, and workflows are deleted
- Cannot be undone
- Consider deactivating instead of deleting
!!! warning "Data Loss"
Deleting a user permanently removes all their content. Back up the database first if recovery might be needed.
### Viewing User Activity
**Queue Management:**
1. Navigate to **Admin** → **Queue Overview**
2. View all users' active and pending generations
3. Filter by user
4. Cancel stuck or problematic tasks
**User Statistics:**
- Number of boards created
- Number of images generated
- Storage usage (if enabled)
- Last login time
## Model Management
As an administrator, you have full access to model management.
### Adding Models
**Via Model Manager UI:**
1. Go to **Models** tab
2. Click **Add Model**
3. Choose installation method:
- **From URL**: Provide HuggingFace repo or download URL
- **From Local Path**: Scan local directories
- **Import**: Import model from filesystem
**Supported Model Types:**
- Main models (Stable Diffusion, SDXL, FLUX)
- LoRA models
- ControlNet models
- VAE models
- Textual Inversions
- IP-Adapters
### Configuring Models
**Model Settings:**
- Display name
- Description
- Default generation settings (CFG, steps, scheduler)
- Variant selection (fp16/fp32)
- Model thumbnail image
**Default Settings:**
Set default parameters that users will start with:
1. Select a model
2. Go to **Default Settings** tab
3. Configure:
- CFG Scale
- Steps
- Scheduler
- VAE selection
4. Save settings
### Removing Models
1. Go to **Models** tab
2. Select model(s) to remove
3. Click **Delete**
4. Confirm deletion
!!! warning "Impact"
Removing a model affects all users who may be using it in workflows or saved settings.
## Shared Boards
Shared boards enable collaboration between users while maintaining control.
!!! note "Future Feature"
Board sharing will be implemented in a future release.
### Creating Shared Boards
1. Log in as administrator
2. Create a new board (or use existing board)
3. Right-click the board → **Share Board**
4. Add users and set permissions
5. Click **Save Sharing Settings**
### Permission Levels
| Level | View | Add Images | Edit/Delete | Manage Sharing |
|-------|------|------------|-------------|----------------|
| **Read** | ✅ | ❌ | ❌ | ❌ |
| **Write** | ✅ | ✅ | ✅ | ❌ |
| **Admin** | ✅ | ✅ | ✅ | ✅ |
**Permission Recommendations:**
- **Read**: For viewers who should see but not modify content
- **Write**: For active collaborators who add and organize images
- **Admin**: For trusted users who help manage the shared board
### Managing Shared Boards
**Add Users to Shared Board:**
1. Right-click shared board → **Manage Sharing**
2. Click **Add User**
3. Select user from dropdown
4. Choose permission level
5. Save changes
**Remove Users from Shared Board:**
1. Right-click shared board → **Manage Sharing**
2. Find user in list
3. Click **Remove**
4. Confirm removal
**Change User Permissions:**
1. Right-click shared board → **Manage Sharing**
2. Find user in list
3. Change permission dropdown
4. Save changes
### Shared Board Best Practices
- Give meaningful names to shared boards
- Document the board's purpose in the description
- Assign minimum necessary permissions
- Regularly audit access lists
- Remove users who no longer need access
## Security
### Password Policies
**Enforced Requirements:**
- Minimum 8 characters
- Must contain uppercase letters
- Must contain lowercase letters
- Must contain numbers
**Recommended Policies:**
- Require 12+ character passwords
- Include special characters
- Implement password rotation every 90 days
- Prevent password reuse
- Use multi-factor authentication (when available)
### Session Management
**Session Security and Token Management:**
This system uses stateless JWT tokens with HMAC signatures to
identify users after they provide their initial credentials. The
tokens will persist for 24 hours by default, or for 7 days if the user
clicks the "Remember me" checkbox at login. Expired tokens are
automatically rejected and the user will have to log in again.
At the client side, tokens are stored in browser localStorage. Logging
out clears them. No server-side session storage is required.
The tokens include the user's ID, email, and admin status, along with
an HMAC signature.
### Secret Key Management
**Important:** The JWT secret key must be kept confidential.
To generate tokens, each InvokeAI instance has a distinct secret JWT key that must be
kept confidential. The key is stored in the `app_settings` table of
the InvokeAI database with in a field value named `jwt_secret`.
The secret key is automatically generated during database creation or
migration. If you wish to change the key, you may generate a
replacement using either of these commands:
```bash
# Python
python -c "import secrets; print(secrets.token_urlsafe(32))"
# OpenSSL
openssl rand -base64 32
```
Then cut and paste the printed secret into this Sqlite3 command:
```bash
sqlite3 INVOKE_ROOT/databases/invokeai.db 'update app_settings set value="THE_SECRET" where key="jwt_secret"'
```
(replace INVOKE_ROOT with your InvokeAI root directory and THE_SECRET
with the new secret).
After this, restart the server. All logged in users will be logged out
and will need to provide their usernames and passwords again.
### Hosting a Shared InvokeAI Instance
The multiuser feature allows you to run an InvokeAI backend that can
be accessed by your friends and family across your home network. It is
also possible to host a backend that is accessible over the Internet.
By default, InvokeAI runs on `localhost`, IP address `127.0.0.1`,
which is only accessible to browsers running on the same machine as
the backend. To make the backend accessible to any machine on your
home or work LAN, add the line `host: 0.0.0.0` to the InvokeAI
configuration file, usually stored at `INVOKE_ROOT/invokeai.yaml`.
Here is a minimal example.
```yaml
# Internal metadata - do not edit:
schema_version: 4.0.2
# Put user settings here - see https://invoke-ai.github.io/InvokeAI/configuration/:
multiuser: true
host: 0.0.0.0
```
After relaunching the backend you will be able to reach the server
from other machines on the LAN using the server machine's IP address
or hostname and port 9090.
#### Connecting to the Internet
!!! warning "Use at your own risk"
The InvokeAI team has done its best to make the software free of
exploitable bugs, but the software has not undergone a rigorous security
audit or intrusion testing. Use at your own risk
It is also possible to create a (semi) public server accessible from
the Internet. The details of how to do this depend very much on your
home or corporate router/firewall system and are beyond the scope of
this document.
If you expose InvokeAI to the Internet, there are a number of
precautions to take. Here is a brief list of recommended network
security practices.
**HTTPS Configuration:**
For internet deployments, always use HTTPS:
```yaml
# Use a reverse proxy like nginx or Traefik
# Example nginx configuration:
server {
listen 443 ssl http2;
server_name invoke.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:9090;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
```
**Firewall Rules:**
It is best to restrict access to trusted networks and remote IP
addresses, or use a VPN to connect to your home network. Rate limit
connections to InvokeAI's authentication endpoint
`http://your.host:9090/login`.
**Backup and Recovery:**
It is a good idea to periodically backup your InvokeAI database,
images, and possibly models in the event of unauthorized use of a
publicly-accessible server.
**Manual Backup:**
```bash
# Stop InvokeAI
# Copy database file
cd INVOKE_ROOT
cp databases/invokeai.db databases/invokeai.db.$(date +%Y%m%d)
# Or create compressed backup
tar -czf invokeai_backup_$(date +%Y%m%d).tar.gz databases/
```
**Automated Backup Script:**
```bash
#!/bin/bash
# backup_invokeai.sh
INVOKE_ROOT="/path/to/invoke_root"
BACKUP_DIR="/path/to/backups"
DB_PATH="$INVOKE_ROOT/databases/invokeai.db"
DATE=$(date +%Y%m%d_%H%M%S)
# Create backup directory
mkdir -p "$BACKUP_DIR"
# Copy database
cp "$DB_PATH" "$BACKUP_DIR/invokeai_$DATE.db"
# Keep only last 30 days
find "$BACKUP_DIR" -name "invokeai_*.db" -mtime +30 -delete
echo "Backup completed: invokeai_$DATE.db"
```
**Schedule with cron:**
```bash
# Edit crontab
crontab -e
# Add daily backup at 2 AM
0 2 * * * /path/to/backup_invokeai.sh
```
```bash
# Stop InvokeAI
# Replace current database with backup
cd INVOKE_ROOT
cp databases/invokeai.db databases/invokeai.db.old # Save current
cp databases/invokeai_backup.db databases/invokeai.db
# Restart InvokeAI
invokeai-web
```
**Disaster Recover - Complete System Backup:**
Include these directories/files:
- `databases/` - All database files
- `models/` - Installed models (if locally stored)
- `outputs/` - Generated images
- `invokeai.yaml` - Configuration file
- Any custom scripts or modifications
**Recovery Process:**
1. Install InvokeAI on new system
2. Restore configuration file
3. Restore database directory
4. Restore models and outputs
5. Verify file permissions
6. Start InvokeAI and test
## Troubleshooting
### User Cannot Login
**Symptom:** User reports unable to log in
**Diagnosis:**
1. Verify account exists and is active
```bash
sqlite3 databases/invokeai.db "SELECT * FROM users WHERE email = 'user@example.com';"
```
2. Check password (have user try resetting)
3. Verify account is active (`is_active = 1`)
4. Check for account lockout (if implemented)
**Solutions:**
- Reset user password
- Reactivate disabled account
- Verify email address is correct
- Check system logs for auth errors
### Database Locked Errors
**Symptom:** "Database is locked" errors
**Causes:**
- Concurrent write operations
- Long-running transactions
- Backup process accessing database
- File system issues
**Solutions:**
```bash
# Check for locks
fuser databases/invokeai.db
# Increase timeout (in config)
# Or switch to WAL mode:
sqlite3 databases/invokeai.db "PRAGMA journal_mode=WAL;"
```
### Forgotten Admin Password
**Recovery Process:**
1. Stop InvokeAI
2. Direct database access:
```bash
sqlite3 databases/invokeai.db
```
3. Reset admin password (requires password hash):
```sql
-- Generate hash first using Python:
-- from passlib.context import CryptContext
-- pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
-- print(pwd_context.hash("NewPassword123"))
UPDATE users
SET password_hash = '$2b$12$...'
WHERE email = 'admin@example.com';
```
4. Restart InvokeAI
**Alternative:** Remove `jwt_secret_key` from config to trigger setup wizard (will create new admin).
### Performance Issues
**Symptom:** Slow generation or UI
**Diagnosis:**
1. Check active generation count
2. Review resource usage (CPU/GPU/RAM)
3. Check database size and performance
4. Review network latency
**Solutions:**
- Limit concurrent generations
- Increase hardware resources
- Optimize database (`VACUUM`, `ANALYZE`)
- Add indexes for slow queries
- Consider load balancing
### Migration Failures
**Symptom:** Database migration fails on upgrade
**Prevention:**
- Always backup before upgrading
- Test migration on copy of database
- Review migration logs
**Recovery:**
```bash
# Restore backup
cp databases/invokeai.db.backup databases/invokeai.db
# Try migration again with verbose logging
invokeai-web --log-level DEBUG
```
## Configuration Reference
### Complete Configuration Example for a Public Site
```yaml
# invokeai.yaml - Multi-user configuration
# Internal metadata - do not edit:
schema_version: 4.0.2
# Put user settings here
multiuser: true
# Server
host: "0.0.0.0"
port: 9090
# Performance
enable_partial_loading: true
precision: float16
pytorch_cuda_alloc_conf: "backend:cudaMallocAsync"
hashing_algorithm: blake3_multi
```
## Frequently Asked Questions
### How many users can InvokeAI support?
The backend will support dozens of concurrent users. However, because
the image generation queue is single-threaded, image generation tasks
are processed on a first-come, first-serve basis. This means that a
user may have to wait for all the other users' image generation jobs
to complete before their generation job starts to execute.
A future version of InvokeAI may support concurrent execution on
systems with multiple GPUs/graphics cards.
### Can I integrate with existing authentication systems?
OAuth2/OpenID Connect support is planned for a future release. Currently, InvokeAI uses its own authentication system.
### How do I audit user actions?
Full audit logging is planned for a future release. Currently, you can:
- Monitor the generation queue
- Review database changes
- Check application logs
### Can users have different model access?
Not in the current release. All users can view and use all installed models. Per-user model access is a possible enhancement.
### How do I handle user data when they leave?
Best practice:
1. Deactivate the account first
2. Transfer ownership of shared boards
3. After transition period, delete the account
4. Or keep the account deactivated for audit purposes
### What's the licensing impact of multi-user mode?
InvokeAI remains under its existing license. Multi-user mode does not change licensing terms.
## Getting Help
### Support Resources
- **Documentation**: [InvokeAI Docs](https://invoke-ai.github.io/InvokeAI/)
- **Discord**: [Join Community](https://discord.gg/ZmtBAhwWhy)
- **GitHub Issues**: [Report Problems](https://github.com/invoke-ai/InvokeAI/issues)
- **User Guide**: [For Users](user_guide.md)
- **API Guide**: [For Developers](api_guide.md)
### Reporting Issues
When reporting administrator issues, include:
- InvokeAI version
- Operating system and version
- Database size and user count
- Relevant log excerpts
- Steps to reproduce
- Expected vs actual behavior
## Additional Resources
- [User Guide](user_guide.md) - For end users
- [API Guide](api_guide.md) - For API consumers
- [Multiuser Specification](specification.md) - Technical details
---
**Need additional assistance?** Visit the [InvokeAI Discord](https://discord.gg/ZmtBAhwWhy) or file an issue on [GitHub](https://github.com/invoke-ai/InvokeAI/issues).

1224
docs/multiuser/api_guide.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,870 @@
# InvokeAI Multi-User Support - Detailed Specification
## 1. Executive Summary
This document provides a comprehensive specification for adding multi-user support to InvokeAI. The feature will enable a single InvokeAI instance to support multiple isolated users, each with their own generation settings, image boards, and workflows, while maintaining administrative controls for model management and system configuration.
## 2. Overview
### 2.1 Goals
- Enable multiple users to share a single InvokeAI instance
- Provide user isolation for personal content (boards, images, workflows, settings)
- Maintain centralized model management by administrators
- Support shared boards for collaboration
- Provide secure authentication and authorization
- Minimize impact on existing single-user installations
### 2.2 Non-Goals
- Real-time collaboration features (multiple users editing same workflow simultaneously)
- Advanced team management features (in initial release)
- Migration of existing multi-user enterprise edition data
- Support for external identity providers (in initial release, can be added later)
## 3. User Roles and Permissions
### 3.1 Administrator Role
**Capabilities:**
- Full access to all InvokeAI features
- Model management (add, delete, configure models)
- User management (create, edit, delete users)
- View and manage all users' queue sessions
- Access system configuration
- Create and manage shared boards
- Grant/revoke administrative privileges to other users
**Restrictions:**
- Cannot delete their own account if they are the last administrator
- Cannot revoke their own admin privileges if they are the last administrator
### 3.2 Regular User Role
**Capabilities:**
- Create, edit, and delete their own image boards
- Upload and manage their own assets
- Use all image generation tools (linear, canvas, upscale, workflow tabs)
- Create, edit, save, and load workflows
- Access public/shared workflows
- View and manage their own queue sessions
- Adjust personal UI preferences (theme, hotkeys, etc.)
- Access shared boards (read/write based on permissions)
- **View model configurations** (read-only access to model manager)
- **View model details, default settings, and metadata**
**Restrictions:**
- Cannot add, delete, or edit models
- **Can view but cannot modify model manager settings** (read-only access)
- Cannot reidentify, convert, or update model paths
- Cannot upload or change model thumbnail images
- Cannot save changes to model default settings
- Cannot perform bulk delete operations on models
- Cannot view or modify other users' boards, images, or workflows
- Cannot cancel or modify other users' queue sessions
- Cannot access system configuration
- Cannot manage users or permissions
### 3.3 Future Role Considerations
- **Viewer Role**: Read-only access (future enhancement)
- **Team/Group-based Permissions**: Organizational hierarchy (future enhancement)
## 4. Authentication System
### 4.1 Authentication Method
- **Primary Method**: Username and password authentication with secure password hashing
- **Password Hashing**: Use bcrypt or Argon2 for password storage
- **Session Management**: JWT tokens or secure session cookies
- **Token Expiration**: Configurable session timeout (default: 7 days for "remember me", 24 hours otherwise)
### 4.2 Initial Administrator Setup
**First-time Launch Flow:**
1. Application detects no administrator account exists
2. Displays mandatory setup dialog (cannot be skipped)
3. Prompts for:
- Administrator username (email format recommended)
- Administrator display name
- Strong password (minimum requirements enforced)
- Password confirmation
4. Stores hashed credentials in configuration
5. Creates administrator account in database
6. Proceeds to normal login screen
**Reset Capability:**
- Administrators can be reset by manually editing the config file
- Requires access to server filesystem (intentional security measure)
- Database maintains user records; config file contains root admin credentials
### 4.3 Password Requirements
- Minimum 8 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one number
- At least one special character (optional but recommended)
- Not in common password list
### 4.4 Login Flow
1. User navigates to InvokeAI URL
2. If not authenticated, redirect to login page
3. User enters username/email and password
4. Optional "Remember me" checkbox for extended session
5. Backend validates credentials
6. On success: Generate session token, redirect to application
7. On failure: Display error, allow retry with rate limiting (prevent brute force)
### 4.5 Logout Flow
- User clicks logout button
- Frontend clears session token
- Backend invalidates session (if using server-side sessions)
- Redirect to login page
### 4.6 Future Authentication Enhancements
- OAuth2/OpenID Connect support
- Two-factor authentication (2FA)
- SSO integration
- API key authentication for programmatic access
## 5. User Management
### 5.1 User Creation (Administrator)
**Flow:**
1. Administrator navigates to user management interface
2. Clicks "Add User" button
3. Enters user information:
- Email address (required, used as username)
- Display name (optional, defaults to email)
- Role (User or Administrator)
- Initial password or "Send invitation email"
4. System validates email uniqueness
5. System creates user account
6. If invitation mode:
- Generate one-time secure token
- Send email with setup link
- Link expires after 7 days
7. If direct password mode:
- Administrator provides initial password
- User must change on first login
**Invitation Email Flow:**
1. User receives email with unique link
2. Link contains secure token
3. User clicks link, redirected to setup page
4. User enters desired password
5. Token validated and consumed (single-use)
6. Account activated
7. User redirected to login page
### 5.2 User Profile Management
**User Self-Service:**
- Update display name
- Change password (requires current password)
- Update email address (requires verification)
- Manage UI preferences
- View account creation date and last login
**Administrator Actions:**
- Edit user information (name, email)
- Reset user password (generates reset link)
- Toggle administrator privileges
- Assign to groups (future feature)
- Suspend/unsuspend account
- Delete account (with data retention options)
### 5.3 Password Reset Flow
**User-Initiated (Future Enhancement):**
1. User clicks "Forgot Password" on login page
2. Enters email address
3. System sends password reset link (if email exists)
4. User clicks link, enters new password
5. Password updated, user can login
**Administrator-Initiated:**
1. Administrator selects user
2. Clicks "Send Password Reset"
3. System generates reset token and link
4. Email sent to user
5. User follows same flow as user-initiated reset
## 6. Data Model and Database Schema
### 6.1 New Tables
#### 6.1.1 users
```sql
CREATE TABLE users (
user_id TEXT NOT NULL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
display_name TEXT,
password_hash TEXT NOT NULL,
is_admin BOOLEAN NOT NULL DEFAULT FALSE,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
updated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
last_login_at DATETIME
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_is_admin ON users(is_admin);
CREATE INDEX idx_users_is_active ON users(is_active);
```
#### 6.1.2 user_sessions
```sql
CREATE TABLE user_sessions (
session_id TEXT NOT NULL PRIMARY KEY,
user_id TEXT NOT NULL,
token_hash TEXT NOT NULL,
expires_at DATETIME NOT NULL,
created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
last_activity_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
user_agent TEXT,
ip_address TEXT,
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
);
CREATE INDEX idx_user_sessions_user_id ON user_sessions(user_id);
CREATE INDEX idx_user_sessions_expires_at ON user_sessions(expires_at);
CREATE INDEX idx_user_sessions_token_hash ON user_sessions(token_hash);
```
#### 6.1.3 user_invitations
```sql
CREATE TABLE user_invitations (
invitation_id TEXT NOT NULL PRIMARY KEY,
email TEXT NOT NULL,
token_hash TEXT NOT NULL,
invited_by_user_id TEXT NOT NULL,
expires_at DATETIME NOT NULL,
used_at DATETIME,
created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
FOREIGN KEY (invited_by_user_id) REFERENCES users(user_id) ON DELETE CASCADE
);
CREATE INDEX idx_user_invitations_email ON user_invitations(email);
CREATE INDEX idx_user_invitations_token_hash ON user_invitations(token_hash);
CREATE INDEX idx_user_invitations_expires_at ON user_invitations(expires_at);
```
#### 6.1.4 shared_boards
```sql
CREATE TABLE shared_boards (
board_id TEXT NOT NULL,
user_id TEXT NOT NULL,
permission TEXT NOT NULL CHECK(permission IN ('read', 'write', 'admin')),
created_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
PRIMARY KEY (board_id, user_id),
FOREIGN KEY (board_id) REFERENCES boards(board_id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
);
CREATE INDEX idx_shared_boards_user_id ON shared_boards(user_id);
CREATE INDEX idx_shared_boards_board_id ON shared_boards(board_id);
```
### 6.2 Modified Tables
#### 6.2.1 boards
```sql
-- Add columns:
ALTER TABLE boards ADD COLUMN user_id TEXT NOT NULL DEFAULT 'system';
ALTER TABLE boards ADD COLUMN is_shared BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE boards ADD COLUMN created_by_user_id TEXT;
-- Add foreign key (requires recreation in SQLite):
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
FOREIGN KEY (created_by_user_id) REFERENCES users(user_id) ON DELETE SET NULL
-- Add indices:
CREATE INDEX idx_boards_user_id ON boards(user_id);
CREATE INDEX idx_boards_is_shared ON boards(is_shared);
```
#### 6.2.2 images
```sql
-- Add column:
ALTER TABLE images ADD COLUMN user_id TEXT NOT NULL DEFAULT 'system';
-- Add foreign key:
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
-- Add index:
CREATE INDEX idx_images_user_id ON images(user_id);
```
#### 6.2.3 workflows
```sql
-- Add columns:
ALTER TABLE workflows ADD COLUMN user_id TEXT NOT NULL DEFAULT 'system';
ALTER TABLE workflows ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT FALSE;
-- Add foreign key:
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
-- Add indices:
CREATE INDEX idx_workflows_user_id ON workflows(user_id);
CREATE INDEX idx_workflows_is_public ON workflows(is_public);
```
#### 6.2.4 session_queue
```sql
-- Add column:
ALTER TABLE session_queue ADD COLUMN user_id TEXT NOT NULL DEFAULT 'system';
-- Add foreign key:
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
-- Add index:
CREATE INDEX idx_session_queue_user_id ON session_queue(user_id);
```
#### 6.2.5 style_presets
```sql
-- Add columns:
ALTER TABLE style_presets ADD COLUMN user_id TEXT NOT NULL DEFAULT 'system';
ALTER TABLE style_presets ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT FALSE;
-- Add foreign key:
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE
-- Add indices:
CREATE INDEX idx_style_presets_user_id ON style_presets(user_id);
CREATE INDEX idx_style_presets_is_public ON style_presets(is_public);
```
### 6.3 Migration Strategy
1. Create new user tables (users, user_sessions, user_invitations, shared_boards)
2. Create default 'system' user for backward compatibility
3. Update existing data to reference 'system' user
4. Add foreign key constraints
5. Version as database migration (e.g., migration_25.py)
### 6.4 Migration for Existing Installations
- Single-user installations: Prompt to create admin account on first launch after update
- Existing data migration: Administrator can specify an arbitrary user account to hold legacy data (can be the admin account or a separate user)
- System provides UI during migration to choose destination user for existing data
## 7. API Endpoints
### 7.1 Authentication Endpoints
#### POST /api/v1/auth/setup
- Initialize first administrator account
- Only works if no admin exists
- Body: `{ email, display_name, password }`
- Response: `{ success, user }`
#### POST /api/v1/auth/login
- Authenticate user
- Body: `{ email, password, remember_me? }`
- Response: `{ token, user, expires_at }`
#### POST /api/v1/auth/logout
- Invalidate current session
- Headers: `Authorization: Bearer <token>`
- Response: `{ success }`
#### GET /api/v1/auth/me
- Get current user information
- Headers: `Authorization: Bearer <token>`
- Response: `{ user }`
#### POST /api/v1/auth/change-password
- Change current user's password
- Body: `{ current_password, new_password }`
- Headers: `Authorization: Bearer <token>`
- Response: `{ success }`
### 7.2 User Management Endpoints (Admin Only)
#### GET /api/v1/users
- List all users (paginated)
- Query params: `offset`, `limit`, `search`, `role_filter`
- Response: `{ users[], total, offset, limit }`
#### POST /api/v1/users
- Create new user
- Body: `{ email, display_name, is_admin, send_invitation?, initial_password? }`
- Response: `{ user, invitation_link? }`
#### GET /api/v1/users/{user_id}
- Get user details
- Response: `{ user }`
#### PATCH /api/v1/users/{user_id}
- Update user
- Body: `{ display_name?, is_admin?, is_active? }`
- Response: `{ user }`
#### DELETE /api/v1/users/{user_id}
- Delete user
- Query params: `delete_data` (true/false)
- Response: `{ success }`
#### POST /api/v1/users/{user_id}/reset-password
- Send password reset email
- Response: `{ success, reset_link }`
### 7.3 Shared Boards Endpoints
#### POST /api/v1/boards/{board_id}/share
- Share board with users
- Body: `{ user_ids[], permission: 'read' | 'write' | 'admin' }`
- Response: `{ success, shared_with[] }`
#### GET /api/v1/boards/{board_id}/shares
- Get board sharing information
- Response: `{ shares[] }`
#### DELETE /api/v1/boards/{board_id}/share/{user_id}
- Remove board sharing
- Response: `{ success }`
### 7.4 Modified Endpoints
All existing endpoints will be modified to:
1. Require authentication (except setup/login)
2. Filter data by current user (unless admin viewing all)
3. Enforce permissions (e.g., model management requires admin)
4. Include user context in operations
Example modifications:
- `GET /api/v1/boards` → Returns only user's boards + shared boards
- `POST /api/v1/session/queue` → Associates queue item with current user
- `GET /api/v1/queue` → Returns all items for admin, only user's items for regular users
## 8. Frontend Changes
### 8.1 New Components
#### LoginPage
- Email/password form
- "Remember me" checkbox
- Login button
- Forgot password link (future)
- Branding and welcome message
#### AdministratorSetup
- Modal dialog (cannot be dismissed)
- Administrator account creation form
- Password strength indicator
- Terms/welcome message
#### UserManagementPage (Admin only)
- User list table
- Add user button
- User actions (edit, delete, reset password)
- Search and filter
- Role toggle
#### UserProfilePage
- Display user information
- Change password form
- UI preferences
- Account details
#### BoardSharingDialog
- User picker/search
- Permission selector
- Share button
- Current shares list
### 8.2 Modified Components
#### App Root
- Add authentication check
- Redirect to login if not authenticated
- Handle session expiration
- Add global error boundary for auth errors
#### Navigation/Header
- Add user menu with logout
- Display current user name
- Admin indicator badge
#### ModelManagerTab
- Hide/disable for non-admin users
- Show "Admin only" message
#### QueuePanel
- Filter by current user (for non-admin)
- Show all with user indicators (for admin)
- Disable actions on other users' items (for non-admin)
#### BoardsPanel
- Show personal boards section
- Show shared boards section
- Add sharing controls to board actions
### 8.3 State Management
New Redux slices/zustand stores:
- `authSlice`: Current user, authentication status, token
- `usersSlice`: User list for admin interface
- `sharingSlice`: Board sharing state
Updated slices:
- `boardsSlice`: Include shared boards, ownership info
- `queueSlice`: Include user filtering
- `workflowsSlice`: Include public/private status
## 9. Configuration
### 9.1 New Config Options
Add to `InvokeAIAppConfig`:
```python
# Authentication
auth_enabled: bool = True # Enable/disable multi-user auth
session_expiry_hours: int = 24 # Default session expiration
session_expiry_hours_remember: int = 168 # "Remember me" expiration (7 days)
password_min_length: int = 8 # Minimum password length
require_strong_passwords: bool = True # Enforce password complexity
# Session tracking
enable_server_side_sessions: bool = False # Optional server-side session tracking
# Audit logging
audit_log_auth_events: bool = True # Log authentication events
audit_log_admin_actions: bool = True # Log administrative actions
# Email (optional - for invitations and password reset)
email_enabled: bool = False
smtp_host: str = ""
smtp_port: int = 587
smtp_username: str = ""
smtp_password: str = ""
smtp_from_address: str = ""
smtp_from_name: str = "InvokeAI"
# Initial admin (stored as hash)
admin_email: Optional[str] = None
admin_password_hash: Optional[str] = None
```
### 9.2 Backward Compatibility
- If `auth_enabled = False`, system runs in legacy single-user mode
- All data belongs to implicit "system" user
- No authentication required
- Smooth upgrade path for existing installations
## 10. Security Considerations
### 10.1 Password Security
- Never store passwords in plain text
- Use bcrypt or Argon2id for password hashing
- Implement proper salt generation
- Enforce password complexity requirements
- Implement rate limiting on login attempts
- Consider password breach checking (Have I Been Pwned API)
### 10.2 Session Security
- Use cryptographically secure random tokens
- Implement token rotation
- Set appropriate cookie flags (HttpOnly, Secure, SameSite)
- Implement session timeout and renewal
- Invalidate sessions on logout
- Clean up expired sessions periodically
### 10.3 Authorization
- Always verify user identity from session token (never trust client)
- Check permissions on every API call
- Implement principle of least privilege
- Validate user ownership of resources before operations
- Implement proper error messages (avoid information leakage)
### 10.4 Data Isolation
- Strict separation of user data in database queries
- Prevent SQL injection via parameterized queries
- Validate all user inputs
- Implement proper access control checks
- Audit trail for sensitive operations
### 10.5 API Security
- Implement rate limiting on sensitive endpoints
- Use HTTPS in production (enforce via config)
- Implement CSRF protection
- Validate and sanitize all inputs
- Implement proper CORS configuration
- Add security headers (CSP, X-Frame-Options, etc.)
### 10.6 Deployment Security
- Document secure deployment practices
- Recommend reverse proxy configuration (nginx, Apache)
- Provide example configurations for HTTPS
- Document firewall requirements
- Recommend network isolation strategies
## 11. Email Integration (Optional)
**Note**: Email/SMTP configuration is optional. Many administrators will not have ready access to an outgoing SMTP server. When email is not configured, the system provides fallback mechanisms by displaying setup links directly in the admin UI.
### 11.1 Email Templates
#### User Invitation
```
Subject: You've been invited to InvokeAI
Hello,
You've been invited to join InvokeAI by [Administrator Name].
Click the link below to set up your account:
[Setup Link]
This link expires in 7 days.
---
InvokeAI
```
#### Password Reset
```
Subject: Reset your InvokeAI password
Hello [User Name],
A password reset was requested for your account.
Click the link below to reset your password:
[Reset Link]
This link expires in 24 hours.
If you didn't request this, please ignore this email.
---
InvokeAI
```
### 11.2 Email Service
- Support SMTP configuration
- Use secure connection (TLS)
- Handle email failures gracefully
- Implement email queue for reliability
- Log email activities (without sensitive data)
- Provide fallback for no-email deployments (show links in admin UI)
## 12. Testing Requirements
### 12.1 Unit Tests
- Authentication service (password hashing, validation)
- Authorization checks
- Token generation and validation
- User management operations
- Shared board permissions
- Data isolation queries
### 12.2 Integration Tests
- Complete authentication flows
- User creation and invitation
- Password reset flow
- Multi-user data isolation
- Shared board access
- Session management
- Admin operations
### 12.3 Security Tests
- SQL injection prevention
- XSS prevention
- CSRF protection
- Session hijacking prevention
- Brute force protection
- Authorization bypass attempts
### 12.4 Performance Tests
- Authentication overhead
- Query performance with user filters
- Concurrent user sessions
- Database scalability with many users
## 13. Documentation Requirements
### 13.1 User Documentation
- Getting started with multi-user InvokeAI
- Login and account management
- Using shared boards
- Understanding permissions
- Troubleshooting authentication issues
### 13.2 Administrator Documentation
- Setting up multi-user InvokeAI
- User management guide
- Creating and managing shared boards
- Email configuration
- Security best practices
- Backup and restore with user data
### 13.3 Developer Documentation
- Authentication architecture
- API authentication requirements
- Adding new multi-user features
- Database schema changes
- Testing multi-user features
### 13.4 Migration Documentation
- Upgrading from single-user to multi-user
- Data migration strategies
- Rollback procedures
- Common issues and solutions
## 14. Future Enhancements
### 14.1 Phase 2 Features
- **OAuth2/OpenID Connect integration** (deferred from initial release to keep scope manageable)
- Two-factor authentication
- API keys for programmatic access
- Enhanced team/group management
- Advanced permission system (roles and capabilities)
### 14.2 Phase 3 Features
- SSO integration (SAML, LDAP)
- User quotas and limits
- Resource usage tracking
- Advanced collaboration features
- Workflow template library with permissions
- Model access controls per user/group
## 15. Success Metrics
### 15.1 Functionality Metrics
- Successful user authentication rate
- Zero unauthorized data access incidents
- All tests passing (unit, integration, security)
- API response time within acceptable limits
### 15.2 Usability Metrics
- User setup completion time < 2 minutes
- Login time < 2 seconds
- Clear error messages for all auth failures
- Positive user feedback on multi-user features
### 15.3 Security Metrics
- No critical security vulnerabilities identified
- CodeQL scan passes
- Penetration testing completed
- Security best practices followed
## 16. Risks and Mitigations
### 16.1 Technical Risks
| Risk | Impact | Probability | Mitigation |
|------|--------|-------------|------------|
| Performance degradation with user filtering | Medium | Low | Index optimization, query caching |
| Database migration failures | High | Low | Thorough testing, rollback procedures |
| Session management complexity | Medium | Medium | Use proven libraries (PyJWT), extensive testing |
| Auth bypass vulnerabilities | High | Low | Security review, penetration testing |
### 16.2 UX Risks
| Risk | Impact | Probability | Mitigation |
|------|--------|-------------|------------|
| Confusion in migration for existing users | Medium | High | Clear documentation, migration wizard |
| Friction from additional login step | Low | High | Remember me option, long session timeout |
| Complexity of admin interface | Medium | Medium | Intuitive UI design, user testing |
### 16.3 Operational Risks
| Risk | Impact | Probability | Mitigation |
|------|--------|-------------|------------|
| Email delivery failures | Low | Medium | Show links in UI, document manual methods |
| Lost admin password | High | Low | Document recovery procedure, config reset |
| User data conflicts in migration | Medium | Low | Data validation, backup requirements |
## 17. Implementation Phases
### Phase 1: Foundation (Weeks 1-2)
- Database schema design and migration
- Basic authentication service
- Password hashing and validation
- Session management
### Phase 2: Backend API (Weeks 3-4)
- Authentication endpoints
- User management endpoints
- Authorization middleware
- Update existing endpoints with auth
### Phase 3: Frontend Auth (Weeks 5-6)
- Login page and flow
- Administrator setup
- Session management
- Auth state management
### Phase 4: Multi-tenancy (Weeks 7-9)
- User isolation in all services
- Shared boards implementation
- Queue permission filtering
- Workflow public/private
### Phase 5: Admin Interface (Weeks 10-11)
- User management UI
- Board sharing UI
- Admin-specific features
- User profile page
### Phase 6: Testing & Polish (Weeks 12-13)
- Comprehensive testing
- Security audit
- Performance optimization
- Documentation
- Bug fixes
### Phase 7: Beta & Release (Week 14+)
- Beta testing with selected users
- Feedback incorporation
- Final testing
- Release preparation
- Documentation finalization
## 18. Acceptance Criteria
- [ ] Administrator can set up initial account on first launch
- [ ] Users can log in with email and password
- [ ] Users can change their password
- [ ] Administrators can create, edit, and delete users
- [ ] User data is properly isolated (boards, images, workflows)
- [ ] Shared boards work correctly with permissions
- [ ] Non-admin users cannot access model management
- [ ] Queue filtering works correctly for users and admins
- [ ] Session management works correctly (expiry, renewal, logout)
- [ ] All security tests pass
- [ ] API documentation is updated
- [ ] User and admin documentation is complete
- [ ] Migration from single-user works smoothly
- [ ] Performance is acceptable with multiple concurrent users
- [ ] Backward compatibility mode works (auth disabled)
## 19. Design Decisions
The following design decisions have been approved for implementation:
1. **OAuth2 Priority**: OAuth2/OpenID Connect integration will be a **future enhancement**. The initial release will focus on username/password authentication to keep scope manageable.
2. **Email Requirement**: Email/SMTP configuration is **optional**. Many administrators will not have ready access to an outgoing SMTP server. The system will provide fallback mechanisms (showing setup links directly in the admin UI) when email is not configured.
3. **Data Migration**: During migration from single-user to multi-user mode, the administrator will be given the **option to specify an arbitrary user account** to hold legacy data. The admin account can be used for this purpose if the administrator wishes.
4. **API Compatibility**: Authentication will be **required on all APIs**, but authentication will not be required if multi-user support is disabled (backward compatibility mode with `auth_enabled: false`).
5. **Session Storage**: The system will use **JWT tokens with optional server-side session tracking**. This provides scalability while allowing administrators to enable server-side tracking if needed.
6. **Audit Logging**: The system will **log authentication events and admin actions**. This provides accountability and security monitoring for critical operations.
## 20. Conclusion
This specification provides a comprehensive blueprint for implementing multi-user support in InvokeAI. The design prioritizes:
- **Security**: Proper authentication, authorization, and data isolation
- **Usability**: Intuitive UI, smooth migration, minimal friction
- **Scalability**: Efficient database design, performant queries
- **Maintainability**: Clean architecture, comprehensive testing
- **Flexibility**: Future enhancement paths, optional features
The phased implementation approach allows for iterative development and testing, while the detailed specifications ensure all stakeholders have clear expectations of the final system.

View File

@@ -0,0 +1,399 @@
# InvokeAI Multi-User Guide
## Overview
Multi-User mode is a recent feature (introduced in version 6.12), which allows multiple individuals to share a single InvokeAI server while keeping their work separate and organized. Each user has their own username and login password, images, assets, image boards, customization settings and workflows.
Two types of users are recognized:
* A user with **Administrator** status can add, remove and modify other users, and can install models. They also have the ability to view the full session queue and pause or kill other users' jobs.
* **Non-administrator** users can modify their own profile but not others. They also do not have the ability to install or configure models, but must ask an Administrator to do this task.
Multiple users can be granted Administrator status.
***
## Getting Started
To activate Multi-User mode, open the `INVOKEAI_ROOT/invokeai.yaml` configuration file in a text editor. Add this line anywhere in the file:
```yaml
multiuser: true
```
You may also wish to make InvokeAI available to other machines on your local LAN. Add an additional line to `invokeai.yaml`:
```yaml
host: 0.0.0.0
```
Restart the server. It will now be in multi-user mode. If you enabled
the `host` option, other users on your home or office LAN will be able
to reach it by browsing to the IP address of the machine the backend
is running on (`http://host-ip-address:9090`).
!!! tip "Do not expose InvokeAI to the internet"
It is not recommended to expose the InvokeAI host to the internet
due to security concerns.
### Initial Setup (First Time in Multi-User Mode)
If you're the first person to access a fresh InvokeAI installation in multi-user mode, you'll see the **Administrator Setup** dialog:
![Administrator Setup Screen](../../assets/multiuser/admin-setup.png)
Now
1. Enter your email address (this will be your login name)
2. Create a display name (this will be the name other users see)
3. Choose a strong password that meets the requirements:
- At least 8 characters long
- Contains uppercase letters
- Contains lowercase letters
- Contains numbers
4. Confirm your password
5. Click **Create Administrator Account**
You'll now be taken to a login screen and can enter the credentials
you just created.
### Adding and Modifying Users
If you are logged in as Administrator, you can add additional users. Click on the small "person silhouette" icon at the bottom left of the main Invoke screen and select "User Management:"
![Administrator Menu](../../assets/multiuser/admin-add-user-1.png)
This will take you to the User Management screen...
![User Management screen](../../assets/multiuser/admin-add-user-2.png)
...where you can click "Create User" to add a new user.
![Add User Screen](../../assets/multiuser/admin-add-user-3.png)
The User Management screen also allows you to:
1. Temporarily change a user's status to Inactive, preventing them from logging in to Invoke.
2. Edit a user (by clicking on the pencil icon) to change the user's display name or password.
3. Permanently delete a user.
4. Grant a user Administrator privileges.
### Command-line User Management Scripts
Administrators can also use a series of command-line scripts to add, modify, or delete users. If you use the launcher, click the ">" icon to enter the command-line interface. Otherwise, if you are a native command-line user, activate the InvokeAI environment from your terminal.
The commands are named:
* **invoke-useradd** -- add a user
* **invoke-usermod** -- modify a user
* **invoke-userdel** -- delete a user
* **invoke-userlist** -- list all users
Pass the `--help` argument to get the usage of each script. For example:
```bash
> invoke-useradd --help
usage: invoke-useradd [-h] [--root ROOT] [--email EMAIL] [--password PASSWORD] [--name NAME] [--admin]
Add a user to the InvokeAI database
options:
-h, --help show this help message and exit
--root ROOT, -r ROOT Path to the InvokeAI root directory. If omitted, the root is resolved in this order: the $INVOKEAI_ROOT environment
variable, the active virtual environment's parent directory, or $HOME/invokeai.
--email EMAIL, -e EMAIL
User email address
--password PASSWORD, -p PASSWORD
User password
--name NAME, -n NAME User display name (optional)
--admin, -a Make user an administrator
If no arguments are provided, the script will run in interactive mode.
```
***
## Logging in as a Non-Administrative User
If you are a registered user on the system, enter your email address and password to log in. The Administrator will be able to provide you with the values to use:
![Login Screen](../../assets/multiuser/user-login-1.png)
As an unprivileged user you can do pretty much anything that's allowed under single-user mode -- generating images, using LoRAs, creating and running workflows, creating image boards -- but you are restricted against installing new models, changing low-level server settings, or interfering with other users. More information on user roles is given below.
### Changing your Profile
To change your display name or profile, click on the person silhouette icon at the bottom left of the screen and choose "My Profile". This will take you to a screen that lets you change these values. At this time you can change your display name but not your login ID (ordinarily your contact email address).
***
## Understanding User Roles
In single-user mode, you have access to all features without restrictions. In multi-user mode, InvokeAI has two user roles:
### Regular User
As a regular user, you can:
- ✅ Create and manage your own image boards
- ✅ Generate images using all AI tools (Linear, Canvas, Upscale, Workflows)
- ✅ Create, save, and load your own workflows
- ✅ View your own generation queue
- ✅ Customize your UI preferences (theme, hotkeys, etc.)
- ✅ View available models (read-only access to Model Manager)
- ✅ Access shared boards (based on permissions granted to you) (FUTURE FEATURE)
- ✅ Access workflows marked as public (FUTURE FEATURE)
You cannot:
- ❌ Add, delete, or modify models
- ❌ View or modify other users' boards, images, or workflows
- ❌ Manage user accounts
- ❌ Access system configuration
- ❌ View or cancel other users' generation tasks
!!! tip "The generation queue"
When two or more users are accessing InvokeAI at the same time,
their image generation jobs will be placed on the session queue on
a first-come, first-serve basis. This means that you will have to
wait for other users' image rendering jobs to complete before
yours will start.
When another user's job is running, you will see the image
generation progress bar and a queue badge that reads `X/Y`, where
"X" is the number of jobs you have queued and "Y" is the total
number of jobs queued, including your own and others.
You can also pull up the Queue tab in order to see where your job
is in relationship to other queued tasks.
### Administrator
Administrators have all regular user capabilities, plus:
- ✅ Full model management (add, delete, configure models)
- ✅ Create and manage user accounts
- ✅ View and manage all users' generation queues
- ✅ Create and manage shared boards (FUTURE FEATURE)
- ✅ Access system configuration
- ✅ Grant or revoke admin privileges
***
## Working with Your Content in Multi-User Mode
### Image Boards
In multi-user model, Image Boards work as before. Each user can create an unlimited number of boards and organize their images and assets as they see fit. Boards are private: you cannot see a board owned by a different user.
!!! tip "Shared Boards"
InvokeAI 6.13 will add support for creating public boards that are accessible to all users.
The Administrator can see all users Image Boards and their contents.
### Going From Multi-User to Single-User mode
If an InvokeAI instance was in multiuser mode and then restarted in single user mode (by setting `multiuser: false` in the configuration file), all users' boards will be consolidated in one place. Any images that were in "Uncategorized" will be merged together into a single Uncategorized board. If, at a later date, the server is restarted in multi-user mode, the boards and images will be separated and restored to their owners.
### Workflows
In the current released version (6.12) workflows are always shared among users. Any workflow that you create will be visible to other users and vice-versa, and there is no protection against one user modifying another user's workflow.
!!! tip "Private and Shared Workflows"
InvokeAI 6.13 will provide the ability to create private and shared workflows. A private workflow can only be viewed by the user who created it. At any time, however, the user can designate the workflow *shared*, in which case it can be opened on a read-only basis by all logged-in users.
### The Generation Queue
The queue shows your pending and running generation tasks.
**Queue Features:**
- View your current and completed generations
- Cancel pending tasks
- Re-run previous generations
- Monitor progress in real-time
**Queue Isolation:**
- You will see your own queue items, as well as the items generated by
either users, but the generation parameters (e.g. prompts) for other
users' are hidden for privacy reasons.
- Administrators can view all queues for troubleshooting
- Your generations won't interfere with other users' tasks
***
## Customizing Your Experience
### Personal Preferences
Your UI preferences are saved to your account and are restored when you log in:
- **Theme**: Choose between light and dark modes
- **Hotkeys**: Customize keyboard shortcuts
- **Canvas Settings**: Default zoom, grid visibility, etc.
- **Generation Defaults**: Default values for width, height, steps, etc.
These settings are stored per-user and won't affect other users.
***
## Troubleshooting
### Cannot Log In
**Issue:** Login fails with "Incorrect email or password"
**Solutions:**
- Verify you're entering the correct email address
- Check that Caps Lock is off
- Try typing the password slowly to avoid mistakes
- Contact your administrator if you've forgotten your password
**Issue:** Login fails with "Account is disabled"
**Solution:** Contact your administrator to reactivate your account
### Session Expired
**Issue:** You're suddenly logged out and see "Session expired"
**Explanation:** Sessions expire after 24 hours (or 7 days with "remember me")
**Solution:** Simply log in again with your credentials
### Cannot Access Features
**Issue:** Features like Model Manager show "Admin privileges required"
**Explanation:** Some features are restricted to administrators
**Solution:**
- For model viewing: You can view but not modify models
- For user management: Contact an administrator
- For system configuration: Contact an administrator
### Missing Boards or Images
**Issue:** Boards or images you created are not visible
**Possible Causes:**
1. **Filter Applied:** Check if a filter is hiding content
2. **Wrong User:** Ensure you're logged in with the correct account
3. **Archived Board:** Check the "Show Archived" option
**Solution:**
- Clear any active filters
- Verify you're logged in as the right user
- Check archived items
### Slow Performance
**Issue:** Generation or UI feels slower than expected
**Possible Causes:**
- Other users generating images simultaneously
- Server resource limits
- Network latency
**Solutions:**
- Check the queue to see if others are generating
- Wait for current generations to complete
- Contact administrator if persistent
### Generation Stuck in Queue
**Issue:** Your generation is queued but not starting
**Possible Causes:**
- Server is processing other users' generations
- Server resources are fully utilized
- Technical issue with the server
**Solutions:**
- Wait for your turn in the queue
- Check if your generation is paused
- Contact administrator if stuck for extended period
***
## Frequently Asked Questions
### Can other users see my images?
No, unless you add them to a shared board (FUTURE FEATURE). All your personal boards and images are private.
### Can I share my workflows with others?
Not directly. Ask your administrator to mark workflows as public if you want to share them.
### How long do sessions last?
- 24 hours by default
- 7 days if you check "Remember me" during login
### Can I use the API with multi-user mode?
Yes, but you'll need to authenticate with a JWT token. See the [API Guide](api_guide.md) for details.
### What happens if I forget my password?
Contact your administrator. They can reset your password for you.
### Can I have multiple sessions?
Yes, you can log in from multiple devices or browsers simultaneously. All sessions will use the same account and see the same content.
### Why can't I see the Model Manager "Add Models" tab?
Regular users can see the Models tab but with read-only access. Check that you're logged in and try refreshing the page.
### How do I know if I'm an administrator?
Administrators see an "Admin" badge next to their name in the top-right corner and have access to additional features like User Management.
### Can I request admin privileges?
Yes, ask your current administrator to grant you admin
privileges. Admin privileges will give you the ability to see all
other user's boards and images, as well as to add models and change
various server-wide settings.
## Getting Help
### Support Channels
- **Administrator:** Contact your system administrator for account issues
- **Documentation:** Check the [FAQ](../faq.md) for common issues
- **Community:** Join the [Discord](https://discord.gg/ZmtBAhwWhy) for help
- **Bug Reports:** File issues on [GitHub](https://github.com/invoke-ai/InvokeAI/issues)
### Reporting Issues
When reporting an issue, include:
- Your role (regular user or administrator)
- What you were trying to do
- What happened instead
- Any error messages you saw
- Your browser and operating system
## Additional Resources
- [Administrator Guide](admin_guide.md) - For administrators managing users and the system
- [API Guide](api_guide.md) - For developers using the InvokeAI API
- [Multiuser Specification](specification.md) - Technical details about the feature
- [InvokeAI Documentation](../index.md) - Main documentation hub
---
**Need more help?** Contact your administrator or visit the [InvokeAI Discord](https://discord.gg/ZmtBAhwWhy).

View File

@@ -41,7 +41,7 @@ Nodes have a "Use Cache" option in their footer. This allows for performance imp
There are several node grouping concepts that can be examined with a narrow focus. These (and other) groupings can be pieced together to make up functional graph setups, and are important to understanding how groups of nodes work together as part of a whole. Note that the screenshots below aren't examples of complete functioning node graphs (see Examples).
### Noise
### Create Latent Noise
An initial noise tensor is necessary for the latent diffusion process. As a result, the Denoising node requires a noise node input.

View File

@@ -4,21 +4,22 @@ These are nodes that have been developed by the community, for the community. If
If you'd like to submit a node for the community, please refer to the [node creation overview](contributingNodes.md).
To use a node, add the node to the `nodes` folder found in your InvokeAI install location.
To use a node, add the node to the `nodes` folder found in your InvokeAI install location.
The suggested method is to use `git clone` to clone the repository the node is found in. This allows for easy updates of the node in the future.
The suggested method is to use `git clone` to clone the repository the node is found in. This allows for easy updates of the node in the future.
If you'd prefer, you can also just download the whole node folder from the linked repository and add it to the `nodes` folder.
If you'd prefer, you can also just download the whole node folder from the linked repository and add it to the `nodes` folder.
To use a community workflow, download the `.json` node graph file and load it into Invoke AI via the **Load Workflow** button in the Workflow Editor.
To use a community workflow, download the `.json` node graph file and load it into Invoke AI via the **Load Workflow** button in the Workflow Editor.
- Community Nodes
+ [Anamorphic Tools](#anamorphic-tools)
+ [Adapters-Linked](#adapters-linked-nodes)
+ [Autostereogram](#autostereogram-nodes)
+ [Average Images](#average-images)
+ [BiRefNet Background Removal](#birefnet-background-removal)
+ [Clean Image Artifacts After Cut](#clean-image-artifacts-after-cut)
+ [Close Color Mask](#close-color-mask)
+ [Close Color Mask](#close-color-mask)
+ [Clothing Mask](#clothing-mask)
+ [Contrast Limited Adaptive Histogram Equalization](#contrast-limited-adaptive-histogram-equalization)
+ [Curves](#curves)
@@ -34,6 +35,7 @@ To use a community workflow, download the `.json` node graph file and load it in
+ [Hand Refiner with MeshGraphormer](#hand-refiner-with-meshgraphormer)
+ [Image and Mask Composition Pack](#image-and-mask-composition-pack)
+ [Image Dominant Color](#image-dominant-color)
+ [Image Export](#image-export)
+ [Image to Character Art Image Nodes](#image-to-character-art-image-nodes)
+ [Image Picker](#image-picker)
+ [Image Resize Plus](#image-resize-plus)
@@ -51,7 +53,7 @@ To use a community workflow, download the `.json` node graph file and load it in
+ [Prompt Tools](#prompt-tools)
+ [Remote Image](#remote-image)
+ [BriaAI Background Remove](#briaai-remove-background)
+ [Remove Background](#remove-background)
+ [Remove Background](#remove-background)
+ [Retroize](#retroize)
+ [Stereogram](#stereogram-nodes)
+ [Size Stepper Nodes](#size-stepper-nodes)
@@ -81,7 +83,7 @@ To use a community workflow, download the `.json` node graph file and load it in
- `IP-Adapter-Linked` - Collects IP-Adapter info to pass to other nodes.
- `T2I-Adapter-Linked` - Collects T2I-Adapter info to pass to other nodes.
Note: These are inherited from the core nodes so any update to the core nodes should be reflected in these.
Note: These are inherited from the core nodes so any update to the core nodes should be reflected in these.
**Node Link:** https://github.com/skunkworxdark/adapters-linked-nodes
@@ -103,6 +105,20 @@ Note: These are inherited from the core nodes so any update to the core nodes sh
**Node Link:** https://github.com/JPPhoto/average-images-node
--------------------------------
### BiRefNet Background Removal
**Description:** Remove image backgrounds using BiRefNet (Bilateral Reference Network), a high-quality segmentation model. Supports multiple model variants including standard, high-resolution, matting, portrait, and specialized models for different use cases.
**Node Link:** https://github.com/veeliks/invoke_birefnet
**Output Examples**
<section>
<img src="https://raw.githubusercontent.com/veeliks/invoke_birefnet/main/.readme/example_before_removal.png" width="49%" alt="Before background removal">
<img src="https://raw.githubusercontent.com/veeliks/invoke_birefnet/main/.readme/example_after_removal.png" width="49%" alt="After background removal">
</section>
--------------------------------
### Clean Image Artifacts After Cut
@@ -216,7 +232,7 @@ This includes 3 Nodes:
**Node Link:** https://github.com/mickr777/GPT2RandomPromptMaker
**Output Examples**
**Output Examples**
Generated Prompt: An enchanted weapon will be usable by any character regardless of their alignment.
@@ -231,7 +247,7 @@ Generated Prompt: An enchanted weapon will be usable by any character regardless
**Example Node Graph:** https://github.com/mildmisery/invokeai-GridToGifNode/blob/main/Grid%20to%20Gif%20Example%20Workflow.json
**Output Examples**
**Output Examples**
<img src="https://raw.githubusercontent.com/mildmisery/invokeai-GridToGifNode/main/input.png" width="300" />
<img src="https://raw.githubusercontent.com/mildmisery/invokeai-GridToGifNode/main/output.gif" width="300" />
@@ -293,7 +309,7 @@ This includes 15 Nodes:
- *Text Mask (simple 2D)* - create and position a white on black (or black on white) line of text using any font locally available to Invoke.
**Node Link:** https://github.com/dwringer/composition-nodes
</br><img src="https://raw.githubusercontent.com/dwringer/composition-nodes/main/composition_pack_overview.jpg" width="500" />
--------------------------------
@@ -306,6 +322,23 @@ Node Link: https://github.com/VeyDlin/image-dominant-color-node
View:
</br><img src="https://raw.githubusercontent.com/VeyDlin/image-dominant-color-node/master/.readme/node.png" width="500" />
--------------------------------
### Image Export
**Description:** Export images in multiple formats (AVIF, JPEG, PNG, TIFF, WebP) with format-specific compression and quality options.
**Node Link:** https://github.com/veeliks/invoke_image_export
**Nodes:**
<section>
<img src="https://raw.githubusercontent.com/veeliks/invoke_image_export/main/.readme/node_avif.png" width="19%" alt="Save Image as AVIF">
<img src="https://raw.githubusercontent.com/veeliks/invoke_image_export/main/.readme/node_jpeg.png" width="19%" alt="Save Image as JPEG">
<img src="https://raw.githubusercontent.com/veeliks/invoke_image_export/main/.readme/node_png.png" width="19%" alt="Save Image as PNG">
<img src="https://raw.githubusercontent.com/veeliks/invoke_image_export/main/.readme/node_tiff.png" width="19%" alt="Save Image as TIFF">
<img src="https://raw.githubusercontent.com/veeliks/invoke_image_export/main/.readme/node_webp.png" width="19%" alt="Save Image as WebP">
</section>
--------------------------------
### Image to Character Art Image Nodes
@@ -352,7 +385,7 @@ View:
**Node Link:** https://github.com/helix4u/load_video_frame
**Output Example:**
**Output Example:**
<img src="https://raw.githubusercontent.com/helix4u/load_video_frame/refs/heads/main/_git_assets/dance1736978273.gif" width="500" />
--------------------------------
@@ -364,7 +397,7 @@ View:
**Example Node Graph:** https://gitlab.com/srcrr/shift3d/-/raw/main/example-workflow.json?ref_type=heads&inline=false
**Output Examples**
**Output Examples**
<img src="https://gitlab.com/srcrr/shift3d/-/raw/main/example-1.png" width="300" />
<img src="https://gitlab.com/srcrr/shift3d/-/raw/main/example-2.png" width="300" />
@@ -386,13 +419,13 @@ View:
- Option to only transfer luminance channel.
- Option to save output as grayscale
A good use case for this node is to normalize the colors of an image that has been through the tiled scaling workflow of my XYGrid Nodes.
A good use case for this node is to normalize the colors of an image that has been through the tiled scaling workflow of my XYGrid Nodes.
See full docs here: https://github.com/skunkworxdark/Prompt-tools-nodes/edit/main/README.md
**Node Link:** https://github.com/skunkworxdark/match_histogram
**Output Examples**
**Output Examples**
<img src="https://github.com/skunkworxdark/match_histogram/assets/21961335/ed12f329-a0ef-444a-9bae-129ed60d6097" />
@@ -410,12 +443,12 @@ See full docs here: https://github.com/skunkworxdark/Prompt-tools-nodes/edit/mai
- `Metadata To Bool` - Extracts Bool types from metadata
- `Metadata To Model` - Extracts model types from metadata
- `Metadata To SDXL Model` - Extracts SDXL model types from metadata
- `Metadata To LoRAs` - Extracts Loras from metadata.
- `Metadata To LoRAs` - Extracts Loras from metadata.
- `Metadata To SDXL LoRAs` - Extracts SDXL Loras from metadata
- `Metadata To ControlNets` - Extracts ControNets from metadata
- `Metadata To IP-Adapters` - Extracts IP-Adapters from metadata
- `Metadata To T2I-Adapters` - Extracts T2I-Adapters from metadata
- `Denoise Latents + Metadata` - This is an inherited version of the existing `Denoise Latents` node but with a metadata input and output.
- `Denoise Latents + Metadata` - This is an inherited version of the existing `Denoise Latents` node but with a metadata input and output.
**Node Link:** https://github.com/skunkworxdark/metadata-linked-nodes
@@ -445,7 +478,7 @@ View:
**Example Node Graph:** https://github.com/Jonseed/Ollama-Node/blob/main/Ollama-Node-Flux-example.json
**View:**
**View:**
![ollama node](https://raw.githubusercontent.com/Jonseed/Ollama-Node/a3e7cdc55e394cb89c1ea7ed54e106c212c85e8c/ollama-node-screenshot.png)
@@ -454,7 +487,7 @@ View:
<img src="https://raw.githubusercontent.com/AIrjen/OneButtonPrompt_X_InvokeAI/refs/heads/main/images/background.png" width="800" />
**Description:** an extensive suite of auto prompt generation and prompt helper nodes based on extensive logic. Get creative with the best prompt generator in the world.
**Description:** an extensive suite of auto prompt generation and prompt helper nodes based on extensive logic. Get creative with the best prompt generator in the world.
The main node generates interesting prompts based on a set of parameters. There are also some additional nodes such as Auto Negative Prompt, One Button Artify, Create Prompt Variant and other cool prompt toys to play around with.
@@ -491,14 +524,14 @@ a Text-Generation-Webui instance (might work remotely too, but I never tried it)
This node works best with SDXL models, especially as the style can be described independently of the LLM's output.
--------------------------------
### Prompt Tools
### Prompt Tools
**Description:** A set of InvokeAI nodes that add general prompt (string) manipulation tools. Designed to accompany the `Prompts From File` node and other prompt generation nodes.
1. `Prompt To File` - saves a prompt or collection of prompts to a file. one per line. There is an append/overwrite option.
2. `PTFields Collect` - Converts image generation fields into a Json format string that can be passed to Prompt to file.
2. `PTFields Collect` - Converts image generation fields into a Json format string that can be passed to Prompt to file.
3. `PTFields Expand` - Takes Json string and converts it to individual generation parameters. This can be fed from the Prompts to file node.
4. `Prompt Strength` - Formats prompt with strength like the weighted format of compel
4. `Prompt Strength` - Formats prompt with strength like the weighted format of compel
5. `Prompt Strength Combine` - Combines weighted prompts for .and()/.blend()
6. `CSV To Index String` - Gets a string from a CSV by index. Includes a Random index option
@@ -513,7 +546,7 @@ See full docs here: https://github.com/skunkworxdark/Prompt-tools-nodes/edit/mai
**Node Link:** https://github.com/skunkworxdark/Prompt-tools-nodes
**Workflow Examples**
**Workflow Examples**
<img src="https://raw.githubusercontent.com/skunkworxdark/prompt-tools/refs/heads/main/images/CSVToIndexStringNode.png"/>
@@ -648,7 +681,7 @@ Highlights/Midtones/Shadows (with LUT blur enabled):
- Generate grids of images from multiple input images
- Create XY grid images with labels from parameters
- Split images into overlapping tiles for processing (for super-resolution workflows)
- Recombine image tiles into a single output image blending the seams
- Recombine image tiles into a single output image blending the seams
The nodes include:
1. `Images To Grids` - Combine multiple images into a grid of images
@@ -661,7 +694,7 @@ See full docs here: https://github.com/skunkworxdark/XYGrid_nodes/edit/main/READ
**Node Link:** https://github.com/skunkworxdark/XYGrid_nodes
**Output Examples**
**Output Examples**
<img src="https://raw.githubusercontent.com/skunkworxdark/XYGrid_nodes/refs/heads/main/images/collage.png" />
@@ -675,7 +708,7 @@ See full docs here: https://github.com/skunkworxdark/XYGrid_nodes/edit/main/READ
**Example Workflow:** https://github.com/invoke-ai/InvokeAI/blob/docs/main/docs/workflows/Prompt_from_File.json
**Output Examples**
**Output Examples**
</br><img src="https://invoke-ai.github.io/InvokeAI/assets/invoke_ai_banner.png" width="500" />
@@ -686,5 +719,5 @@ The nodes linked have been developed and contributed by members of the Invoke AI
## Help
If you run into any issues with a node, please post in the [InvokeAI Discord](https://discord.gg/ZmtBAhwWhy).
If you run into any issues with a node, please post in the [InvokeAI Discord](https://discord.gg/ZmtBAhwWhy).

View File

@@ -0,0 +1,166 @@
"""FastAPI dependencies for authentication."""
from typing import Annotated
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.services.auth.token_service import TokenData, verify_token
from invokeai.backend.util.logging import logging
logger = logging.getLogger(__name__)
# HTTP Bearer token security scheme
security = HTTPBearer(auto_error=False)
async def get_current_user(
credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(security)],
) -> TokenData:
"""Get current authenticated user from Bearer token.
Note: This function accesses ApiDependencies.invoker.services.users directly,
which is the established pattern in this codebase. The ApiDependencies.invoker
is initialized in the FastAPI lifespan context before any requests are handled.
Args:
credentials: The HTTP authorization credentials containing the Bearer token
Returns:
TokenData containing user information from the token
Raises:
HTTPException: If token is missing, invalid, or expired (401 Unauthorized)
"""
if credentials is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
token = credentials.credentials
token_data = verify_token(token)
if token_data is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired authentication token",
headers={"WWW-Authenticate": "Bearer"},
)
# Verify user still exists and is active
user_service = ApiDependencies.invoker.services.users
user = user_service.get(token_data.user_id)
if user is None or not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User account is inactive or does not exist",
headers={"WWW-Authenticate": "Bearer"},
)
return token_data
async def get_current_user_or_default(
credentials: Annotated[HTTPAuthorizationCredentials | None, Depends(security)],
) -> TokenData:
"""Get current authenticated user from Bearer token, or return a default system user if not authenticated.
This dependency is useful for endpoints that should work in both single-user and multiuser modes.
When multiuser mode is disabled (default), this always returns a system user with admin privileges,
allowing unrestricted access to all operations.
When multiuser mode is enabled, authentication is required and this function validates the token,
returning authenticated user data or raising 401 Unauthorized if no valid credentials are provided.
Args:
credentials: The HTTP authorization credentials containing the Bearer token
Returns:
TokenData containing user information from the token, or system user in single-user mode
Raises:
HTTPException: 401 Unauthorized if in multiuser mode and credentials are missing, invalid, or user is inactive
"""
# Get configuration to check if multiuser is enabled
config = ApiDependencies.invoker.services.configuration
# In single-user mode (multiuser=False), always return system user with admin privileges
if not config.multiuser:
return TokenData(user_id="system", email="system@system.invokeai", is_admin=True)
# Multiuser mode is enabled - validate credentials
if credentials is None:
# In multiuser mode, authentication is required
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required")
token = credentials.credentials
token_data = verify_token(token)
if token_data is None:
# Invalid token in multiuser mode - reject
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired token")
# Verify user still exists and is active
user_service = ApiDependencies.invoker.services.users
user = user_service.get(token_data.user_id)
if user is None or not user.is_active:
# User doesn't exist or is inactive in multiuser mode - reject
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found or inactive")
return token_data
async def require_admin(
current_user: Annotated[TokenData, Depends(get_current_user)],
) -> TokenData:
"""Require admin role for the current user.
Args:
current_user: The current authenticated user's token data
Returns:
The token data if user is an admin
Raises:
HTTPException: If user does not have admin privileges (403 Forbidden)
"""
if not current_user.is_admin:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required")
return current_user
async def require_admin_or_default(
current_user: Annotated[TokenData, Depends(get_current_user_or_default)],
) -> TokenData:
"""Require admin role for the current user, or return default system admin in single-user mode.
This dependency is useful for admin-only endpoints that should work in both single-user and multiuser modes.
When multiuser mode is disabled (default), this always returns a system user with admin privileges.
When multiuser mode is enabled, this validates that the authenticated user has admin privileges.
Args:
current_user: The current authenticated user's token data (or default system user)
Returns:
The token data if user is an admin (or system user in single-user mode)
Raises:
HTTPException: If user does not have admin privileges (403 Forbidden) in multiuser mode
"""
if not current_user.is_admin:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin privileges required")
return current_user
# Type aliases for convenient use in route dependencies
CurrentUser = Annotated[TokenData, Depends(get_current_user)]
CurrentUserOrDefault = Annotated[TokenData, Depends(get_current_user_or_default)]
AdminUser = Annotated[TokenData, Depends(require_admin)]
AdminUserOrDefault = Annotated[TokenData, Depends(require_admin_or_default)]

View File

@@ -5,11 +5,14 @@ from logging import Logger
import torch
from invokeai.app.services.app_settings import AppSettingsService
from invokeai.app.services.auth.token_service import set_jwt_secret
from invokeai.app.services.board_image_records.board_image_records_sqlite import SqliteBoardImageRecordStorage
from invokeai.app.services.board_images.board_images_default import BoardImagesService
from invokeai.app.services.board_records.board_records_sqlite import SqliteBoardRecordStorage
from invokeai.app.services.boards.boards_default import BoardService
from invokeai.app.services.bulk_download.bulk_download_default import BulkDownloadService
from invokeai.app.services.client_state_persistence.client_state_persistence_sqlite import ClientStatePersistenceSqlite
from invokeai.app.services.config.config_default import InvokeAIAppConfig
from invokeai.app.services.download.download_default import DownloadQueueService
from invokeai.app.services.events.events_fastapievents import FastAPIEventService
@@ -39,6 +42,7 @@ from invokeai.app.services.shared.sqlite.sqlite_util import init_db
from invokeai.app.services.style_preset_images.style_preset_images_disk import StylePresetImageFileStorageDisk
from invokeai.app.services.style_preset_records.style_preset_records_sqlite import SqliteStylePresetRecordsStorage
from invokeai.app.services.urls.urls_default import LocalUrlService
from invokeai.app.services.users.users_default import UserService
from invokeai.app.services.workflow_records.workflow_records_sqlite import SqliteWorkflowRecordsStorage
from invokeai.app.services.workflow_thumbnails.workflow_thumbnails_disk import WorkflowThumbnailFileStorageDisk
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import (
@@ -48,6 +52,7 @@ from invokeai.backend.stable_diffusion.diffusion.conditioning_data import (
FLUXConditioningInfo,
SD3ConditioningInfo,
SDXLConditioningInfo,
ZImageConditioningInfo,
)
from invokeai.backend.util.logging import InvokeAILogger
from invokeai.version.invokeai_version import __version__
@@ -99,6 +104,12 @@ class ApiDependencies:
db = init_db(config=config, logger=logger, image_files=image_files)
# Initialize JWT secret from database
app_settings = AppSettingsService(db=db)
jwt_secret = app_settings.get_jwt_secret()
set_jwt_secret(jwt_secret)
logger.info("JWT secret loaded from database")
configuration = config
logger = logger
@@ -128,6 +139,7 @@ class ApiDependencies:
FLUXConditioningInfo,
SD3ConditioningInfo,
CogView4ConditioningInfo,
ZImageConditioningInfo,
],
ephemeral=True,
),
@@ -151,6 +163,8 @@ class ApiDependencies:
style_preset_records = SqliteStylePresetRecordsStorage(db=db)
style_preset_image_files = StylePresetImageFileStorageDisk(style_presets_folder / "images")
workflow_thumbnails = WorkflowThumbnailFileStorageDisk(workflow_thumbnails_folder)
client_state_persistence = ClientStatePersistenceSqlite(db=db)
users = UserService(db=db)
services = InvocationServices(
board_image_records=board_image_records,
@@ -181,6 +195,8 @@ class ApiDependencies:
style_preset_records=style_preset_records,
style_preset_image_files=style_preset_image_files,
workflow_thumbnails=workflow_thumbnails,
client_state_persistence=client_state_persistence,
users=users,
)
ApiDependencies.invoker = Invoker(services)

View File

@@ -1,7 +1,9 @@
from typing import Any
from starlette.exceptions import HTTPException
from starlette.responses import Response
from starlette.staticfiles import StaticFiles
from starlette.types import Scope
class NoCacheStaticFiles(StaticFiles):
@@ -12,6 +14,10 @@ class NoCacheStaticFiles(StaticFiles):
Static files include the javascript bundles, fonts, locales, and some images. Generated
images are not included, as they are served by a router.
This class also implements proper SPA (Single Page Application) routing by serving index.html
for any routes that don't match static files, enabling client-side routing to work correctly
in production builds.
"""
def __init__(self, *args: Any, **kwargs: Any):
@@ -26,3 +32,19 @@ class NoCacheStaticFiles(StaticFiles):
resp.headers.setdefault("Pragma", self.pragma)
resp.headers.setdefault("Expires", self.expires)
return resp
async def get_response(self, path: str, scope: Scope) -> Response:
"""
Override get_response to implement SPA routing.
When a file is not found and html mode is enabled, serve index.html instead of raising a 404.
This allows client-side routing to work correctly in SPAs.
"""
try:
return await super().get_response(path, scope)
except HTTPException as exc:
# If the file is not found (404) and html mode is enabled, serve index.html
# This allows client-side routing to handle the path
if exc.status_code == 404 and self.html:
return await super().get_response("index.html", scope)
raise

View File

@@ -1,8 +1,5 @@
import typing
from enum import Enum
from importlib.metadata import distributions
from pathlib import Path
from typing import Optional
import torch
from fastapi import Body
@@ -10,7 +7,6 @@ from fastapi.routing import APIRouter
from pydantic import BaseModel, Field
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.invocations.upscale import ESRGAN_MODELS
from invokeai.app.services.config.config_default import InvokeAIAppConfig, get_config
from invokeai.app.services.invocation_cache.invocation_cache_common import InvocationCacheStatus
from invokeai.backend.image_util.infill_methods.patchmatch import PatchMatch
@@ -27,11 +23,6 @@ class LogLevel(int, Enum):
Critical = logging.CRITICAL
class Upscaler(BaseModel):
upscaling_method: str = Field(description="Name of upscaling method")
upscaling_models: list[str] = Field(description="List of upscaling models for this method")
app_router = APIRouter(prefix="/v1/app", tags=["app"])
@@ -40,17 +31,6 @@ class AppVersion(BaseModel):
version: str = Field(description="App version")
highlights: Optional[list[str]] = Field(default=None, description="Highlights of release")
class AppConfig(BaseModel):
"""App Config Response"""
infill_methods: list[str] = Field(description="List of available infill methods")
upscaling_methods: list[Upscaler] = Field(description="List of upscaling methods")
nsfw_methods: list[str] = Field(description="List of NSFW checking methods")
watermarking_methods: list[str] = Field(description="List of invisible watermark methods")
@app_router.get("/version", operation_id="app_version", status_code=200, response_model=AppVersion)
async def get_version() -> AppVersion:
@@ -72,27 +52,9 @@ async def get_app_deps() -> dict[str, str]:
return sorted_deps
@app_router.get("/config", operation_id="get_config", status_code=200, response_model=AppConfig)
async def get_config_() -> AppConfig:
infill_methods = ["lama", "tile", "cv2", "color"] # TODO: add mosaic back
if PatchMatch.patchmatch_available():
infill_methods.append("patchmatch")
upscaling_models = []
for model in typing.get_args(ESRGAN_MODELS):
upscaling_models.append(str(Path(model).stem))
upscaler = Upscaler(upscaling_method="esrgan", upscaling_models=upscaling_models)
nsfw_methods = ["nsfw_checker"]
watermarking_methods = ["invisible_watermark"]
return AppConfig(
infill_methods=infill_methods,
upscaling_methods=[upscaler],
nsfw_methods=nsfw_methods,
watermarking_methods=watermarking_methods,
)
@app_router.get("/patchmatch_status", operation_id="get_patchmatch_status", status_code=200, response_model=bool)
async def get_patchmatch_status() -> bool:
return PatchMatch.patchmatch_available()
class InvokeAIAppConfigWithSetFields(BaseModel):

View File

@@ -0,0 +1,524 @@
"""Authentication endpoints."""
import secrets
import string
from datetime import timedelta
from typing import Annotated
from fastapi import APIRouter, Body, HTTPException, Path, status
from pydantic import BaseModel, Field, field_validator
from invokeai.app.api.auth_dependencies import AdminUser, CurrentUser
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.services.auth.token_service import TokenData, create_access_token
from invokeai.app.services.users.users_common import (
UserCreateRequest,
UserDTO,
UserUpdateRequest,
validate_email_with_special_domains,
)
auth_router = APIRouter(prefix="/v1/auth", tags=["authentication"])
# Token expiration constants (in days)
TOKEN_EXPIRATION_NORMAL = 1 # 1 day for normal login
TOKEN_EXPIRATION_REMEMBER_ME = 7 # 7 days for "remember me" login
class LoginRequest(BaseModel):
"""Request body for user login."""
email: str = Field(description="User email address")
password: str = Field(description="User password")
remember_me: bool = Field(default=False, description="Whether to extend session duration")
@field_validator("email")
@classmethod
def validate_email(cls, v: str) -> str:
"""Validate email address, allowing special-use domains."""
return validate_email_with_special_domains(v)
class LoginResponse(BaseModel):
"""Response from successful login."""
token: str = Field(description="JWT access token")
user: UserDTO = Field(description="User information")
expires_in: int = Field(description="Token expiration time in seconds")
class SetupRequest(BaseModel):
"""Request body for initial admin setup."""
email: str = Field(description="Admin email address")
display_name: str | None = Field(default=None, description="Admin display name")
password: str = Field(description="Admin password")
@field_validator("email")
@classmethod
def validate_email(cls, v: str) -> str:
"""Validate email address, allowing special-use domains."""
return validate_email_with_special_domains(v)
class SetupResponse(BaseModel):
"""Response from successful admin setup."""
success: bool = Field(description="Whether setup was successful")
user: UserDTO = Field(description="Created admin user information")
class LogoutResponse(BaseModel):
"""Response from logout."""
success: bool = Field(description="Whether logout was successful")
class SetupStatusResponse(BaseModel):
"""Response for setup status check."""
setup_required: bool = Field(description="Whether initial setup is required")
multiuser_enabled: bool = Field(description="Whether multiuser mode is enabled")
strict_password_checking: bool = Field(description="Whether strict password requirements are enforced")
@auth_router.get("/status", response_model=SetupStatusResponse)
async def get_setup_status() -> SetupStatusResponse:
"""Check if initial administrator setup is required.
Returns:
SetupStatusResponse indicating whether setup is needed and multiuser mode status
"""
config = ApiDependencies.invoker.services.configuration
# If multiuser is disabled, setup is never required
if not config.multiuser:
return SetupStatusResponse(
setup_required=False, multiuser_enabled=False, strict_password_checking=config.strict_password_checking
)
# In multiuser mode, check if an admin exists
user_service = ApiDependencies.invoker.services.users
setup_required = not user_service.has_admin()
return SetupStatusResponse(
setup_required=setup_required, multiuser_enabled=True, strict_password_checking=config.strict_password_checking
)
@auth_router.post("/login", response_model=LoginResponse)
async def login(
request: Annotated[LoginRequest, Body(description="Login credentials")],
) -> LoginResponse:
"""Authenticate user and return access token.
Args:
request: Login credentials (email and password)
Returns:
LoginResponse containing JWT token and user information
Raises:
HTTPException: 401 if credentials are invalid or user is inactive
HTTPException: 403 if multiuser mode is disabled
"""
config = ApiDependencies.invoker.services.configuration
# Check if multiuser is enabled
if not config.multiuser:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Multiuser mode is disabled. Authentication is not required in single-user mode.",
)
user_service = ApiDependencies.invoker.services.users
user = user_service.authenticate(request.email, request.password)
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="User account is disabled")
# Create token with appropriate expiration
expires_delta = timedelta(days=TOKEN_EXPIRATION_REMEMBER_ME if request.remember_me else TOKEN_EXPIRATION_NORMAL)
token_data = TokenData(
user_id=user.user_id,
email=user.email,
is_admin=user.is_admin,
)
token = create_access_token(token_data, expires_delta)
return LoginResponse(
token=token,
user=user,
expires_in=int(expires_delta.total_seconds()),
)
@auth_router.post("/logout", response_model=LogoutResponse)
async def logout(
current_user: CurrentUser,
) -> LogoutResponse:
"""Logout current user.
Currently a no-op since we use stateless JWT tokens. For token invalidation in
future implementations, consider:
- Token blacklist: Store invalidated tokens in Redis/database with expiration
- Token versioning: Add version field to user record, increment on logout
- Short-lived tokens: Use refresh token pattern with token rotation
- Session storage: Track active sessions server-side for revocation
Args:
current_user: The authenticated user (validates token)
Returns:
LogoutResponse indicating success
"""
# TODO: Implement token invalidation when server-side session management is added
# For now, this is a no-op since we use stateless JWT tokens
return LogoutResponse(success=True)
@auth_router.get("/me", response_model=UserDTO)
async def get_current_user_info(
current_user: CurrentUser,
) -> UserDTO:
"""Get current authenticated user's information.
Args:
current_user: The authenticated user's token data
Returns:
UserDTO containing user information
Raises:
HTTPException: 404 if user is not found (should not happen normally)
"""
user_service = ApiDependencies.invoker.services.users
user = user_service.get(current_user.user_id)
if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
return user
@auth_router.post("/setup", response_model=SetupResponse)
async def setup_admin(
request: Annotated[SetupRequest, Body(description="Admin account details")],
) -> SetupResponse:
"""Set up initial administrator account.
This endpoint can only be called once, when no admin user exists. It creates
the first admin user for the system.
Args:
request: Admin account details (email, display_name, password)
Returns:
SetupResponse containing the created admin user
Raises:
HTTPException: 400 if admin already exists or password is weak
HTTPException: 403 if multiuser mode is disabled
"""
config = ApiDependencies.invoker.services.configuration
# Check if multiuser is enabled
if not config.multiuser:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Multiuser mode is disabled. Admin setup is not required in single-user mode.",
)
user_service = ApiDependencies.invoker.services.users
# Check if any admin exists
if user_service.has_admin():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Administrator account already configured",
)
# Create admin user - this will validate password strength
try:
user_data = UserCreateRequest(
email=request.email,
display_name=request.display_name,
password=request.password,
is_admin=True,
)
user = user_service.create_admin(user_data, strict_password_checking=config.strict_password_checking)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
return SetupResponse(success=True, user=user)
# ---------------------------------------------------------------------------
# User management models
# ---------------------------------------------------------------------------
_PASSWORD_ALPHABET = string.ascii_letters + string.digits + string.punctuation
class AdminUserCreateRequest(BaseModel):
"""Request body for admin to create a new user."""
email: str = Field(description="User email address")
display_name: str | None = Field(default=None, description="Display name")
password: str = Field(description="User password")
is_admin: bool = Field(default=False, description="Whether user should have admin privileges")
@field_validator("email")
@classmethod
def validate_email(cls, v: str) -> str:
"""Validate email address, allowing special-use domains."""
return validate_email_with_special_domains(v)
class AdminUserUpdateRequest(BaseModel):
"""Request body for admin to update any user."""
display_name: str | None = Field(default=None, description="Display name")
password: str | None = Field(default=None, description="New password")
is_admin: bool | None = Field(default=None, description="Whether user should have admin privileges")
is_active: bool | None = Field(default=None, description="Whether user account should be active")
class UserProfileUpdateRequest(BaseModel):
"""Request body for a user to update their own profile."""
display_name: str | None = Field(default=None, description="New display name")
current_password: str | None = Field(default=None, description="Current password (required when changing password)")
new_password: str | None = Field(default=None, description="New password")
class GeneratePasswordResponse(BaseModel):
"""Response containing a generated password."""
password: str = Field(description="Generated strong password")
# ---------------------------------------------------------------------------
# User management endpoints
# ---------------------------------------------------------------------------
@auth_router.get("/generate-password", response_model=GeneratePasswordResponse)
async def generate_password(
current_user: CurrentUser,
) -> GeneratePasswordResponse:
"""Generate a strong random password.
Returns a cryptographically secure random password of 16 characters
containing uppercase, lowercase, digits, and punctuation.
"""
# Ensure the generated password always meets strength requirements:
# at least one uppercase, one lowercase, one digit, one special char.
while True:
password = "".join(secrets.choice(_PASSWORD_ALPHABET) for _ in range(16))
if (
any(c.isupper() for c in password)
and any(c.islower() for c in password)
and any(c.isdigit() for c in password)
):
return GeneratePasswordResponse(password=password)
@auth_router.get("/users", response_model=list[UserDTO])
async def list_users(
current_user: AdminUser,
) -> list[UserDTO]:
"""List all users. Requires admin privileges.
The internal 'system' user (created for backward compatibility) is excluded
from the results since it cannot be managed through this interface.
Returns:
List of all real users (system user excluded)
"""
user_service = ApiDependencies.invoker.services.users
return [u for u in user_service.list_users() if u.user_id != "system"]
@auth_router.post("/users", response_model=UserDTO, status_code=status.HTTP_201_CREATED)
async def create_user(
request: Annotated[AdminUserCreateRequest, Body(description="New user details")],
current_user: AdminUser,
) -> UserDTO:
"""Create a new user. Requires admin privileges.
Args:
request: New user details
Returns:
The created user
Raises:
HTTPException: 400 if email already exists or password is weak
"""
user_service = ApiDependencies.invoker.services.users
config = ApiDependencies.invoker.services.configuration
try:
user_data = UserCreateRequest(
email=request.email,
display_name=request.display_name,
password=request.password,
is_admin=request.is_admin,
)
return user_service.create(user_data, strict_password_checking=config.strict_password_checking)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
@auth_router.get("/users/{user_id}", response_model=UserDTO)
async def get_user(
user_id: Annotated[str, Path(description="User ID")],
current_user: AdminUser,
) -> UserDTO:
"""Get a user by ID. Requires admin privileges.
Args:
user_id: The user ID
Returns:
The user
Raises:
HTTPException: 404 if user not found
"""
user_service = ApiDependencies.invoker.services.users
user = user_service.get(user_id)
if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
return user
@auth_router.patch("/users/{user_id}", response_model=UserDTO)
async def update_user(
user_id: Annotated[str, Path(description="User ID")],
request: Annotated[AdminUserUpdateRequest, Body(description="User fields to update")],
current_user: AdminUser,
) -> UserDTO:
"""Update a user. Requires admin privileges.
Args:
user_id: The user ID
request: Fields to update
Returns:
The updated user
Raises:
HTTPException: 400 if password is weak
HTTPException: 404 if user not found
"""
user_service = ApiDependencies.invoker.services.users
config = ApiDependencies.invoker.services.configuration
try:
changes = UserUpdateRequest(
display_name=request.display_name,
password=request.password,
is_admin=request.is_admin,
is_active=request.is_active,
)
return user_service.update(user_id, changes, strict_password_checking=config.strict_password_checking)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
@auth_router.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(
user_id: Annotated[str, Path(description="User ID")],
current_user: AdminUser,
) -> None:
"""Delete a user. Requires admin privileges.
Admins can delete any user including other admins, but cannot delete the last
remaining admin.
Args:
user_id: The user ID
Raises:
HTTPException: 400 if attempting to delete the last admin
HTTPException: 404 if user not found
"""
user_service = ApiDependencies.invoker.services.users
user = user_service.get(user_id)
if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
# Prevent deleting the last active admin
if user.is_admin and user.is_active and user_service.count_admins() <= 1:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Cannot delete the last administrator",
)
try:
user_service.delete(user_id)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e
@auth_router.patch("/me", response_model=UserDTO)
async def update_current_user(
request: Annotated[UserProfileUpdateRequest, Body(description="Profile fields to update")],
current_user: CurrentUser,
) -> UserDTO:
"""Update the current user's own profile.
To change the password, both ``current_password`` and ``new_password`` must
be provided. The current password is verified before the change is applied.
Args:
request: Profile fields to update
current_user: The authenticated user
Returns:
The updated user
Raises:
HTTPException: 400 if current password is incorrect or new password is weak
HTTPException: 404 if user not found
"""
user_service = ApiDependencies.invoker.services.users
config = ApiDependencies.invoker.services.configuration
# Verify current password when attempting a password change
if request.new_password is not None:
if not request.current_password:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Current password is required to set a new password",
)
# Re-authenticate to verify the current password
user = user_service.get(current_user.user_id)
if user is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
authenticated = user_service.authenticate(user.email, request.current_password)
if authenticated is None:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Current password is incorrect",
)
try:
changes = UserUpdateRequest(
display_name=request.display_name,
password=request.new_password,
)
return user_service.update(
current_user.user_id, changes, strict_password_checking=config.strict_password_checking
)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) from e

View File

@@ -4,6 +4,7 @@ from fastapi import Body, HTTPException, Path, Query
from fastapi.routing import APIRouter
from pydantic import BaseModel, Field
from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.services.board_records.board_records_common import BoardChanges, BoardRecordOrderBy
from invokeai.app.services.boards.boards_common import BoardDTO
@@ -32,12 +33,12 @@ class DeleteBoardResult(BaseModel):
response_model=BoardDTO,
)
async def create_board(
current_user: CurrentUserOrDefault,
board_name: str = Query(description="The name of the board to create", max_length=300),
is_private: bool = Query(default=False, description="Whether the board is private"),
) -> BoardDTO:
"""Creates a board"""
"""Creates a board for the current user"""
try:
result = ApiDependencies.invoker.services.boards.create(board_name=board_name)
result = ApiDependencies.invoker.services.boards.create(board_name=board_name, user_id=current_user.user_id)
return result
except Exception:
raise HTTPException(status_code=500, detail="Failed to create board")
@@ -45,16 +46,21 @@ async def create_board(
@boards_router.get("/{board_id}", operation_id="get_board", response_model=BoardDTO)
async def get_board(
current_user: CurrentUserOrDefault,
board_id: str = Path(description="The id of board to get"),
) -> BoardDTO:
"""Gets a board"""
"""Gets a board (user must have access to it)"""
try:
result = ApiDependencies.invoker.services.boards.get_dto(board_id=board_id)
return result
except Exception:
raise HTTPException(status_code=404, detail="Board not found")
if not current_user.is_admin and result.user_id != current_user.user_id:
raise HTTPException(status_code=403, detail="Not authorized to access this board")
return result
@boards_router.patch(
"/{board_id}",
@@ -68,10 +74,19 @@ async def get_board(
response_model=BoardDTO,
)
async def update_board(
current_user: CurrentUserOrDefault,
board_id: str = Path(description="The id of board to update"),
changes: BoardChanges = Body(description="The changes to apply to the board"),
) -> BoardDTO:
"""Updates a board"""
"""Updates a board (user must have access to it)"""
try:
board = ApiDependencies.invoker.services.boards.get_dto(board_id=board_id)
except Exception:
raise HTTPException(status_code=404, detail="Board not found")
if not current_user.is_admin and board.user_id != current_user.user_id:
raise HTTPException(status_code=403, detail="Not authorized to update this board")
try:
result = ApiDependencies.invoker.services.boards.update(board_id=board_id, changes=changes)
return result
@@ -81,10 +96,19 @@ async def update_board(
@boards_router.delete("/{board_id}", operation_id="delete_board", response_model=DeleteBoardResult)
async def delete_board(
current_user: CurrentUserOrDefault,
board_id: str = Path(description="The id of board to delete"),
include_images: Optional[bool] = Query(description="Permanently delete all images on the board", default=False),
) -> DeleteBoardResult:
"""Deletes a board"""
"""Deletes a board (user must have access to it)"""
try:
board = ApiDependencies.invoker.services.boards.get_dto(board_id=board_id)
except Exception:
raise HTTPException(status_code=404, detail="Board not found")
if not current_user.is_admin and board.user_id != current_user.user_id:
raise HTTPException(status_code=403, detail="Not authorized to delete this board")
try:
if include_images is True:
deleted_images = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
@@ -121,6 +145,7 @@ async def delete_board(
response_model=Union[OffsetPaginatedResults[BoardDTO], list[BoardDTO]],
)
async def list_boards(
current_user: CurrentUserOrDefault,
order_by: BoardRecordOrderBy = Query(default=BoardRecordOrderBy.CreatedAt, description="The attribute to order by"),
direction: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The direction to order by"),
all: Optional[bool] = Query(default=None, description="Whether to list all boards"),
@@ -128,11 +153,15 @@ async def list_boards(
limit: Optional[int] = Query(default=None, description="The number of boards per page"),
include_archived: bool = Query(default=False, description="Whether or not to include archived boards in list"),
) -> Union[OffsetPaginatedResults[BoardDTO], list[BoardDTO]]:
"""Gets a list of boards"""
"""Gets a list of boards for the current user, including shared boards. Admin users see all boards."""
if all:
return ApiDependencies.invoker.services.boards.get_all(order_by, direction, include_archived)
return ApiDependencies.invoker.services.boards.get_all(
current_user.user_id, current_user.is_admin, order_by, direction, include_archived
)
elif offset is not None and limit is not None:
return ApiDependencies.invoker.services.boards.get_many(order_by, direction, offset, limit, include_archived)
return ApiDependencies.invoker.services.boards.get_many(
current_user.user_id, current_user.is_admin, order_by, direction, offset, limit, include_archived
)
else:
raise HTTPException(
status_code=400,
@@ -146,12 +175,22 @@ async def list_boards(
response_model=list[str],
)
async def list_all_board_image_names(
current_user: CurrentUserOrDefault,
board_id: str = Path(description="The id of the board or 'none' for uncategorized images"),
categories: list[ImageCategory] | None = Query(default=None, description="The categories of image to include."),
is_intermediate: bool | None = Query(default=None, description="Whether to list intermediate images."),
) -> list[str]:
"""Gets a list of images for a board"""
if board_id != "none":
try:
board = ApiDependencies.invoker.services.boards.get_dto(board_id=board_id)
except Exception:
raise HTTPException(status_code=404, detail="Board not found")
if not current_user.is_admin and board.user_id != current_user.user_id:
raise HTTPException(status_code=403, detail="Not authorized to access this board")
image_names = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
board_id,
categories,

View File

@@ -0,0 +1,62 @@
from fastapi import Body, HTTPException, Path, Query
from fastapi.routing import APIRouter
from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.backend.util.logging import logging
client_state_router = APIRouter(prefix="/v1/client_state", tags=["client_state"])
@client_state_router.get(
"/{queue_id}/get_by_key",
operation_id="get_client_state_by_key",
response_model=str | None,
)
async def get_client_state_by_key(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id (ignored, kept for backwards compatibility)"),
key: str = Query(..., description="Key to get"),
) -> str | None:
"""Gets the client state for the current user (or system user if not authenticated)"""
try:
return ApiDependencies.invoker.services.client_state_persistence.get_by_key(current_user.user_id, key)
except Exception as e:
logging.error(f"Error getting client state: {e}")
raise HTTPException(status_code=500, detail="Error getting client state")
@client_state_router.post(
"/{queue_id}/set_by_key",
operation_id="set_client_state",
response_model=str,
)
async def set_client_state(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id (ignored, kept for backwards compatibility)"),
key: str = Query(..., description="Key to set"),
value: str = Body(..., description="Stringified value to set"),
) -> str:
"""Sets the client state for the current user (or system user if not authenticated)"""
try:
return ApiDependencies.invoker.services.client_state_persistence.set_by_key(current_user.user_id, key, value)
except Exception as e:
logging.error(f"Error setting client state: {e}")
raise HTTPException(status_code=500, detail="Error setting client state")
@client_state_router.post(
"/{queue_id}/delete",
operation_id="delete_client_state",
responses={204: {"description": "Client state deleted"}},
)
async def delete_client_state(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id (ignored, kept for backwards compatibility)"),
) -> None:
"""Deletes the client state for the current user (or system user if not authenticated)"""
try:
ApiDependencies.invoker.services.client_state_persistence.delete(current_user.user_id)
except Exception as e:
logging.error(f"Error deleting client state: {e}")
raise HTTPException(status_code=500, detail="Error deleting client state")

View File

@@ -9,6 +9,7 @@ from fastapi.routing import APIRouter
from PIL import Image
from pydantic import BaseModel, Field, model_validator
from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.api.extract_metadata_from_image import extract_metadata_from_image
from invokeai.app.invocations.fields import MetadataField
@@ -61,6 +62,7 @@ class ResizeToDimensions(BaseModel):
response_model=ImageDTO,
)
async def upload_image(
current_user: CurrentUserOrDefault,
file: UploadFile,
request: Request,
response: Response,
@@ -80,7 +82,7 @@ async def upload_image(
embed=True,
),
) -> ImageDTO:
"""Uploads an image"""
"""Uploads an image for the current user"""
if not file.content_type or not file.content_type.startswith("image"):
raise HTTPException(status_code=415, detail="Not an image")
@@ -133,6 +135,7 @@ async def upload_image(
workflow=extracted_metadata.invokeai_workflow,
graph=extracted_metadata.invokeai_graph,
is_intermediate=is_intermediate,
user_id=current_user.user_id,
)
response.status_code = 201
@@ -373,6 +376,7 @@ async def get_image_urls(
response_model=OffsetPaginatedResults[ImageDTO],
)
async def list_image_dtos(
current_user: CurrentUserOrDefault,
image_origin: Optional[ResourceOrigin] = Query(default=None, description="The origin of images to list."),
categories: Optional[list[ImageCategory]] = Query(default=None, description="The categories of image to include."),
is_intermediate: Optional[bool] = Query(default=None, description="Whether to list intermediate images."),
@@ -386,10 +390,19 @@ async def list_image_dtos(
starred_first: bool = Query(default=True, description="Whether to sort by starred images first"),
search_term: Optional[str] = Query(default=None, description="The term to search for"),
) -> OffsetPaginatedResults[ImageDTO]:
"""Gets a list of image DTOs"""
"""Gets a list of image DTOs for the current user"""
image_dtos = ApiDependencies.invoker.services.images.get_many(
offset, limit, starred_first, order_dir, image_origin, categories, is_intermediate, board_id, search_term
offset,
limit,
starred_first,
order_dir,
image_origin,
categories,
is_intermediate,
board_id,
search_term,
current_user.user_id,
)
return image_dtos
@@ -567,6 +580,7 @@ async def get_bulk_download_item(
@images_router.get("/names", operation_id="get_image_names")
async def get_image_names(
current_user: CurrentUserOrDefault,
image_origin: Optional[ResourceOrigin] = Query(default=None, description="The origin of images to list."),
categories: Optional[list[ImageCategory]] = Query(default=None, description="The categories of image to include."),
is_intermediate: Optional[bool] = Query(default=None, description="Whether to list intermediate images."),
@@ -589,6 +603,8 @@ async def get_image_names(
is_intermediate=is_intermediate,
board_id=board_id,
search_term=search_term,
user_id=current_user.user_id,
is_admin=current_user.is_admin,
)
return result
except Exception:

View File

@@ -19,6 +19,7 @@ from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field
from starlette.exceptions import HTTPException
from typing_extensions import Annotated
from invokeai.app.api.auth_dependencies import AdminUserOrDefault
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.services.model_images.model_images_common import ModelImageFileNotFoundException
from invokeai.app.services.model_install.model_install_common import ModelInstallJob
@@ -27,15 +28,19 @@ from invokeai.app.services.model_records import (
ModelRecordChanges,
UnknownModelException,
)
from invokeai.app.services.orphaned_models import OrphanedModelInfo
from invokeai.app.util.suppress_output import SuppressOutput
from invokeai.backend.model_manager import BaseModelType, ModelFormat, ModelType
from invokeai.backend.model_manager.config import (
AnyModelConfig,
MainCheckpointConfig,
from invokeai.backend.model_manager.configs.factory import AnyModelConfig, ModelConfigFactory
from invokeai.backend.model_manager.configs.main import (
Main_Checkpoint_SD1_Config,
Main_Checkpoint_SD2_Config,
Main_Checkpoint_SDXL_Config,
Main_Checkpoint_SDXLRefiner_Config,
)
from invokeai.backend.model_manager.load.model_cache.cache_stats import CacheStats
from invokeai.backend.model_manager.metadata.fetch.huggingface import HuggingFaceMetadataFetch
from invokeai.backend.model_manager.metadata.metadata_base import ModelMetadataWithFiles, UnknownMetadataException
from invokeai.backend.model_manager.model_on_disk import ModelOnDisk
from invokeai.backend.model_manager.search import ModelSearch
from invokeai.backend.model_manager.starter_models import (
STARTER_BUNDLES,
@@ -44,6 +49,7 @@ from invokeai.backend.model_manager.starter_models import (
StarterModelBundle,
StarterModelWithoutDependencies,
)
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType
model_manager_router = APIRouter(prefix="/v2/models", tags=["model_manager"])
@@ -144,6 +150,28 @@ async def list_model_records(
return ModelsList(models=found_models)
@model_manager_router.get(
"/missing",
operation_id="list_missing_models",
responses={200: {"description": "List of models with missing files"}},
)
async def list_missing_models() -> ModelsList:
"""Get models whose files are missing from disk.
These are models that have database entries but their corresponding
weight files have been deleted externally (not via Model Manager).
"""
record_store = ApiDependencies.invoker.services.model_manager.store
models_path = ApiDependencies.invoker.services.configuration.models_path
missing_models: list[AnyModelConfig] = []
for model_config in record_store.all_models():
if not (models_path / model_config.path).resolve().exists():
missing_models.append(model_config)
return ModelsList(models=missing_models)
@model_manager_router.get(
"/get_by_attrs",
operation_id="get_model_records_by_attrs",
@@ -165,6 +193,23 @@ async def get_model_records_by_attrs(
return configs[0]
@model_manager_router.get(
"/get_by_hash",
operation_id="get_model_records_by_hash",
response_model=AnyModelConfig,
)
async def get_model_records_by_hash(
hash: str = Query(description="The hash of the model"),
) -> AnyModelConfig:
"""Gets a model by its hash. This is useful for recalling models that were deleted and reinstalled,
as the hash remains stable across reinstallations while the key (UUID) changes."""
configs = ApiDependencies.invoker.services.model_manager.store.search_by_hash(hash)
if not configs:
raise HTTPException(status_code=404, detail="No model found with this hash")
return configs[0]
@model_manager_router.get(
"/i/{key}",
operation_id="get_model_record",
@@ -188,6 +233,52 @@ async def get_model_record(
raise HTTPException(status_code=404, detail=str(e))
@model_manager_router.post(
"/i/{key}/reidentify",
operation_id="reidentify_model",
responses={
200: {
"description": "The model configuration was retrieved successfully",
"content": {"application/json": {"example": example_model_config}},
},
400: {"description": "Bad request"},
404: {"description": "The model could not be found"},
},
)
async def reidentify_model(
key: Annotated[str, Path(description="Key of the model to reidentify.")],
current_admin: AdminUserOrDefault,
) -> AnyModelConfig:
"""Attempt to reidentify a model by re-probing its weights file."""
try:
config = ApiDependencies.invoker.services.model_manager.store.get_model(key)
models_path = ApiDependencies.invoker.services.configuration.models_path
if pathlib.Path(config.path).is_relative_to(models_path):
model_path = pathlib.Path(config.path)
else:
model_path = models_path / config.path
mod = ModelOnDisk(model_path)
result = ModelConfigFactory.from_model_on_disk(mod)
if result.config is None:
raise InvalidModelException("Unable to identify model format")
# Retain user-editable fields from the original config
result.config.path = config.path
result.config.key = config.key
result.config.name = config.name
result.config.description = config.description
result.config.cover_image = config.cover_image
if hasattr(config, "trigger_phrases") and hasattr(result.config, "trigger_phrases"):
result.config.trigger_phrases = config.trigger_phrases
result.config.source = config.source
result.config.source_type = config.source_type
new_config = ApiDependencies.invoker.services.model_manager.store.replace_model(config.key, result.config)
return new_config
except UnknownModelException as e:
raise HTTPException(status_code=404, detail=str(e))
class FoundModel(BaseModel):
path: str = Field(description="Path to the model")
is_installed: bool = Field(description="Whether or not the model is already installed")
@@ -235,9 +326,10 @@ async def scan_for_models(
found_model = FoundModel(path=path, is_installed=is_installed)
scan_results.append(found_model)
except Exception as e:
error_type = type(e).__name__
raise HTTPException(
status_code=500,
detail=f"An error occurred while searching the directory: {e}",
detail=f"An error occurred while searching the directory: {error_type}",
)
return scan_results
@@ -293,14 +385,13 @@ async def get_hugging_face_models(
async def update_model_record(
key: Annotated[str, Path(description="Unique key of model")],
changes: Annotated[ModelRecordChanges, Body(description="Model config", examples=[example_model_input])],
current_admin: AdminUserOrDefault,
) -> AnyModelConfig:
"""Update a model's config."""
logger = ApiDependencies.invoker.services.logger
record_store = ApiDependencies.invoker.services.model_manager.store
installer = ApiDependencies.invoker.services.model_manager.install
try:
record_store.update_model(key, changes=changes)
config = installer.sync_model_path(key)
config = record_store.update_model(key, changes=changes, allow_class_change=True)
config = add_cover_image_to_model_config(config, ApiDependencies)
logger.info(f"Updated model: {key}")
except UnknownModelException as e:
@@ -357,6 +448,7 @@ async def get_model_image(
async def update_model_image(
key: Annotated[str, Path(description="Unique key of model")],
image: UploadFile,
current_admin: AdminUserOrDefault,
) -> None:
if not image.content_type or not image.content_type.startswith("image"):
raise HTTPException(status_code=415, detail="Not an image")
@@ -390,6 +482,7 @@ async def update_model_image(
status_code=204,
)
async def delete_model(
current_admin: AdminUserOrDefault,
key: str = Path(description="Unique key of model to remove from model registry."),
) -> Response:
"""
@@ -410,6 +503,60 @@ async def delete_model(
raise HTTPException(status_code=404, detail=str(e))
class BulkDeleteModelsRequest(BaseModel):
"""Request body for bulk model deletion."""
keys: List[str] = Field(description="List of model keys to delete")
class BulkDeleteModelsResponse(BaseModel):
"""Response body for bulk model deletion."""
deleted: List[str] = Field(description="List of successfully deleted model keys")
failed: List[dict] = Field(description="List of failed deletions with error messages")
@model_manager_router.post(
"/i/bulk_delete",
operation_id="bulk_delete_models",
responses={
200: {"description": "Models deleted (possibly with some failures)"},
},
status_code=200,
)
async def bulk_delete_models(
current_admin: AdminUserOrDefault,
request: BulkDeleteModelsRequest = Body(description="List of model keys to delete"),
) -> BulkDeleteModelsResponse:
"""
Delete multiple model records from database.
The configuration records will be removed. The corresponding weights files will be
deleted as well if they reside within the InvokeAI "models" directory.
Returns a list of successfully deleted keys and failed deletions with error messages.
"""
logger = ApiDependencies.invoker.services.logger
installer = ApiDependencies.invoker.services.model_manager.install
deleted = []
failed = []
for key in request.keys:
try:
installer.delete(key)
deleted.append(key)
logger.info(f"Deleted model: {key}")
except UnknownModelException as e:
logger.error(f"Failed to delete model {key}: {str(e)}")
failed.append({"key": key, "error": str(e)})
except Exception as e:
logger.error(f"Failed to delete model {key}: {str(e)}")
failed.append({"key": key, "error": str(e)})
logger.info(f"Bulk delete completed: {len(deleted)} deleted, {len(failed)} failed")
return BulkDeleteModelsResponse(deleted=deleted, failed=failed)
@model_manager_router.delete(
"/i/{key}/image",
operation_id="delete_model_image",
@@ -420,6 +567,7 @@ async def delete_model(
status_code=204,
)
async def delete_model_image(
current_admin: AdminUserOrDefault,
key: str = Path(description="Unique key of model image to remove from model_images directory."),
) -> None:
logger = ApiDependencies.invoker.services.logger
@@ -445,6 +593,7 @@ async def delete_model_image(
status_code=201,
)
async def install_model(
current_admin: AdminUserOrDefault,
source: str = Query(description="Model source to install, can be a local path, repo_id, or remote URL"),
inplace: Optional[bool] = Query(description="Whether or not to install a local model in place", default=False),
access_token: Optional[str] = Query(description="access token for the remote resource", default=None),
@@ -515,6 +664,7 @@ async def install_model(
response_class=HTMLResponse,
)
async def install_hugging_face_model(
current_admin: AdminUserOrDefault,
source: str = Query(description="HuggingFace repo_id to install"),
) -> HTMLResponse:
"""Install a Hugging Face model using a string identifier."""
@@ -643,6 +793,7 @@ async def list_model_installs() -> List[ModelInstallJob]:
* "waiting" -- Job is waiting in the queue to run
* "downloading" -- Model file(s) are downloading
* "running" -- Model has downloaded and the model probing and registration process is running
* "paused" -- Job is paused and can be resumed
* "completed" -- Installation completed successfully
* "error" -- An error occurred. Details will be in the "error_type" and "error" fields.
* "cancelled" -- Job was cancelled before completion.
@@ -686,7 +837,10 @@ async def get_model_install_job(id: int = Path(description="Model install id"))
},
status_code=201,
)
async def cancel_model_install_job(id: int = Path(description="Model install job ID")) -> None:
async def cancel_model_install_job(
current_admin: AdminUserOrDefault,
id: int = Path(description="Model install job ID"),
) -> None:
"""Cancel the model install job(s) corresponding to the given job ID."""
installer = ApiDependencies.invoker.services.model_manager.install
try:
@@ -696,6 +850,89 @@ async def cancel_model_install_job(id: int = Path(description="Model install job
installer.cancel_job(job)
@model_manager_router.post(
"/install/{id}/pause",
operation_id="pause_model_install_job",
responses={
201: {"description": "The job was paused successfully"},
415: {"description": "No such job"},
},
status_code=201,
)
async def pause_model_install_job(id: int = Path(description="Model install job ID")) -> ModelInstallJob:
"""Pause the model install job corresponding to the given job ID."""
installer = ApiDependencies.invoker.services.model_manager.install
try:
job = installer.get_job_by_id(id)
except ValueError as e:
raise HTTPException(status_code=415, detail=str(e))
installer.pause_job(job)
return job
@model_manager_router.post(
"/install/{id}/resume",
operation_id="resume_model_install_job",
responses={
201: {"description": "The job was resumed successfully"},
415: {"description": "No such job"},
},
status_code=201,
)
async def resume_model_install_job(id: int = Path(description="Model install job ID")) -> ModelInstallJob:
"""Resume a paused model install job corresponding to the given job ID."""
installer = ApiDependencies.invoker.services.model_manager.install
try:
job = installer.get_job_by_id(id)
except ValueError as e:
raise HTTPException(status_code=415, detail=str(e))
installer.resume_job(job)
return job
@model_manager_router.post(
"/install/{id}/restart_failed",
operation_id="restart_failed_model_install_job",
responses={
201: {"description": "Failed files restarted successfully"},
415: {"description": "No such job"},
},
status_code=201,
)
async def restart_failed_model_install_job(id: int = Path(description="Model install job ID")) -> ModelInstallJob:
"""Restart failed or non-resumable file downloads for the given job."""
installer = ApiDependencies.invoker.services.model_manager.install
try:
job = installer.get_job_by_id(id)
except ValueError as e:
raise HTTPException(status_code=415, detail=str(e))
installer.restart_failed(job)
return job
@model_manager_router.post(
"/install/{id}/restart_file",
operation_id="restart_model_install_file",
responses={
201: {"description": "File restarted successfully"},
415: {"description": "No such job"},
},
status_code=201,
)
async def restart_model_install_file(
id: int = Path(description="Model install job ID"),
file_source: AnyHttpUrl = Body(description="File download URL to restart"),
) -> ModelInstallJob:
"""Restart a specific file download for the given job."""
installer = ApiDependencies.invoker.services.model_manager.install
try:
job = installer.get_job_by_id(id)
except ValueError as e:
raise HTTPException(status_code=415, detail=str(e))
installer.restart_file(job, str(file_source))
return job
@model_manager_router.delete(
"/install",
operation_id="prune_model_install_jobs",
@@ -704,7 +941,7 @@ async def cancel_model_install_job(id: int = Path(description="Model install job
400: {"description": "Bad request"},
},
)
async def prune_model_install_jobs() -> Response:
async def prune_model_install_jobs(current_admin: AdminUserOrDefault) -> Response:
"""Prune all completed and errored jobs from the install job list."""
ApiDependencies.invoker.services.model_manager.install.prune_jobs()
return Response(status_code=204)
@@ -724,6 +961,7 @@ async def prune_model_install_jobs() -> Response:
},
)
async def convert_model(
current_admin: AdminUserOrDefault,
key: str = Path(description="Unique key of the safetensors main model to convert to diffusers format."),
) -> AnyModelConfig:
"""
@@ -743,9 +981,18 @@ async def convert_model(
logger.error(str(e))
raise HTTPException(status_code=424, detail=str(e))
if not isinstance(model_config, MainCheckpointConfig):
logger.error(f"The model with key {key} is not a main checkpoint model.")
raise HTTPException(400, f"The model with key {key} is not a main checkpoint model.")
if not isinstance(
model_config,
(
Main_Checkpoint_SD1_Config,
Main_Checkpoint_SD2_Config,
Main_Checkpoint_SDXL_Config,
Main_Checkpoint_SDXLRefiner_Config,
),
):
msg = f"The model with key {key} is not a main SD 1/2/XL checkpoint model."
logger.error(msg)
raise HTTPException(400, msg)
with TemporaryDirectory(dir=ApiDependencies.invoker.services.configuration.models_path) as tmpdir:
convert_path = pathlib.Path(tmpdir) / pathlib.Path(model_config.path).stem
@@ -806,15 +1053,48 @@ class StarterModelResponse(BaseModel):
def get_is_installed(
starter_model: StarterModel | StarterModelWithoutDependencies, installed_models: list[AnyModelConfig]
) -> bool:
from invokeai.backend.model_manager.taxonomy import ModelType
for model in installed_models:
# Check if source matches exactly
if model.source == starter_model.source:
return True
# Check if name (or previous names), base and type match
if (
(model.name == starter_model.name or model.name in starter_model.previous_names)
and model.base == starter_model.base
and model.type == starter_model.type
):
return True
# Special handling for Qwen3Encoder models - check by type and variant
# This allows renamed models to still be detected as installed
if starter_model.type == ModelType.Qwen3Encoder:
from invokeai.backend.model_manager.taxonomy import Qwen3VariantType
# Determine expected variant from source pattern
expected_variant: Qwen3VariantType | None = None
if "klein-9B" in starter_model.source or "qwen3_8b" in starter_model.source.lower():
expected_variant = Qwen3VariantType.Qwen3_8B
elif (
"klein-4B" in starter_model.source
or "qwen3_4b" in starter_model.source.lower()
or "Z-Image" in starter_model.source
):
expected_variant = Qwen3VariantType.Qwen3_4B
if expected_variant is not None:
for model in installed_models:
if model.type == ModelType.Qwen3Encoder and hasattr(model, "variant"):
model_variant = model.variant
# Handle both enum and string values
if isinstance(model_variant, Qwen3VariantType):
if model_variant == expected_variant:
return True
elif isinstance(model_variant, str):
if model_variant == expected_variant.value:
return True
return False
@@ -863,7 +1143,7 @@ async def get_stats() -> Optional[CacheStats]:
operation_id="empty_model_cache",
status_code=200,
)
async def empty_model_cache() -> None:
async def empty_model_cache(current_admin: AdminUserOrDefault) -> None:
"""Drop all models from the model cache to free RAM/VRAM. 'Locked' models that are in active use will not be dropped."""
# Request 1000GB of room in order to force the cache to drop all models.
ApiDependencies.invoker.services.logger.info("Emptying model cache.")
@@ -880,11 +1160,11 @@ class HFTokenHelper:
@classmethod
def get_status(cls) -> HFTokenStatus:
try:
if huggingface_hub.get_token_permission(huggingface_hub.get_token()):
# Valid token!
return HFTokenStatus.VALID
# No token set
return HFTokenStatus.INVALID
token = huggingface_hub.get_token()
if not token:
return HFTokenStatus.INVALID
huggingface_hub.whoami(token=token)
return HFTokenStatus.VALID
except Exception:
return HFTokenStatus.UNKNOWN
@@ -913,6 +1193,7 @@ async def get_hf_login_status() -> HFTokenStatus:
@model_manager_router.post("/hf_login", operation_id="do_hf_login", response_model=HFTokenStatus)
async def do_hf_login(
current_admin: AdminUserOrDefault,
token: str = Body(description="Hugging Face token to use for login", embed=True),
) -> HFTokenStatus:
HFTokenHelper.set_token(token)
@@ -925,5 +1206,81 @@ async def do_hf_login(
@model_manager_router.delete("/hf_login", operation_id="reset_hf_token", response_model=HFTokenStatus)
async def reset_hf_token() -> HFTokenStatus:
async def reset_hf_token(current_admin: AdminUserOrDefault) -> HFTokenStatus:
return HFTokenHelper.reset_token()
# Orphaned Models Management Routes
class DeleteOrphanedModelsRequest(BaseModel):
"""Request to delete specific orphaned model directories."""
paths: list[str] = Field(description="List of relative paths to delete")
class DeleteOrphanedModelsResponse(BaseModel):
"""Response from deleting orphaned models."""
deleted: list[str] = Field(description="Paths that were successfully deleted")
errors: dict[str, str] = Field(description="Paths that had errors, with error messages")
@model_manager_router.get(
"/sync/orphaned",
operation_id="get_orphaned_models",
response_model=list[OrphanedModelInfo],
)
async def get_orphaned_models() -> list[OrphanedModelInfo]:
"""Find orphaned model directories.
Orphaned models are directories in the models folder that contain model files
but are not referenced in the database. This can happen when models are deleted
from the database but the files remain on disk.
Returns:
List of orphaned model directory information
"""
from invokeai.app.services.orphaned_models import OrphanedModelsService
# Access the database through the model records service
model_records_service = ApiDependencies.invoker.services.model_manager.store
service = OrphanedModelsService(
config=ApiDependencies.invoker.services.configuration,
db=model_records_service._db, # Access the database from model records service
)
return service.find_orphaned_models()
@model_manager_router.delete(
"/sync/orphaned",
operation_id="delete_orphaned_models",
response_model=DeleteOrphanedModelsResponse,
)
async def delete_orphaned_models(request: DeleteOrphanedModelsRequest) -> DeleteOrphanedModelsResponse:
"""Delete specified orphaned model directories.
Args:
request: Request containing list of relative paths to delete
Returns:
Response indicating which paths were deleted and which had errors
"""
from invokeai.app.services.orphaned_models import OrphanedModelsService
# Access the database through the model records service
model_records_service = ApiDependencies.invoker.services.model_manager.store
service = OrphanedModelsService(
config=ApiDependencies.invoker.services.configuration,
db=model_records_service._db, # Access the database from model records service
)
results = service.delete_orphaned_models(request.paths)
# Separate successful deletions from errors
deleted = [path for path, status in results.items() if status == "deleted"]
errors = {path: status for path, status in results.items() if status != "deleted"}
return DeleteOrphanedModelsResponse(deleted=deleted, errors=errors)

View File

@@ -0,0 +1,458 @@
"""Router for updating recallable parameters on the frontend."""
import json
from typing import Any, Literal, Optional
from fastapi import Body, HTTPException, Path
from fastapi.routing import APIRouter
from pydantic import BaseModel, ConfigDict, Field
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.backend.image_util.controlnet_processor import process_controlnet_image
from invokeai.backend.model_manager.taxonomy import ModelType
recall_parameters_router = APIRouter(prefix="/v1/recall", tags=["recall"])
class LoRARecallParameter(BaseModel):
"""LoRA configuration for recall"""
model_name: str = Field(description="The name of the LoRA model")
weight: float = Field(default=0.75, ge=-10, le=10, description="The weight for the LoRA")
is_enabled: bool = Field(default=True, description="Whether the LoRA is enabled")
class ControlNetRecallParameter(BaseModel):
"""ControlNet configuration for recall"""
model_name: str = Field(description="The name of the ControlNet/T2I Adapter/Control LoRA model")
image_name: Optional[str] = Field(default=None, description="The filename of the control image in outputs/images")
weight: float = Field(default=1.0, ge=-1, le=2, description="The weight for the control adapter")
begin_step_percent: Optional[float] = Field(
default=None, ge=0, le=1, description="When the control adapter is first applied (% of total steps)"
)
end_step_percent: Optional[float] = Field(
default=None, ge=0, le=1, description="When the control adapter is last applied (% of total steps)"
)
control_mode: Optional[Literal["balanced", "more_prompt", "more_control"]] = Field(
default=None, description="The control mode (ControlNet only)"
)
class IPAdapterRecallParameter(BaseModel):
"""IP Adapter configuration for recall"""
model_name: str = Field(description="The name of the IP Adapter model")
image_name: Optional[str] = Field(default=None, description="The filename of the reference image in outputs/images")
weight: float = Field(default=1.0, ge=-1, le=2, description="The weight for the IP Adapter")
begin_step_percent: Optional[float] = Field(
default=None, ge=0, le=1, description="When the IP Adapter is first applied (% of total steps)"
)
end_step_percent: Optional[float] = Field(
default=None, ge=0, le=1, description="When the IP Adapter is last applied (% of total steps)"
)
method: Optional[Literal["full", "style", "composition"]] = Field(default=None, description="The IP Adapter method")
image_influence: Optional[Literal["lowest", "low", "medium", "high", "highest"]] = Field(
default=None, description="FLUX Redux image influence (if model is flux_redux)"
)
class RecallParameter(BaseModel):
"""Request model for updating recallable parameters."""
model_config = ConfigDict(extra="forbid")
# Prompts
positive_prompt: Optional[str] = Field(None, description="Positive prompt text")
negative_prompt: Optional[str] = Field(None, description="Negative prompt text")
# Model configuration
model: Optional[str] = Field(None, description="Main model name/identifier")
refiner_model: Optional[str] = Field(None, description="Refiner model name/identifier")
vae_model: Optional[str] = Field(None, description="VAE model name/identifier")
scheduler: Optional[str] = Field(None, description="Scheduler name")
# Generation parameters
steps: Optional[int] = Field(None, ge=1, description="Number of generation steps")
refiner_steps: Optional[int] = Field(None, ge=0, description="Number of refiner steps")
cfg_scale: Optional[float] = Field(None, description="CFG scale for guidance")
cfg_rescale_multiplier: Optional[float] = Field(None, description="CFG rescale multiplier")
refiner_cfg_scale: Optional[float] = Field(None, description="Refiner CFG scale")
guidance: Optional[float] = Field(None, description="Guidance scale")
# Image parameters
width: Optional[int] = Field(None, ge=64, description="Image width in pixels")
height: Optional[int] = Field(None, ge=64, description="Image height in pixels")
seed: Optional[int] = Field(None, ge=0, description="Random seed")
# Advanced parameters
denoise_strength: Optional[float] = Field(None, ge=0, le=1, description="Denoising strength")
refiner_denoise_start: Optional[float] = Field(None, ge=0, le=1, description="Refiner denoising start")
clip_skip: Optional[int] = Field(None, ge=0, description="CLIP skip layers")
seamless_x: Optional[bool] = Field(None, description="Enable seamless X tiling")
seamless_y: Optional[bool] = Field(None, description="Enable seamless Y tiling")
# Refiner aesthetics
refiner_positive_aesthetic_score: Optional[float] = Field(None, description="Refiner positive aesthetic score")
refiner_negative_aesthetic_score: Optional[float] = Field(None, description="Refiner negative aesthetic score")
# LoRAs, ControlNets, and IP Adapters
loras: Optional[list[LoRARecallParameter]] = Field(None, description="List of LoRAs with their weights")
control_layers: Optional[list[ControlNetRecallParameter]] = Field(
None, description="List of control adapters (ControlNet, T2I Adapter, Control LoRA) with their settings"
)
ip_adapters: Optional[list[IPAdapterRecallParameter]] = Field(
None, description="List of IP Adapters with their settings"
)
def resolve_model_name_to_key(model_name: str, model_type: ModelType = ModelType.Main) -> Optional[str]:
"""
Look up a model by name and return its key.
Args:
model_name: The name of the model to look up
model_type: The type of model to search for (default: Main)
Returns:
The key of the first matching model, or None if not found.
"""
logger = ApiDependencies.invoker.services.logger
try:
models = ApiDependencies.invoker.services.model_manager.store.search_by_attr(
model_name=model_name, model_type=model_type
)
if models:
logger.info(f"Resolved {model_type.value} model name '{model_name}' to key '{models[0].key}'")
return models[0].key
logger.warning(f"Could not find {model_type.value} model with name '{model_name}'")
return None
except Exception as e:
logger.error(f"Exception during {model_type.value} model lookup: {e}", exc_info=True)
return None
def load_image_file(image_name: str) -> Optional[dict[str, Any]]:
"""
Load an image from the outputs/images directory.
Args:
image_name: The filename of the image in outputs/images
Returns:
A dictionary with image_name, width, and height, or None if the image cannot be found
"""
logger = ApiDependencies.invoker.services.logger
try:
# Prefer using the image_files service to validate & open images
image_files = ApiDependencies.invoker.services.image_files
# Resolve a safe path inside outputs
image_path = image_files.get_path(image_name)
if not image_files.validate_path(str(image_path)):
logger.warning(f"Image file not found: {image_name} (searched in {image_path.parent})")
return None
# Open the image via service to leverage caching
pil_image = image_files.get(image_name)
width, height = pil_image.size
logger.info(f"Found image file: {image_name} ({width}x{height})")
return {"image_name": image_name, "width": width, "height": height}
except Exception as e:
logger.warning(f"Error loading image file {image_name}: {e}")
return None
def resolve_lora_models(loras: list[LoRARecallParameter]) -> list[dict[str, Any]]:
"""
Resolve LoRA model names to keys and build configuration list.
Args:
loras: List of LoRA recall parameters
Returns:
List of resolved LoRA configurations with model keys
"""
logger = ApiDependencies.invoker.services.logger
resolved_loras = []
for lora in loras:
model_key = resolve_model_name_to_key(lora.model_name, ModelType.LoRA)
if model_key:
resolved_loras.append({"model_key": model_key, "weight": lora.weight, "is_enabled": lora.is_enabled})
else:
logger.warning(f"Skipping LoRA '{lora.model_name}' - model not found")
return resolved_loras
def resolve_control_models(control_layers: list[ControlNetRecallParameter]) -> list[dict[str, Any]]:
"""
Resolve control adapter model names to keys and build configuration list.
Tries to resolve as ControlNet, T2I Adapter, or Control LoRA in that order.
Args:
control_layers: List of control adapter recall parameters
Returns:
List of resolved control adapter configurations with model keys
"""
logger = ApiDependencies.invoker.services.logger
services = ApiDependencies.invoker.services
resolved_controls = []
for control in control_layers:
model_key = None
# Try ControlNet first
model_key = resolve_model_name_to_key(control.model_name, ModelType.ControlNet)
if not model_key:
# Try T2I Adapter
model_key = resolve_model_name_to_key(control.model_name, ModelType.T2IAdapter)
if not model_key:
# Try Control LoRA (also uses LoRA type)
model_key = resolve_model_name_to_key(control.model_name, ModelType.LoRA)
if model_key:
config: dict[str, Any] = {"model_key": model_key, "weight": control.weight}
if control.image_name is not None:
image_data = load_image_file(control.image_name)
if image_data:
config["image"] = image_data
# Try to process the image using the model's default processor
processed_image_data = process_controlnet_image(control.image_name, model_key, services)
if processed_image_data:
config["processed_image"] = processed_image_data
logger.info(f"Added processed image for control adapter {control.model_name}")
else:
logger.warning(f"Could not load image for control adapter: {control.image_name}")
if control.begin_step_percent is not None:
config["begin_step_percent"] = control.begin_step_percent
if control.end_step_percent is not None:
config["end_step_percent"] = control.end_step_percent
if control.control_mode is not None:
config["control_mode"] = control.control_mode
resolved_controls.append(config)
else:
logger.warning(f"Skipping control adapter '{control.model_name}' - model not found")
return resolved_controls
def resolve_ip_adapter_models(ip_adapters: list[IPAdapterRecallParameter]) -> list[dict[str, Any]]:
"""
Resolve IP Adapter model names to keys and build configuration list.
Args:
ip_adapters: List of IP Adapter recall parameters
Returns:
List of resolved IP Adapter configurations with model keys
"""
logger = ApiDependencies.invoker.services.logger
resolved_adapters = []
for adapter in ip_adapters:
# Try resolving as IP Adapter; if not found, try FLUX Redux
model_key = resolve_model_name_to_key(adapter.model_name, ModelType.IPAdapter)
if not model_key:
model_key = resolve_model_name_to_key(adapter.model_name, ModelType.FluxRedux)
if model_key:
config: dict[str, Any] = {
"model_key": model_key,
# Always include weight; ignored by FLUX Redux on the frontend
"weight": adapter.weight,
}
if adapter.image_name is not None:
image_data = load_image_file(adapter.image_name)
if image_data:
config["image"] = image_data
else:
logger.warning(f"Could not load image for IP Adapter: {adapter.image_name}")
if adapter.begin_step_percent is not None:
config["begin_step_percent"] = adapter.begin_step_percent
if adapter.end_step_percent is not None:
config["end_step_percent"] = adapter.end_step_percent
if adapter.method is not None:
config["method"] = adapter.method
# Include FLUX Redux image influence when provided
if adapter.image_influence is not None:
config["image_influence"] = adapter.image_influence
resolved_adapters.append(config)
else:
logger.warning(f"Skipping IP Adapter '{adapter.model_name}' - model not found")
return resolved_adapters
@recall_parameters_router.post(
"/{queue_id}",
operation_id="update_recall_parameters",
response_model=dict[str, Any],
)
async def update_recall_parameters(
queue_id: str = Path(..., description="The queue id to perform this operation on"),
parameters: RecallParameter = Body(..., description="Recall parameters to update"),
) -> dict[str, Any]:
"""
Update recallable parameters that can be recalled on the frontend.
This endpoint allows updating parameters such as prompt, model, steps, and other
generation settings. These parameters are stored in client state and can be
accessed by the frontend to populate UI elements.
Args:
queue_id: The queue ID to associate these parameters with
parameters: The RecallParameter object containing the parameters to update
Returns:
A dictionary containing the updated parameters and status
Example:
POST /api/v1/recall/{queue_id}
{
"positive_prompt": "a beautiful landscape",
"model": "sd-1.5",
"steps": 20,
"cfg_scale": 7.5,
"width": 512,
"height": 512,
"seed": 12345
}
"""
logger = ApiDependencies.invoker.services.logger
try:
# Get only the parameters that were actually provided (non-None values)
provided_params = {k: v for k, v in parameters.model_dump().items() if v is not None}
if not provided_params:
return {"status": "no_parameters_provided", "updated_count": 0}
# Store each parameter in client state using a consistent key format
updated_count = 0
for param_key, param_value in provided_params.items():
# Convert parameter values to JSON strings for storage
value_str = json.dumps(param_value)
try:
ApiDependencies.invoker.services.client_state_persistence.set_by_key(
queue_id, f"recall_{param_key}", value_str
)
updated_count += 1
except Exception as e:
logger.error(f"Error setting recall parameter {param_key}: {e}")
raise HTTPException(
status_code=500,
detail=f"Error setting recall parameter {param_key}",
)
logger.info(f"Updated {updated_count} recall parameters for queue {queue_id}")
# Resolve model name to key if a model was provided
if "model" in provided_params and isinstance(provided_params["model"], str):
model_name = provided_params["model"]
model_key = resolve_model_name_to_key(model_name, ModelType.Main)
if model_key:
logger.info(f"Resolved model name '{model_name}' to key '{model_key}'")
provided_params["model"] = model_key
else:
logger.warning(f"Could not resolve model name '{model_name}' to a model key")
# Remove model from parameters if we couldn't resolve it
del provided_params["model"]
# Process LoRAs if provided
if "loras" in provided_params:
loras_param = parameters.loras
if loras_param is not None:
resolved_loras = resolve_lora_models(loras_param)
provided_params["loras"] = resolved_loras
logger.info(f"Resolved {len(resolved_loras)} LoRA(s)")
# Process control layers if provided
if "control_layers" in provided_params:
control_layers_param = parameters.control_layers
if control_layers_param is not None:
resolved_controls = resolve_control_models(control_layers_param)
provided_params["control_layers"] = resolved_controls
logger.info(f"Resolved {len(resolved_controls)} control layer(s)")
# Process IP adapters if provided
if "ip_adapters" in provided_params:
ip_adapters_param = parameters.ip_adapters
if ip_adapters_param is not None:
resolved_adapters = resolve_ip_adapter_models(ip_adapters_param)
provided_params["ip_adapters"] = resolved_adapters
logger.info(f"Resolved {len(resolved_adapters)} IP adapter(s)")
# Emit event to notify frontend of parameter updates
try:
logger.info(
f"Emitting recall_parameters_updated event for queue {queue_id} with {len(provided_params)} parameters"
)
ApiDependencies.invoker.services.events.emit_recall_parameters_updated(queue_id, provided_params)
logger.info("Successfully emitted recall_parameters_updated event")
except Exception as e:
logger.error(f"Error emitting recall parameters event: {e}", exc_info=True)
# Don't fail the request if event emission fails, just log it
return {
"status": "success",
"queue_id": queue_id,
"updated_count": updated_count,
"parameters": provided_params,
}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating recall parameters: {e}")
raise HTTPException(
status_code=500,
detail="Error updating recall parameters",
)
@recall_parameters_router.get(
"/{queue_id}",
operation_id="get_recall_parameters",
response_model=dict[str, Any],
)
async def get_recall_parameters(
queue_id: str = Path(..., description="The queue id to retrieve parameters for"),
) -> dict[str, Any]:
"""
Retrieve all stored recall parameters for a given queue.
Returns a dictionary of all recall parameters that have been set for the queue.
Args:
queue_id: The queue ID to retrieve parameters for
Returns:
A dictionary containing all stored recall parameters
"""
logger = ApiDependencies.invoker.services.logger
try:
# Retrieve all recall parameters by iterating through expected keys
# Since client_state_persistence doesn't have a "get_all" method, we'll
# return an informative response
return {
"status": "success",
"queue_id": queue_id,
"note": "Use the frontend to access stored recall parameters, or set specific parameters using POST",
}
except Exception as e:
logger.error(f"Error retrieving recall parameters: {e}")
raise HTTPException(
status_code=500,
detail="Error retrieving recall parameters",
)

View File

@@ -2,12 +2,12 @@ from typing import Optional
from fastapi import Body, HTTPException, Path, Query
from fastapi.routing import APIRouter
from pydantic import BaseModel, Field
from pydantic import BaseModel
from invokeai.app.api.auth_dependencies import AdminUserOrDefault, CurrentUserOrDefault
from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.services.session_processor.session_processor_common import SessionProcessorStatus
from invokeai.app.services.session_queue.session_queue_common import (
QUEUE_ITEM_STATUS,
Batch,
BatchStatus,
CancelAllExceptCurrentResult,
@@ -17,7 +17,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
DeleteAllExceptCurrentResult,
DeleteByDestinationResult,
EnqueueBatchResult,
FieldIdentifier,
ItemIdsResult,
PruneResult,
RetryItemsResult,
SessionQueueCountsByDestination,
@@ -25,7 +25,8 @@ from invokeai.app.services.session_queue.session_queue_common import (
SessionQueueItemNotFoundError,
SessionQueueStatus,
)
from invokeai.app.services.shared.pagination import CursorPaginatedResults
from invokeai.app.services.shared.graph import Graph, GraphExecutionState
from invokeai.app.services.shared.sqlite.sqlite_common import SQLiteDirection
session_queue_router = APIRouter(prefix="/v1/queue", tags=["queue"])
@@ -37,10 +38,38 @@ class SessionQueueAndProcessorStatus(BaseModel):
processor: SessionProcessorStatus
class ValidationRunData(BaseModel):
workflow_id: str = Field(description="The id of the workflow being published.")
input_fields: list[FieldIdentifier] = Body(description="The input fields for the published workflow")
output_fields: list[FieldIdentifier] = Body(description="The output fields for the published workflow")
def sanitize_queue_item_for_user(
queue_item: SessionQueueItem, current_user_id: str, is_admin: bool
) -> SessionQueueItem:
"""Sanitize queue item for non-admin users viewing other users' items.
For non-admin users viewing queue items belonging to other users,
the field_values, session graph, and workflow should be hidden/cleared to protect privacy.
Args:
queue_item: The queue item to sanitize
current_user_id: The ID of the current user viewing the item
is_admin: Whether the current user is an admin
Returns:
The sanitized queue item (sensitive fields cleared if necessary)
"""
# Admins and item owners can see everything
if is_admin or queue_item.user_id == current_user_id:
return queue_item
# For non-admins viewing other users' items, clear sensitive fields
# Create a shallow copy to avoid mutating the original
sanitized_item = queue_item.model_copy(deep=False)
sanitized_item.field_values = None
sanitized_item.workflow = None
# Clear the session graph by replacing it with an empty graph execution state
# This prevents information leakage through the generation graph
sanitized_item.session = GraphExecutionState(
id=queue_item.session.id,
graph=Graph(),
)
return sanitized_item
@session_queue_router.post(
@@ -51,53 +80,20 @@ class ValidationRunData(BaseModel):
},
)
async def enqueue_batch(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id to perform this operation on"),
batch: Batch = Body(description="Batch to process"),
prepend: bool = Body(default=False, description="Whether or not to prepend this batch in the queue"),
validation_run_data: Optional[ValidationRunData] = Body(
default=None,
description="The validation run data to use for this batch. This is only used if this is a validation run.",
),
) -> EnqueueBatchResult:
"""Processes a batch and enqueues the output graphs for execution."""
"""Processes a batch and enqueues the output graphs for execution for the current user."""
try:
return await ApiDependencies.invoker.services.session_queue.enqueue_batch(
queue_id=queue_id, batch=batch, prepend=prepend
queue_id=queue_id, batch=batch, prepend=prepend, user_id=current_user.user_id
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Unexpected error while enqueuing batch: {e}")
@session_queue_router.get(
"/{queue_id}/list",
operation_id="list_queue_items",
responses={
200: {"model": CursorPaginatedResults[SessionQueueItem]},
},
)
async def list_queue_items(
queue_id: str = Path(description="The queue id to perform this operation on"),
limit: int = Query(default=50, description="The number of items to fetch"),
status: Optional[QUEUE_ITEM_STATUS] = Query(default=None, description="The status of items to fetch"),
cursor: Optional[int] = Query(default=None, description="The pagination cursor"),
priority: int = Query(default=0, description="The pagination cursor priority"),
destination: Optional[str] = Query(default=None, description="The destination of queue items to fetch"),
) -> CursorPaginatedResults[SessionQueueItem]:
"""Gets cursor-paginated queue items"""
try:
return ApiDependencies.invoker.services.session_queue.list_queue_items(
queue_id=queue_id,
limit=limit,
status=status,
cursor=cursor,
priority=priority,
destination=destination,
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Unexpected error while listing all items: {e}")
@session_queue_router.get(
"/{queue_id}/list_all",
operation_id="list_all_queue_items",
@@ -106,28 +102,85 @@ async def list_queue_items(
},
)
async def list_all_queue_items(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id to perform this operation on"),
destination: Optional[str] = Query(default=None, description="The destination of queue items to fetch"),
) -> list[SessionQueueItem]:
"""Gets all queue items"""
try:
return ApiDependencies.invoker.services.session_queue.list_all_queue_items(
items = ApiDependencies.invoker.services.session_queue.list_all_queue_items(
queue_id=queue_id,
destination=destination,
)
# Sanitize items for non-admin users
return [sanitize_queue_item_for_user(item, current_user.user_id, current_user.is_admin) for item in items]
except Exception as e:
raise HTTPException(status_code=500, detail=f"Unexpected error while listing all queue items: {e}")
@session_queue_router.get(
"/{queue_id}/item_ids",
operation_id="get_queue_item_ids",
responses={
200: {"model": ItemIdsResult},
},
)
async def get_queue_item_ids(
queue_id: str = Path(description="The queue id to perform this operation on"),
order_dir: SQLiteDirection = Query(default=SQLiteDirection.Descending, description="The order of sort"),
) -> ItemIdsResult:
"""Gets all queue item ids that match the given parameters"""
try:
return ApiDependencies.invoker.services.session_queue.get_queue_item_ids(queue_id=queue_id, order_dir=order_dir)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Unexpected error while listing all queue item ids: {e}")
@session_queue_router.post(
"/{queue_id}/items_by_ids",
operation_id="get_queue_items_by_item_ids",
responses={200: {"model": list[SessionQueueItem]}},
)
async def get_queue_items_by_item_ids(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id to perform this operation on"),
item_ids: list[int] = Body(
embed=True, description="Object containing list of queue item ids to fetch queue items for"
),
) -> list[SessionQueueItem]:
"""Gets queue items for the specified queue item ids. Maintains order of item ids."""
try:
session_queue_service = ApiDependencies.invoker.services.session_queue
# Fetch queue items preserving the order of requested item ids
queue_items: list[SessionQueueItem] = []
for item_id in item_ids:
try:
queue_item = session_queue_service.get_queue_item(item_id=item_id)
if queue_item.queue_id != queue_id: # Auth protection for items from other queues
continue
# Sanitize item for non-admin users
sanitized_item = sanitize_queue_item_for_user(queue_item, current_user.user_id, current_user.is_admin)
queue_items.append(sanitized_item)
except Exception:
# Skip missing queue items - they may have been deleted between item id fetch and queue item fetch
continue
return queue_items
except Exception:
raise HTTPException(status_code=500, detail="Failed to get queue items")
@session_queue_router.put(
"/{queue_id}/processor/resume",
operation_id="resume",
responses={200: {"model": SessionProcessorStatus}},
)
async def resume(
current_user: AdminUserOrDefault,
queue_id: str = Path(description="The queue id to perform this operation on"),
) -> SessionProcessorStatus:
"""Resumes session processor"""
"""Resumes session processor. Admin only."""
try:
return ApiDependencies.invoker.services.session_processor.resume()
except Exception as e:
@@ -139,10 +192,11 @@ async def resume(
operation_id="pause",
responses={200: {"model": SessionProcessorStatus}},
)
async def Pause(
async def pause(
current_user: AdminUserOrDefault,
queue_id: str = Path(description="The queue id to perform this operation on"),
) -> SessionProcessorStatus:
"""Pauses session processor"""
"""Pauses session processor. Admin only."""
try:
return ApiDependencies.invoker.services.session_processor.pause()
except Exception as e:
@@ -155,11 +209,16 @@ async def Pause(
responses={200: {"model": CancelAllExceptCurrentResult}},
)
async def cancel_all_except_current(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id to perform this operation on"),
) -> CancelAllExceptCurrentResult:
"""Immediately cancels all queue items except in-processing items"""
"""Immediately cancels all queue items except in-processing items. Non-admin users can only cancel their own items."""
try:
return ApiDependencies.invoker.services.session_queue.cancel_all_except_current(queue_id=queue_id)
# Admin users can cancel all items, non-admin users can only cancel their own
user_id = None if current_user.is_admin else current_user.user_id
return ApiDependencies.invoker.services.session_queue.cancel_all_except_current(
queue_id=queue_id, user_id=user_id
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Unexpected error while canceling all except current: {e}")
@@ -170,11 +229,16 @@ async def cancel_all_except_current(
responses={200: {"model": DeleteAllExceptCurrentResult}},
)
async def delete_all_except_current(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id to perform this operation on"),
) -> DeleteAllExceptCurrentResult:
"""Immediately deletes all queue items except in-processing items"""
"""Immediately deletes all queue items except in-processing items. Non-admin users can only delete their own items."""
try:
return ApiDependencies.invoker.services.session_queue.delete_all_except_current(queue_id=queue_id)
# Admin users can delete all items, non-admin users can only delete their own
user_id = None if current_user.is_admin else current_user.user_id
return ApiDependencies.invoker.services.session_queue.delete_all_except_current(
queue_id=queue_id, user_id=user_id
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Unexpected error while deleting all except current: {e}")
@@ -185,13 +249,16 @@ async def delete_all_except_current(
responses={200: {"model": CancelByBatchIDsResult}},
)
async def cancel_by_batch_ids(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id to perform this operation on"),
batch_ids: list[str] = Body(description="The list of batch_ids to cancel all queue items for", embed=True),
) -> CancelByBatchIDsResult:
"""Immediately cancels all queue items from the given batch ids"""
"""Immediately cancels all queue items from the given batch ids. Non-admin users can only cancel their own items."""
try:
# Admin users can cancel all items, non-admin users can only cancel their own
user_id = None if current_user.is_admin else current_user.user_id
return ApiDependencies.invoker.services.session_queue.cancel_by_batch_ids(
queue_id=queue_id, batch_ids=batch_ids
queue_id=queue_id, batch_ids=batch_ids, user_id=user_id
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Unexpected error while canceling by batch id: {e}")
@@ -203,13 +270,16 @@ async def cancel_by_batch_ids(
responses={200: {"model": CancelByDestinationResult}},
)
async def cancel_by_destination(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id to perform this operation on"),
destination: str = Query(description="The destination to cancel all queue items for"),
) -> CancelByDestinationResult:
"""Immediately cancels all queue items with the given origin"""
"""Immediately cancels all queue items with the given destination. Non-admin users can only cancel their own items."""
try:
# Admin users can cancel all items, non-admin users can only cancel their own
user_id = None if current_user.is_admin else current_user.user_id
return ApiDependencies.invoker.services.session_queue.cancel_by_destination(
queue_id=queue_id, destination=destination
queue_id=queue_id, destination=destination, user_id=user_id
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Unexpected error while canceling by destination: {e}")
@@ -221,12 +291,28 @@ async def cancel_by_destination(
responses={200: {"model": RetryItemsResult}},
)
async def retry_items_by_id(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id to perform this operation on"),
item_ids: list[int] = Body(description="The queue item ids to retry"),
) -> RetryItemsResult:
"""Immediately cancels all queue items with the given origin"""
"""Retries the given queue items. Users can only retry their own items unless they are an admin."""
try:
# Check authorization: user must own all items or be an admin
if not current_user.is_admin:
for item_id in item_ids:
try:
queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id)
if queue_item.user_id != current_user.user_id:
raise HTTPException(
status_code=403, detail=f"You do not have permission to retry queue item {item_id}"
)
except SessionQueueItemNotFoundError:
# Skip items that don't exist - they will be handled by retry_items_by_id
continue
return ApiDependencies.invoker.services.session_queue.retry_items_by_id(queue_id=queue_id, item_ids=item_ids)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Unexpected error while retrying queue items: {e}")
@@ -239,15 +325,25 @@ async def retry_items_by_id(
},
)
async def clear(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id to perform this operation on"),
) -> ClearResult:
"""Clears the queue entirely, immediately canceling the currently-executing session"""
"""Clears the queue entirely. Admin users clear all items; non-admin users only clear their own items. If there's a currently-executing item, users can only cancel it if they own it or are an admin."""
try:
queue_item = ApiDependencies.invoker.services.session_queue.get_current(queue_id)
if queue_item is not None:
# Check authorization for canceling the current item
if queue_item.user_id != current_user.user_id and not current_user.is_admin:
raise HTTPException(
status_code=403, detail="You do not have permission to cancel the currently executing queue item"
)
ApiDependencies.invoker.services.session_queue.cancel_queue_item(queue_item.item_id)
clear_result = ApiDependencies.invoker.services.session_queue.clear(queue_id)
# Admin users can clear all items, non-admin users can only clear their own
user_id = None if current_user.is_admin else current_user.user_id
clear_result = ApiDependencies.invoker.services.session_queue.clear(queue_id, user_id=user_id)
return clear_result
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Unexpected error while clearing queue: {e}")
@@ -260,11 +356,14 @@ async def clear(
},
)
async def prune(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id to perform this operation on"),
) -> PruneResult:
"""Prunes all completed or errored queue items"""
"""Prunes all completed or errored queue items. Non-admin users can only prune their own items."""
try:
return ApiDependencies.invoker.services.session_queue.prune(queue_id)
# Admin users can prune all items, non-admin users can only prune their own
user_id = None if current_user.is_admin else current_user.user_id
return ApiDependencies.invoker.services.session_queue.prune(queue_id, user_id=user_id)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Unexpected error while pruning queue: {e}")
@@ -311,11 +410,12 @@ async def get_next_queue_item(
},
)
async def get_queue_status(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id to perform this operation on"),
) -> SessionQueueAndProcessorStatus:
"""Gets the status of the session queue"""
try:
queue = ApiDependencies.invoker.services.session_queue.get_queue_status(queue_id)
queue = ApiDependencies.invoker.services.session_queue.get_queue_status(queue_id, user_id=current_user.user_id)
processor = ApiDependencies.invoker.services.session_processor.get_status()
return SessionQueueAndProcessorStatus(queue=queue, processor=processor)
except Exception as e:
@@ -349,12 +449,17 @@ async def get_batch_status(
response_model_exclude_none=True,
)
async def get_queue_item(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id to perform this operation on"),
item_id: int = Path(description="The queue item to get"),
) -> SessionQueueItem:
"""Gets a queue item"""
try:
return ApiDependencies.invoker.services.session_queue.get_queue_item(item_id)
queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id=item_id)
if queue_item.queue_id != queue_id:
raise HTTPException(status_code=404, detail=f"Queue item with id {item_id} not found in queue {queue_id}")
# Sanitize item for non-admin users
return sanitize_queue_item_for_user(queue_item, current_user.user_id, current_user.is_admin)
except SessionQueueItemNotFoundError:
raise HTTPException(status_code=404, detail=f"Queue item with id {item_id} not found in queue {queue_id}")
except Exception as e:
@@ -366,12 +471,24 @@ async def get_queue_item(
operation_id="delete_queue_item",
)
async def delete_queue_item(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id to perform this operation on"),
item_id: int = Path(description="The queue item to delete"),
) -> None:
"""Deletes a queue item"""
"""Deletes a queue item. Users can only delete their own items unless they are an admin."""
try:
# Get the queue item to check ownership
queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id)
# Check authorization: user must own the item or be an admin
if queue_item.user_id != current_user.user_id and not current_user.is_admin:
raise HTTPException(status_code=403, detail="You do not have permission to delete this queue item")
ApiDependencies.invoker.services.session_queue.delete_queue_item(item_id)
except SessionQueueItemNotFoundError:
raise HTTPException(status_code=404, detail=f"Queue item with id {item_id} not found in queue {queue_id}")
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Unexpected error while deleting queue item: {e}")
@@ -384,14 +501,24 @@ async def delete_queue_item(
},
)
async def cancel_queue_item(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id to perform this operation on"),
item_id: int = Path(description="The queue item to cancel"),
) -> SessionQueueItem:
"""Deletes a queue item"""
"""Cancels a queue item. Users can only cancel their own items unless they are an admin."""
try:
# Get the queue item to check ownership
queue_item = ApiDependencies.invoker.services.session_queue.get_queue_item(item_id)
# Check authorization: user must own the item or be an admin
if queue_item.user_id != current_user.user_id and not current_user.is_admin:
raise HTTPException(status_code=403, detail="You do not have permission to cancel this queue item")
return ApiDependencies.invoker.services.session_queue.cancel_queue_item(item_id)
except SessionQueueItemNotFoundError:
raise HTTPException(status_code=404, detail=f"Queue item with id {item_id} not found in queue {queue_id}")
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"Unexpected error while canceling queue item: {e}")
@@ -420,13 +547,16 @@ async def counts_by_destination(
responses={200: {"model": DeleteByDestinationResult}},
)
async def delete_by_destination(
current_user: CurrentUserOrDefault,
queue_id: str = Path(description="The queue id to query"),
destination: str = Path(description="The destination to query"),
) -> DeleteByDestinationResult:
"""Deletes all items with the given destination"""
"""Deletes all items with the given destination. Non-admin users can only delete their own items."""
try:
# Admin users can delete all items, non-admin users can only delete their own
user_id = None if current_user.is_admin else current_user.user_id
return ApiDependencies.invoker.services.session_queue.delete_by_destination(
queue_id=queue_id, destination=destination
queue_id=queue_id, destination=destination, user_id=user_id
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"Unexpected error while deleting by destination: {e}")

View File

@@ -106,7 +106,6 @@ async def list_workflows(
tags: Optional[list[str]] = Query(default=None, description="The tags of workflow to get"),
query: Optional[str] = Query(default=None, description="The text to query by (matches name and description)"),
has_been_opened: Optional[bool] = Query(default=None, description="Whether to include/exclude recent workflows"),
is_published: Optional[bool] = Query(default=None, description="Whether to include/exclude published workflows"),
) -> PaginatedResults[WorkflowRecordListItemWithThumbnailDTO]:
"""Gets a page of workflows"""
workflows_with_thumbnails: list[WorkflowRecordListItemWithThumbnailDTO] = []
@@ -119,7 +118,6 @@ async def list_workflows(
categories=categories,
tags=tags,
has_been_opened=has_been_opened,
is_published=is_published,
)
for workflow in workflows.items:
workflows_with_thumbnails.append(
@@ -225,6 +223,15 @@ async def get_workflow_thumbnail(
raise HTTPException(status_code=404)
@workflows_router.get("/tags", operation_id="get_all_tags")
async def get_all_tags(
categories: Optional[list[WorkflowCategory]] = Query(default=None, description="The categories to include"),
) -> list[str]:
"""Gets all unique tags from workflows"""
return ApiDependencies.invoker.services.workflow_records.get_all_tags(categories=categories)
@workflows_router.get("/counts_by_tag", operation_id="get_counts_by_tag")
async def get_counts_by_tag(
tags: list[str] = Query(description="The tags to get counts for"),

View File

@@ -6,6 +6,7 @@ from fastapi import FastAPI
from pydantic import BaseModel
from socketio import ASGIApp, AsyncServer
from invokeai.app.services.auth.token_service import verify_token
from invokeai.app.services.events.events_common import (
BatchEnqueuedEvent,
BulkDownloadCompleteEvent,
@@ -35,8 +36,12 @@ from invokeai.app.services.events.events_common import (
QueueClearedEvent,
QueueEventBase,
QueueItemStatusChangedEvent,
RecallParametersUpdatedEvent,
register_events,
)
from invokeai.backend.util.logging import InvokeAILogger
logger = InvokeAILogger.get_logger()
class QueueSubscriptionEvent(BaseModel):
@@ -61,6 +66,7 @@ QUEUE_EVENTS = {
QueueItemStatusChangedEvent,
BatchEnqueuedEvent,
QueueClearedEvent,
RecallParametersUpdatedEvent,
}
MODEL_EVENTS = {
@@ -94,6 +100,13 @@ class SocketIO:
self._app = ASGIApp(socketio_server=self._sio, socketio_path="/ws/socket.io")
app.mount("/ws", self._app)
# Track user information for each socket connection
self._socket_users: dict[str, dict[str, Any]] = {}
# Set up authentication middleware
self._sio.on("connect", handler=self._handle_connect)
self._sio.on("disconnect", handler=self._handle_disconnect)
self._sio.on(self._sub_queue, handler=self._handle_sub_queue)
self._sio.on(self._unsub_queue, handler=self._handle_unsub_queue)
self._sio.on(self._sub_bulk_download, handler=self._handle_sub_bulk_download)
@@ -103,8 +116,83 @@ class SocketIO:
register_events(MODEL_EVENTS, self._handle_model_event)
register_events(BULK_DOWNLOAD_EVENTS, self._handle_bulk_image_download_event)
async def _handle_connect(self, sid: str, environ: dict, auth: dict | None) -> bool:
"""Handle socket connection and authenticate the user.
Returns True to accept the connection, False to reject it.
Stores user_id in the internal socket users dict for later use.
"""
# Extract token from auth data or headers
token = None
if auth and isinstance(auth, dict):
token = auth.get("token")
if not token and environ:
# Try to get token from headers
headers = environ.get("HTTP_AUTHORIZATION", "")
if headers.startswith("Bearer "):
token = headers[7:]
# Verify the token
if token:
token_data = verify_token(token)
if token_data:
# Store user_id and is_admin in socket users dict
self._socket_users[sid] = {
"user_id": token_data.user_id,
"is_admin": token_data.is_admin,
}
logger.info(
f"Socket {sid} connected with user_id: {token_data.user_id}, is_admin: {token_data.is_admin}"
)
return True
# If no valid token, store system user for backward compatibility
self._socket_users[sid] = {
"user_id": "system",
"is_admin": False,
}
logger.debug(f"Socket {sid} connected as system user (no valid token)")
return True
async def _handle_disconnect(self, sid: str) -> None:
"""Handle socket disconnection and cleanup user info."""
if sid in self._socket_users:
del self._socket_users[sid]
logger.debug(f"Socket {sid} disconnected and cleaned up")
async def _handle_sub_queue(self, sid: str, data: Any) -> None:
await self._sio.enter_room(sid, QueueSubscriptionEvent(**data).queue_id)
"""Handle queue subscription and add socket to both queue and user-specific rooms."""
queue_id = QueueSubscriptionEvent(**data).queue_id
# Check if we have user info for this socket
if sid not in self._socket_users:
logger.warning(
f"Socket {sid} subscribing to queue {queue_id} but has no user info - need to authenticate via connect event"
)
# Store as system user temporarily - real auth should happen in connect
self._socket_users[sid] = {
"user_id": "system",
"is_admin": False,
}
user_id = self._socket_users[sid]["user_id"]
is_admin = self._socket_users[sid]["is_admin"]
# Add socket to the queue room
await self._sio.enter_room(sid, queue_id)
# Also add socket to a user-specific room for event filtering
user_room = f"user:{user_id}"
await self._sio.enter_room(sid, user_room)
# If admin, also add to admin room to receive all events
if is_admin:
await self._sio.enter_room(sid, "admin")
logger.debug(
f"Socket {sid} (user_id: {user_id}, is_admin: {is_admin}) subscribed to queue {queue_id} and user room {user_room}"
)
async def _handle_unsub_queue(self, sid: str, data: Any) -> None:
await self._sio.leave_room(sid, QueueSubscriptionEvent(**data).queue_id)
@@ -116,7 +204,62 @@ class SocketIO:
await self._sio.leave_room(sid, BulkDownloadSubscriptionEvent(**data).bulk_download_id)
async def _handle_queue_event(self, event: FastAPIEvent[QueueEventBase]):
await self._sio.emit(event=event[0], data=event[1].model_dump(mode="json"), room=event[1].queue_id)
"""Handle queue events with user isolation.
Invocation events (progress, started, complete) are private - only emit to owner and admins.
Queue item status events are public - emit to all users (field values hidden via API).
Other queue events emit to all subscribers.
IMPORTANT: Check InvocationEventBase BEFORE QueueItemEventBase since InvocationEventBase
inherits from QueueItemEventBase. The order of isinstance checks matters!
"""
try:
event_name, event_data = event
# Import here to avoid circular dependency
from invokeai.app.services.events.events_common import InvocationEventBase, QueueItemEventBase
# Check InvocationEventBase FIRST (before QueueItemEventBase) since it's a subclass
# Invocation events (progress, started, complete, error) are private to owner + admins
if isinstance(event_data, InvocationEventBase) and hasattr(event_data, "user_id"):
user_room = f"user:{event_data.user_id}"
# Emit to the user's room
await self._sio.emit(event=event_name, data=event_data.model_dump(mode="json"), room=user_room)
# Also emit to admin room so admins can see all events, but strip image preview data
# from InvocationProgressEvent to prevent admins from seeing other users' image content
if isinstance(event_data, InvocationProgressEvent):
admin_event_data = event_data.model_copy(update={"image": None})
await self._sio.emit(event=event_name, data=admin_event_data.model_dump(mode="json"), room="admin")
else:
await self._sio.emit(event=event_name, data=event_data.model_dump(mode="json"), room="admin")
logger.debug(f"Emitted private invocation event {event_name} to user room {user_room} and admin room")
# Queue item status events are visible to all users (field values masked via API)
# This catches QueueItemStatusChangedEvent but NOT InvocationEvents (already handled above)
elif isinstance(event_data, QueueItemEventBase) and hasattr(event_data, "user_id"):
# Emit to all subscribers in the queue
await self._sio.emit(
event=event_name, data=event_data.model_dump(mode="json"), room=event_data.queue_id
)
logger.info(
f"Emitted public queue item event {event_name} to all subscribers in queue {event_data.queue_id}"
)
else:
# For other queue events (like QueueClearedEvent, BatchEnqueuedEvent), emit to all subscribers
await self._sio.emit(
event=event_name, data=event_data.model_dump(mode="json"), room=event_data.queue_id
)
logger.info(
f"Emitted general queue event {event_name} to all subscribers in queue {event_data.queue_id}"
)
except Exception as e:
# Log any unhandled exceptions in event handling to prevent silent failures
logger.error(f"Error handling queue event {event[0]}: {e}", exc_info=True)
async def _handle_model_event(self, event: FastAPIEvent[ModelEventBase | DownloadEventBase]) -> None:
await self._sio.emit(event=event[0], data=event[1].model_dump(mode="json"))

View File

@@ -17,12 +17,15 @@ from invokeai.app.api.dependencies import ApiDependencies
from invokeai.app.api.no_cache_staticfiles import NoCacheStaticFiles
from invokeai.app.api.routers import (
app_info,
auth,
board_images,
boards,
client_state,
download_queue,
images,
model_manager,
model_relationships,
recall_parameters,
session_queue,
style_presets,
utilities,
@@ -120,6 +123,8 @@ app.add_middleware(GZipMiddleware, minimum_size=1000)
# Include all routers
# Authentication router should be first so it's registered before protected routes
app.include_router(auth.auth_router, prefix="/api")
app.include_router(utilities.utilities_router, prefix="/api")
app.include_router(model_manager.model_manager_router, prefix="/api")
app.include_router(download_queue.download_queue_router, prefix="/api")
@@ -131,6 +136,8 @@ app.include_router(app_info.app_router, prefix="/api")
app.include_router(session_queue.session_queue_router, prefix="/api")
app.include_router(workflows.workflows_router, prefix="/api")
app.include_router(style_presets.style_presets_router, prefix="/api")
app.include_router(client_state.client_state_router, prefix="/api")
app.include_router(recall_parameters.recall_parameters_router, prefix="/api")
app.openapi = get_openapi_func(app)
@@ -155,6 +162,12 @@ def overridden_redoc() -> HTMLResponse:
web_root_path = Path(list(web_dir.__path__)[0])
if app_config.unsafe_disable_picklescan:
logger.warning(
"The unsafe_disable_picklescan option is enabled. This disables malware scanning while installing and"
"loading models, which may allow malicious code to be executed. Use at your own risk."
)
try:
app.mount("/", NoCacheStaticFiles(directory=Path(web_root_path, "dist"), html=True), name="ui")
except RuntimeError:

View File

@@ -36,6 +36,9 @@ from pydantic_core import PydanticUndefined
from invokeai.app.invocations.fields import (
FieldKind,
Input,
InputFieldJSONSchemaExtra,
UIType,
migrate_model_ui_type,
)
from invokeai.app.services.config.config_default import get_config
from invokeai.app.services.shared.invocation_context import InvocationContext
@@ -256,7 +259,9 @@ class BaseInvocation(ABC, BaseModel):
is_intermediate: bool = Field(
default=False,
description="Whether or not this is an intermediate invocation.",
json_schema_extra={"ui_type": "IsIntermediate", "field_kind": FieldKind.NodeAttribute},
json_schema_extra=InputFieldJSONSchemaExtra(
input=Input.Direct, field_kind=FieldKind.NodeAttribute, ui_type=UIType._IsIntermediate
).model_dump(exclude_none=True),
)
use_cache: bool = Field(
default=True,
@@ -445,6 +450,15 @@ with warnings.catch_warnings():
RESERVED_PYDANTIC_FIELD_NAMES = {m[0] for m in inspect.getmembers(_Model())}
def is_enum_member(value: Any, enum_class: type[Enum]) -> bool:
"""Checks if a value is a member of an enum class."""
try:
enum_class(value)
return True
except ValueError:
return False
def validate_fields(model_fields: dict[str, FieldInfo], model_type: str) -> None:
"""
Validates the fields of an invocation or invocation output:
@@ -456,51 +470,99 @@ def validate_fields(model_fields: dict[str, FieldInfo], model_type: str) -> None
"""
for name, field in model_fields.items():
if name in RESERVED_PYDANTIC_FIELD_NAMES:
raise InvalidFieldError(f'Invalid field name "{name}" on "{model_type}" (reserved by pydantic)')
raise InvalidFieldError(f"{model_type}.{name}: Invalid field name (reserved by pydantic)")
if not field.annotation:
raise InvalidFieldError(f'Invalid field type "{name}" on "{model_type}" (missing annotation)')
raise InvalidFieldError(f"{model_type}.{name}: Invalid field type (missing annotation)")
if not isinstance(field.json_schema_extra, dict):
raise InvalidFieldError(
f'Invalid field definition for "{name}" on "{model_type}" (missing json_schema_extra dict)'
)
raise InvalidFieldError(f"{model_type}.{name}: Invalid field definition (missing json_schema_extra dict)")
field_kind = field.json_schema_extra.get("field_kind", None)
# must have a field_kind
if not isinstance(field_kind, FieldKind):
if not is_enum_member(field_kind, FieldKind):
raise InvalidFieldError(
f'Invalid field definition for "{name}" on "{model_type}" (maybe it\'s not an InputField or OutputField?)'
f"{model_type}.{name}: Invalid field definition for (maybe it's not an InputField or OutputField?)"
)
if field_kind is FieldKind.Input and (
if field_kind == FieldKind.Input.value and (
name in RESERVED_NODE_ATTRIBUTE_FIELD_NAMES or name in RESERVED_INPUT_FIELD_NAMES
):
raise InvalidFieldError(f'Invalid field name "{name}" on "{model_type}" (reserved input field name)')
raise InvalidFieldError(f"{model_type}.{name}: Invalid field name (reserved input field name)")
if field_kind is FieldKind.Output and name in RESERVED_OUTPUT_FIELD_NAMES:
raise InvalidFieldError(f'Invalid field name "{name}" on "{model_type}" (reserved output field name)')
if field_kind == FieldKind.Output.value and name in RESERVED_OUTPUT_FIELD_NAMES:
raise InvalidFieldError(f"{model_type}.{name}: Invalid field name (reserved output field name)")
if (field_kind is FieldKind.Internal) and name not in RESERVED_INPUT_FIELD_NAMES:
raise InvalidFieldError(
f'Invalid field name "{name}" on "{model_type}" (internal field without reserved name)'
)
if field_kind == FieldKind.Internal.value and name not in RESERVED_INPUT_FIELD_NAMES:
raise InvalidFieldError(f"{model_type}.{name}: Invalid field name (internal field without reserved name)")
# node attribute fields *must* be in the reserved list
if (
field_kind is FieldKind.NodeAttribute
field_kind == FieldKind.NodeAttribute.value
and name not in RESERVED_NODE_ATTRIBUTE_FIELD_NAMES
and name not in RESERVED_OUTPUT_FIELD_NAMES
):
raise InvalidFieldError(
f'Invalid field name "{name}" on "{model_type}" (node attribute field without reserved name)'
f"{model_type}.{name}: Invalid field name (node attribute field without reserved name)"
)
ui_type = field.json_schema_extra.get("ui_type", None)
if isinstance(ui_type, str) and ui_type.startswith("DEPRECATED_"):
logger.warning(f'"UIType.{ui_type.split("_")[-1]}" is deprecated, ignoring')
field.json_schema_extra.pop("ui_type")
ui_model_base = field.json_schema_extra.get("ui_model_base", None)
ui_model_type = field.json_schema_extra.get("ui_model_type", None)
ui_model_variant = field.json_schema_extra.get("ui_model_variant", None)
ui_model_format = field.json_schema_extra.get("ui_model_format", None)
if ui_type is not None:
# There are 3 cases where we may need to take action:
#
# 1. The ui_type is a migratable, deprecated value. For example, ui_type=UIType.MainModel value is
# deprecated and should be migrated to:
# - ui_model_base=[BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2]
# - ui_model_type=[ModelType.Main]
#
# 2. ui_type was set in conjunction with any of the new ui_model_[base|type|variant|format] fields, which
# is not allowed (they are mutually exclusive). In this case, we ignore ui_type and log a warning.
#
# 3. ui_type is a deprecated value that is not migratable. For example, ui_type=UIType.Image is deprecated;
# Image fields are now automatically detected based on the field's type annotation. In this case, we
# ignore ui_type and log a warning.
#
# The cases must be checked in this order to ensure proper handling.
# Easier to work with as an enum
ui_type = UIType(ui_type)
# The enum member values are not always the same as their names - we want to log the name so the user can
# easily review their code and see where the deprecated enum member is used.
human_readable_name = f"UIType.{ui_type.name}"
# Case 1: migratable deprecated value
did_migrate = migrate_model_ui_type(ui_type, field.json_schema_extra)
if did_migrate:
logger.warning(
f'{model_type}.{name}: Migrated deprecated "ui_type" "{human_readable_name}" to new ui_model_[base|type|variant|format] fields'
)
field.json_schema_extra.pop("ui_type")
# Case 2: mutually exclusive with new fields
elif (
ui_model_base is not None
or ui_model_type is not None
or ui_model_variant is not None
or ui_model_format is not None
):
logger.warning(
f'{model_type}.{name}: "ui_type" is mutually exclusive with "ui_model_[base|type|format|variant]", ignoring "ui_type"'
)
field.json_schema_extra.pop("ui_type")
# Case 3: deprecated value that is not migratable
elif ui_type.startswith("DEPRECATED_"):
logger.warning(f'{model_type}.{name}: Deprecated "ui_type" "{human_readable_name}", ignoring')
field.json_schema_extra.pop("ui_type")
return None

View File

@@ -22,7 +22,7 @@ from invokeai.app.invocations.model import TransformerField
from invokeai.app.invocations.primitives import LatentsOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.flux.sampling_utils import clip_timestep_schedule_fractional
from invokeai.backend.model_manager.config import BaseModelType
from invokeai.backend.model_manager.taxonomy import BaseModelType
from invokeai.backend.rectified_flow.rectified_flow_inpaint_extension import RectifiedFlowInpaintExtension
from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import CogView4ConditioningInfo

View File

@@ -17,6 +17,7 @@ from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.load.load_base import LoadedModel
from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
from invokeai.backend.util.devices import TorchDevice
from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_cogview4
# TODO(ryand): This is effectively a copy of SD3ImageToLatentsInvocation and a subset of ImageToLatentsInvocation. We
# should refactor to avoid this duplication.
@@ -38,7 +39,11 @@ class CogView4ImageToLatentsInvocation(BaseInvocation, WithMetadata, WithBoard):
@staticmethod
def vae_encode(vae_info: LoadedModel, image_tensor: torch.Tensor) -> torch.Tensor:
with vae_info as vae:
assert isinstance(vae_info.model, AutoencoderKL)
estimated_working_memory = estimate_vae_working_memory_cogview4(
operation="encode", image_tensor=image_tensor, vae=vae_info.model
)
with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
assert isinstance(vae, AutoencoderKL)
vae.disable_tiling()
@@ -62,6 +67,8 @@ class CogView4ImageToLatentsInvocation(BaseInvocation, WithMetadata, WithBoard):
image_tensor = einops.rearrange(image_tensor, "c h w -> 1 c h w")
vae_info = context.models.load(self.vae.vae)
assert isinstance(vae_info.model, AutoencoderKL)
latents = self.vae_encode(vae_info=vae_info, image_tensor=image_tensor)
latents = latents.to("cpu")

View File

@@ -6,7 +6,6 @@ from einops import rearrange
from PIL import Image
from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
from invokeai.app.invocations.fields import (
FieldDescriptions,
Input,
@@ -20,6 +19,7 @@ from invokeai.app.invocations.primitives import ImageOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.stable_diffusion.extensions.seamless import SeamlessExt
from invokeai.backend.util.devices import TorchDevice
from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_cogview4
# TODO(ryand): This is effectively a copy of SD3LatentsToImageInvocation and a subset of LatentsToImageInvocation. We
# should refactor to avoid this duplication.
@@ -39,22 +39,15 @@ class CogView4LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
latents: LatentsField = InputField(description=FieldDescriptions.latents, input=Input.Connection)
vae: VAEField = InputField(description=FieldDescriptions.vae, input=Input.Connection)
def _estimate_working_memory(self, latents: torch.Tensor, vae: AutoencoderKL) -> int:
"""Estimate the working memory required by the invocation in bytes."""
out_h = LATENT_SCALE_FACTOR * latents.shape[-2]
out_w = LATENT_SCALE_FACTOR * latents.shape[-1]
element_size = next(vae.parameters()).element_size()
scaling_constant = 2200 # Determined experimentally.
working_memory = out_h * out_w * element_size * scaling_constant
return int(working_memory)
@torch.no_grad()
def invoke(self, context: InvocationContext) -> ImageOutput:
latents = context.tensors.load(self.latents.latents_name)
vae_info = context.models.load(self.vae.vae)
assert isinstance(vae_info.model, (AutoencoderKL))
estimated_working_memory = self._estimate_working_memory(latents, vae_info.model)
estimated_working_memory = estimate_vae_working_memory_cogview4(
operation="decode", image_tensor=latents, vae=vae_info.model
)
with (
SeamlessExt.static_patch_model(vae_info.model, self.vae.seamless_axes),
vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae),

View File

@@ -5,7 +5,7 @@ from invokeai.app.invocations.baseinvocation import (
invocation,
invocation_output,
)
from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, UIType
from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField
from invokeai.app.invocations.model import (
GlmEncoderField,
ModelIdentifierField,
@@ -13,7 +13,7 @@ from invokeai.app.invocations.model import (
VAEField,
)
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.config import SubModelType
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType, SubModelType
@invocation_output("cogview4_model_loader_output")
@@ -38,8 +38,9 @@ class CogView4ModelLoaderInvocation(BaseInvocation):
model: ModelIdentifierField = InputField(
description=FieldDescriptions.cogview4_model,
ui_type=UIType.CogView4MainModel,
input=Input.Direct,
ui_model_base=BaseModelType.CogView4,
ui_model_type=ModelType.Main,
)
def invoke(self, context: InvocationContext) -> CogView4ModelLoaderOutput:

View File

@@ -6,11 +6,11 @@ from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField
from invokeai.app.invocations.model import GlmEncoderField
from invokeai.app.invocations.primitives import CogView4ConditioningOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import (
CogView4ConditioningInfo,
ConditioningFieldData,
)
from invokeai.backend.util.devices import TorchDevice
# The CogView4 GLM Text Encoder max sequence length set based on the default in diffusers.
COGVIEW4_GLM_MAX_SEQ_LEN = 1024
@@ -37,6 +37,8 @@ class CogView4TextEncoderInvocation(BaseInvocation):
@torch.no_grad()
def invoke(self, context: InvocationContext) -> CogView4ConditioningOutput:
glm_embeds = self._glm_encode(context, max_seq_len=COGVIEW4_GLM_MAX_SEQ_LEN)
# Move embeddings to CPU for storage to save VRAM
glm_embeds = glm_embeds.detach().to("cpu")
conditioning_data = ConditioningFieldData(conditionings=[CogView4ConditioningInfo(glm_embeds=glm_embeds)])
conditioning_name = context.conditioning.save(conditioning_data)
return CogView4ConditioningOutput.build(conditioning_name)
@@ -45,10 +47,18 @@ class CogView4TextEncoderInvocation(BaseInvocation):
prompt = [self.prompt]
# TODO(ryand): Add model inputs to the invocation rather than hard-coding.
glm_text_encoder_info = context.models.load(self.glm_encoder.text_encoder)
with (
context.models.load(self.glm_encoder.text_encoder).model_on_device() as (_, glm_text_encoder),
glm_text_encoder_info.model_on_device() as (_, glm_text_encoder),
context.models.load(self.glm_encoder.tokenizer).model_on_device() as (_, glm_tokenizer),
):
repaired_tensors = glm_text_encoder_info.repair_required_tensors_on_device()
device = get_effective_device(glm_text_encoder)
if repaired_tensors > 0:
context.logger.warning(
f"Recovered {repaired_tensors} required GLM tensor(s) onto {device} after a partial device mismatch."
)
context.util.signal_progress("Running GLM text encoder")
assert isinstance(glm_text_encoder, GlmModel)
assert isinstance(glm_tokenizer, PreTrainedTokenizerFast)
@@ -84,9 +94,7 @@ class CogView4TextEncoderInvocation(BaseInvocation):
device=text_input_ids.device,
)
text_input_ids = torch.cat([pad_ids, text_input_ids], dim=1)
prompt_embeds = glm_text_encoder(
text_input_ids.to(TorchDevice.choose_torch_device()), output_hidden_states=True
).hidden_states[-2]
prompt_embeds = glm_text_encoder(text_input_ids.to(device), output_hidden_states=True).hidden_states[-2]
assert isinstance(prompt_embeds, torch.Tensor)
return prompt_embeds

View File

@@ -103,7 +103,7 @@ class CompelInvocation(BaseInvocation):
textual_inversion_manager=ti_manager,
dtype_for_device_getter=TorchDevice.choose_torch_dtype,
truncate_long_prompts=False,
device=TorchDevice.choose_torch_device(),
device=text_encoder.device, # Use the device the model is actually on
split_long_text_mode=SplitLongTextMode.SENTENCES,
)
@@ -212,7 +212,7 @@ class SDXLPromptInvocationBase:
truncate_long_prompts=False, # TODO:
returned_embeddings_type=ReturnedEmbeddingsType.PENULTIMATE_HIDDEN_STATES_NON_NORMALIZED, # TODO: clip skip
requires_pooled=get_pooled,
device=TorchDevice.choose_torch_device(),
device=text_encoder.device, # Use the device the model is actually on
split_long_text_mode=SplitLongTextMode.SENTENCES,
)

View File

@@ -16,7 +16,6 @@ from invokeai.app.invocations.fields import (
ImageField,
InputField,
OutputField,
UIType,
)
from invokeai.app.invocations.model import ModelIdentifierField
from invokeai.app.invocations.primitives import ImageOutput
@@ -28,6 +27,7 @@ from invokeai.app.util.controlnet_utils import (
heuristic_resize_fast,
)
from invokeai.backend.image_util.util import np_to_pil, pil_to_np
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
class ControlField(BaseModel):
@@ -63,13 +63,17 @@ class ControlOutput(BaseInvocationOutput):
control: ControlField = OutputField(description=FieldDescriptions.control)
@invocation("controlnet", title="ControlNet - SD1.5, SDXL", tags=["controlnet"], category="controlnet", version="1.1.3")
@invocation(
"controlnet", title="ControlNet - SD1.5, SD2, SDXL", tags=["controlnet"], category="controlnet", version="1.1.3"
)
class ControlNetInvocation(BaseInvocation):
"""Collects ControlNet info to pass to other nodes"""
image: ImageField = InputField(description="The control image")
control_model: ModelIdentifierField = InputField(
description=FieldDescriptions.controlnet_model, ui_type=UIType.ControlNetModel
description=FieldDescriptions.controlnet_model,
ui_model_base=[BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2, BaseModelType.StableDiffusionXL],
ui_model_type=ModelType.ControlNet,
)
control_weight: Union[float, List[float]] = InputField(
default=1.0, ge=-1, le=2, description="The weight given to the ControlNet"

View File

@@ -20,9 +20,7 @@ from invokeai.app.invocations.fields import (
from invokeai.app.invocations.image_to_latents import ImageToLatentsInvocation
from invokeai.app.invocations.model import UNetField, VAEField
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager import LoadedModel
from invokeai.backend.model_manager.config import MainConfigBase
from invokeai.backend.model_manager.taxonomy import ModelVariantType
from invokeai.backend.model_manager.taxonomy import FluxVariantType, ModelType, ModelVariantType
from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
@@ -182,10 +180,11 @@ class CreateGradientMaskInvocation(BaseInvocation):
if self.unet is not None and self.vae is not None and self.image is not None:
# all three fields must be present at the same time
main_model_config = context.models.get_config(self.unet.unet.key)
assert isinstance(main_model_config, MainConfigBase)
if main_model_config.variant is ModelVariantType.Inpaint:
assert main_model_config.type is ModelType.Main
variant = getattr(main_model_config, "variant", None)
if variant is ModelVariantType.Inpaint or variant is FluxVariantType.DevFill:
mask = dilated_mask_tensor
vae_info: LoadedModel = context.models.load(self.vae.vae)
vae_info = context.models.load(self.vae.vae)
image = context.images.get_pil(self.image.image_name)
image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB"))
if image_tensor.dim() == 3:

View File

@@ -39,7 +39,7 @@ from invokeai.app.invocations.t2i_adapter import T2IAdapterField
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.app.util.controlnet_utils import prepare_control_image
from invokeai.backend.ip_adapter.ip_adapter import IPAdapter
from invokeai.backend.model_manager.config import AnyModelConfig
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelVariantType
from invokeai.backend.model_patcher import ModelPatcher
from invokeai.backend.patches.layer_patcher import LayerPatcher

View File

@@ -1,11 +1,19 @@
from enum import Enum
from typing import Any, Callable, Optional, Tuple
from pydantic import BaseModel, ConfigDict, Field, RootModel, TypeAdapter, model_validator
from pydantic import BaseModel, ConfigDict, Field, RootModel, TypeAdapter
from pydantic.fields import _Unset
from pydantic_core import PydanticUndefined
from invokeai.app.util.metaenum import MetaEnum
from invokeai.backend.image_util.segment_anything.shared import BoundingBox
from invokeai.backend.model_manager.taxonomy import (
BaseModelType,
ClipVariantType,
ModelFormat,
ModelType,
ModelVariantType,
)
from invokeai.backend.util.logging import InvokeAILogger
logger = InvokeAILogger.get_logger()
@@ -38,35 +46,6 @@ class UIType(str, Enum, metaclass=MetaEnum):
used, and the type will be ignored. They are included here for backwards compatibility.
"""
# region Model Field Types
MainModel = "MainModelField"
CogView4MainModel = "CogView4MainModelField"
FluxMainModel = "FluxMainModelField"
SD3MainModel = "SD3MainModelField"
SDXLMainModel = "SDXLMainModelField"
SDXLRefinerModel = "SDXLRefinerModelField"
ONNXModel = "ONNXModelField"
VAEModel = "VAEModelField"
FluxVAEModel = "FluxVAEModelField"
LoRAModel = "LoRAModelField"
ControlNetModel = "ControlNetModelField"
IPAdapterModel = "IPAdapterModelField"
T2IAdapterModel = "T2IAdapterModelField"
T5EncoderModel = "T5EncoderModelField"
CLIPEmbedModel = "CLIPEmbedModelField"
CLIPLEmbedModel = "CLIPLEmbedModelField"
CLIPGEmbedModel = "CLIPGEmbedModelField"
SpandrelImageToImageModel = "SpandrelImageToImageModelField"
ControlLoRAModel = "ControlLoRAModelField"
SigLipModel = "SigLipModelField"
FluxReduxModel = "FluxReduxModelField"
LlavaOnevisionModel = "LLaVAModelField"
Imagen3Model = "Imagen3ModelField"
Imagen4Model = "Imagen4ModelField"
ChatGPT4oModel = "ChatGPT4oModelField"
FluxKontextModel = "FluxKontextModelField"
# endregion
# region Misc Field Types
Scheduler = "SchedulerField"
Any = "AnyField"
@@ -75,6 +54,7 @@ class UIType(str, Enum, metaclass=MetaEnum):
# region Internal Field Types
_Collection = "CollectionField"
_CollectionItem = "CollectionItemField"
_IsIntermediate = "IsIntermediate"
# endregion
# region DEPRECATED
@@ -112,13 +92,44 @@ class UIType(str, Enum, metaclass=MetaEnum):
CollectionItem = "DEPRECATED_CollectionItem"
Enum = "DEPRECATED_Enum"
WorkflowField = "DEPRECATED_WorkflowField"
IsIntermediate = "DEPRECATED_IsIntermediate"
BoardField = "DEPRECATED_BoardField"
MetadataItem = "DEPRECATED_MetadataItem"
MetadataItemCollection = "DEPRECATED_MetadataItemCollection"
MetadataItemPolymorphic = "DEPRECATED_MetadataItemPolymorphic"
MetadataDict = "DEPRECATED_MetadataDict"
# Deprecated Model Field Types - use ui_model_[base|type|variant|format] instead
MainModel = "DEPRECATED_MainModelField"
CogView4MainModel = "DEPRECATED_CogView4MainModelField"
FluxMainModel = "DEPRECATED_FluxMainModelField"
SD3MainModel = "DEPRECATED_SD3MainModelField"
SDXLMainModel = "DEPRECATED_SDXLMainModelField"
SDXLRefinerModel = "DEPRECATED_SDXLRefinerModelField"
ONNXModel = "DEPRECATED_ONNXModelField"
VAEModel = "DEPRECATED_VAEModelField"
FluxVAEModel = "DEPRECATED_FluxVAEModelField"
LoRAModel = "DEPRECATED_LoRAModelField"
ControlNetModel = "DEPRECATED_ControlNetModelField"
IPAdapterModel = "DEPRECATED_IPAdapterModelField"
T2IAdapterModel = "DEPRECATED_T2IAdapterModelField"
T5EncoderModel = "DEPRECATED_T5EncoderModelField"
CLIPEmbedModel = "DEPRECATED_CLIPEmbedModelField"
CLIPLEmbedModel = "DEPRECATED_CLIPLEmbedModelField"
CLIPGEmbedModel = "DEPRECATED_CLIPGEmbedModelField"
SpandrelImageToImageModel = "DEPRECATED_SpandrelImageToImageModelField"
ControlLoRAModel = "DEPRECATED_ControlLoRAModelField"
SigLipModel = "DEPRECATED_SigLipModelField"
FluxReduxModel = "DEPRECATED_FluxReduxModelField"
LlavaOnevisionModel = "DEPRECATED_LLaVAModelField"
Imagen3Model = "DEPRECATED_Imagen3ModelField"
Imagen4Model = "DEPRECATED_Imagen4ModelField"
ChatGPT4oModel = "DEPRECATED_ChatGPT4oModelField"
Gemini2_5Model = "DEPRECATED_Gemini2_5ModelField"
FluxKontextModel = "DEPRECATED_FluxKontextModelField"
Veo3Model = "DEPRECATED_Veo3ModelField"
RunwayModel = "DEPRECATED_RunwayModelField"
# endregion
class UIComponent(str, Enum, metaclass=MetaEnum):
"""
@@ -143,6 +154,7 @@ class FieldDescriptions:
clip = "CLIP (tokenizer, text encoder, LoRAs) and skipped layer count"
t5_encoder = "T5 tokenizer and text encoder"
glm_encoder = "GLM (THUDM) tokenizer and text encoder"
qwen3_encoder = "Qwen3 tokenizer and text encoder"
clip_embed_model = "CLIP Embed loader"
clip_g_model = "CLIP-G Embed loader"
unet = "UNet (scheduler, LoRAs)"
@@ -158,6 +170,7 @@ class FieldDescriptions:
flux_model = "Flux model (Transformer) to load"
sd3_model = "SD3 model (MMDiTX) to load"
cogview4_model = "CogView4 model (Transformer) to load"
z_image_model = "Z-Image model (Transformer) to load"
sdxl_main_model = "SDXL Main model (UNet, VAE, CLIP1, CLIP2) to load"
sdxl_refiner_model = "SDXL Refiner Main Modde (UNet, VAE, CLIP2) to load"
onnx_main_model = "ONNX Main model (UNet, VAE, CLIP) to load"
@@ -230,6 +243,12 @@ class BoardField(BaseModel):
board_id: str = Field(description="The id of the board")
class StylePresetField(BaseModel):
"""A style preset primitive field"""
style_preset_id: str = Field(description="The id of the style preset")
class DenoiseMaskField(BaseModel):
"""An inpaint mask field"""
@@ -310,6 +329,17 @@ class CogView4ConditioningField(BaseModel):
conditioning_name: str = Field(description="The name of conditioning tensor")
class ZImageConditioningField(BaseModel):
"""A Z-Image conditioning tensor primitive value"""
conditioning_name: str = Field(description="The name of conditioning tensor")
mask: Optional[TensorField] = Field(
default=None,
description="The mask associated with this conditioning tensor for regional prompting. "
"Excluded regions should be set to False, included regions should be set to True.",
)
class ConditioningField(BaseModel):
"""A conditioning tensor primitive value"""
@@ -321,14 +351,9 @@ class ConditioningField(BaseModel):
)
class BoundingBoxField(BaseModel):
class BoundingBoxField(BoundingBox):
"""A bounding box primitive value."""
x_min: int = Field(ge=0, description="The minimum x-coordinate of the bounding box (inclusive).")
x_max: int = Field(ge=0, description="The maximum x-coordinate of the bounding box (exclusive).")
y_min: int = Field(ge=0, description="The minimum y-coordinate of the bounding box (inclusive).")
y_max: int = Field(ge=0, description="The maximum y-coordinate of the bounding box (exclusive).")
score: Optional[float] = Field(
default=None,
ge=0.0,
@@ -337,21 +362,6 @@ class BoundingBoxField(BaseModel):
"when the bounding box was produced by a detector and has an associated confidence score.",
)
@model_validator(mode="after")
def check_coords(self):
if self.x_min > self.x_max:
raise ValueError(f"x_min ({self.x_min}) is greater than x_max ({self.x_max}).")
if self.y_min > self.y_max:
raise ValueError(f"y_min ({self.y_min}) is greater than y_max ({self.y_max}).")
return self
def tuple(self) -> Tuple[int, int, int, int]:
"""
Returns the bounding box as a tuple suitable for use with PIL's `Image.crop()` method.
This method returns a tuple of the form (left, upper, right, lower) == (x_min, y_min, x_max, y_max).
"""
return (self.x_min, self.y_min, self.x_max, self.y_max)
class MetadataField(RootModel[dict[str, Any]]):
"""
@@ -418,10 +428,15 @@ class InputFieldJSONSchemaExtra(BaseModel):
ui_component: Optional[UIComponent] = None
ui_order: Optional[int] = None
ui_choice_labels: Optional[dict[str, str]] = None
ui_model_base: Optional[list[BaseModelType]] = None
ui_model_type: Optional[list[ModelType]] = None
ui_model_variant: Optional[list[ClipVariantType | ModelVariantType]] = None
ui_model_format: Optional[list[ModelFormat]] = None
model_config = ConfigDict(
validate_assignment=True,
json_schema_serialization_defaults_required=True,
use_enum_values=True,
)
@@ -474,16 +489,100 @@ class OutputFieldJSONSchemaExtra(BaseModel):
"""
field_kind: FieldKind
ui_hidden: bool
ui_type: Optional[UIType]
ui_order: Optional[int]
ui_hidden: bool = False
ui_order: Optional[int] = None
ui_type: Optional[UIType] = None
model_config = ConfigDict(
validate_assignment=True,
json_schema_serialization_defaults_required=True,
use_enum_values=True,
)
def migrate_model_ui_type(ui_type: UIType | str, json_schema_extra: dict[str, Any]) -> bool:
"""Migrate deprecated model-specifier ui_type values to new-style ui_model_[base|type|variant|format] in json_schema_extra."""
if not isinstance(ui_type, UIType):
ui_type = UIType(ui_type)
ui_model_type: list[ModelType] | None = None
ui_model_base: list[BaseModelType] | None = None
ui_model_format: list[ModelFormat] | None = None
ui_model_variant: list[ClipVariantType | ModelVariantType] | None = None
match ui_type:
case UIType.MainModel:
ui_model_base = [BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2]
ui_model_type = [ModelType.Main]
case UIType.CogView4MainModel:
ui_model_base = [BaseModelType.CogView4]
ui_model_type = [ModelType.Main]
case UIType.FluxMainModel:
ui_model_base = [BaseModelType.Flux]
ui_model_type = [ModelType.Main]
case UIType.SD3MainModel:
ui_model_base = [BaseModelType.StableDiffusion3]
ui_model_type = [ModelType.Main]
case UIType.SDXLMainModel:
ui_model_base = [BaseModelType.StableDiffusionXL]
ui_model_type = [ModelType.Main]
case UIType.SDXLRefinerModel:
ui_model_base = [BaseModelType.StableDiffusionXLRefiner]
ui_model_type = [ModelType.Main]
case UIType.VAEModel:
ui_model_type = [ModelType.VAE]
case UIType.FluxVAEModel:
ui_model_base = [BaseModelType.Flux, BaseModelType.Flux2]
ui_model_type = [ModelType.VAE]
case UIType.LoRAModel:
ui_model_type = [ModelType.LoRA]
case UIType.ControlNetModel:
ui_model_type = [ModelType.ControlNet]
case UIType.IPAdapterModel:
ui_model_type = [ModelType.IPAdapter]
case UIType.T2IAdapterModel:
ui_model_type = [ModelType.T2IAdapter]
case UIType.T5EncoderModel:
ui_model_type = [ModelType.T5Encoder]
case UIType.CLIPEmbedModel:
ui_model_type = [ModelType.CLIPEmbed]
case UIType.CLIPLEmbedModel:
ui_model_type = [ModelType.CLIPEmbed]
ui_model_variant = [ClipVariantType.L]
case UIType.CLIPGEmbedModel:
ui_model_type = [ModelType.CLIPEmbed]
ui_model_variant = [ClipVariantType.G]
case UIType.SpandrelImageToImageModel:
ui_model_type = [ModelType.SpandrelImageToImage]
case UIType.ControlLoRAModel:
ui_model_type = [ModelType.ControlLoRa]
case UIType.SigLipModel:
ui_model_type = [ModelType.SigLIP]
case UIType.FluxReduxModel:
ui_model_type = [ModelType.FluxRedux]
case UIType.LlavaOnevisionModel:
ui_model_type = [ModelType.LlavaOnevision]
case _:
pass
did_migrate = False
if ui_model_type is not None:
json_schema_extra["ui_model_type"] = [m.value for m in ui_model_type]
did_migrate = True
if ui_model_base is not None:
json_schema_extra["ui_model_base"] = [m.value for m in ui_model_base]
did_migrate = True
if ui_model_format is not None:
json_schema_extra["ui_model_format"] = [m.value for m in ui_model_format]
did_migrate = True
if ui_model_variant is not None:
json_schema_extra["ui_model_variant"] = [m.value for m in ui_model_variant]
did_migrate = True
return did_migrate
def InputField(
# copied from pydantic's Field
# TODO: Can we support default_factory?
@@ -510,35 +609,63 @@ def InputField(
ui_hidden: Optional[bool] = None,
ui_order: Optional[int] = None,
ui_choice_labels: Optional[dict[str, str]] = None,
ui_model_base: Optional[BaseModelType | list[BaseModelType]] = None,
ui_model_type: Optional[ModelType | list[ModelType]] = None,
ui_model_variant: Optional[ClipVariantType | ModelVariantType | list[ClipVariantType | ModelVariantType]] = None,
ui_model_format: Optional[ModelFormat | list[ModelFormat]] = None,
) -> Any:
"""
Creates an input field for an invocation.
This is a wrapper for Pydantic's [Field](https://docs.pydantic.dev/latest/api/fields/#pydantic.fields.Field) \
This is a wrapper for Pydantic's [Field](https://docs.pydantic.dev/latest/api/fields/#pydantic.fields.Field)
that adds a few extra parameters to support graph execution and the node editor UI.
:param Input input: [Input.Any] The kind of input this field requires. \
`Input.Direct` means a value must be provided on instantiation. \
`Input.Connection` means the value must be provided by a connection. \
`Input.Any` means either will do.
If the field is a `ModelIdentifierField`, use the `ui_model_[base|type|variant|format]` args to filter the model list
in the Workflow Editor. Otherwise, use `ui_type` to provide extra type hints for the UI.
:param UIType ui_type: [None] Optionally provides an extra type hint for the UI. \
In some situations, the field's type is not enough to infer the correct UI type. \
For example, model selection fields should render a dropdown UI component to select a model. \
Internally, there is no difference between SD-1, SD-2 and SDXL model fields, they all use \
`MainModelField`. So to ensure the base-model-specific UI is rendered, you can use \
`UIType.SDXLMainModelField` to indicate that the field is an SDXL main model field.
Don't use both `ui_type` and `ui_model_[base|type|variant|format]` - if both are provided, a warning will be
logged and `ui_type` will be ignored.
:param UIComponent ui_component: [None] Optionally specifies a specific component to use in the UI. \
The UI will always render a suitable component, but sometimes you want something different than the default. \
For example, a `string` field will default to a single-line input, but you may want a multi-line textarea instead. \
For this case, you could provide `UIComponent.Textarea`.
Args:
input: The kind of input this field requires.
- `Input.Direct` means a value must be provided on instantiation.
- `Input.Connection` means the value must be provided by a connection.
- `Input.Any` means either will do.
:param bool ui_hidden: [False] Specifies whether or not this field should be hidden in the UI.
ui_type: Optionally provides an extra type hint for the UI. In some situations, the field's type is not enough
to infer the correct UI type. For example, Scheduler fields are enums, but we want to render a special scheduler
dropdown in the UI. Use `UIType.Scheduler` to indicate this.
:param int ui_order: [None] Specifies the order in which this field should be rendered in the UI.
ui_component: Optionally specifies a specific component to use in the UI. The UI will always render a suitable
component, but sometimes you want something different than the default. For example, a `string` field will
default to a single-line input, but you may want a multi-line textarea instead. In this case, you could use
`UIComponent.Textarea`.
:param dict[str, str] ui_choice_labels: [None] Specifies the labels to use for the choices in an enum field.
ui_hidden: Specifies whether or not this field should be hidden in the UI.
ui_order: Specifies the order in which this field should be rendered in the UI. If omitted, the field will be
rendered after all fields with an explicit order, in the order they are defined in the Invocation class.
ui_model_base: Specifies the base model architectures to filter the model list by in the Workflow Editor. For
example, `ui_model_base=BaseModelType.StableDiffusionXL` will show only SDXL architecture models. This arg is
only valid if this Input field is annotated as a `ModelIdentifierField`.
ui_model_type: Specifies the model type(s) to filter the model list by in the Workflow Editor. For example,
`ui_model_type=ModelType.VAE` will show only VAE models. This arg is only valid if this Input field is
annotated as a `ModelIdentifierField`.
ui_model_variant: Specifies the model variant(s) to filter the model list by in the Workflow Editor. For example,
`ui_model_variant=ModelVariantType.Inpainting` will show only inpainting models. This arg is only valid if this
Input field is annotated as a `ModelIdentifierField`.
ui_model_format: Specifies the model format(s) to filter the model list by in the Workflow Editor. For example,
`ui_model_format=ModelFormat.Diffusers` will show only models in the diffusers format. This arg is only valid
if this Input field is annotated as a `ModelIdentifierField`.
ui_choice_labels: Specifies the labels to use for the choices in an enum field. If omitted, the enum values
will be used. This arg is only valid if the field is annotated with as a `Literal`. For example,
`Literal["choice1", "choice2", "choice3"]` with `ui_choice_labels={"choice1": "Choice 1", "choice2": "Choice 2",
"choice3": "Choice 3"}` will render a dropdown with the labels "Choice 1", "Choice 2" and "Choice 3".
"""
json_schema_extra_ = InputFieldJSONSchemaExtra(
@@ -546,8 +673,6 @@ def InputField(
field_kind=FieldKind.Input,
)
if ui_type is not None:
json_schema_extra_.ui_type = ui_type
if ui_component is not None:
json_schema_extra_.ui_component = ui_component
if ui_hidden is not None:
@@ -556,6 +681,28 @@ def InputField(
json_schema_extra_.ui_order = ui_order
if ui_choice_labels is not None:
json_schema_extra_.ui_choice_labels = ui_choice_labels
if ui_model_base is not None:
if isinstance(ui_model_base, list):
json_schema_extra_.ui_model_base = ui_model_base
else:
json_schema_extra_.ui_model_base = [ui_model_base]
if ui_model_type is not None:
if isinstance(ui_model_type, list):
json_schema_extra_.ui_model_type = ui_model_type
else:
json_schema_extra_.ui_model_type = [ui_model_type]
if ui_model_variant is not None:
if isinstance(ui_model_variant, list):
json_schema_extra_.ui_model_variant = ui_model_variant
else:
json_schema_extra_.ui_model_variant = [ui_model_variant]
if ui_model_format is not None:
if isinstance(ui_model_format, list):
json_schema_extra_.ui_model_format = ui_model_format
else:
json_schema_extra_.ui_model_format = [ui_model_format]
if ui_type is not None:
json_schema_extra_.ui_type = ui_type
"""
There is a conflict between the typing of invocation definitions and the typing of an invocation's
@@ -657,20 +804,20 @@ def OutputField(
"""
Creates an output field for an invocation output.
This is a wrapper for Pydantic's [Field](https://docs.pydantic.dev/1.10/usage/schema/#field-customization) \
This is a wrapper for Pydantic's [Field](https://docs.pydantic.dev/1.10/usage/schema/#field-customization)
that adds a few extra parameters to support graph execution and the node editor UI.
:param UIType ui_type: [None] Optionally provides an extra type hint for the UI. \
In some situations, the field's type is not enough to infer the correct UI type. \
For example, model selection fields should render a dropdown UI component to select a model. \
Internally, there is no difference between SD-1, SD-2 and SDXL model fields, they all use \
`MainModelField`. So to ensure the base-model-specific UI is rendered, you can use \
`UIType.SDXLMainModelField` to indicate that the field is an SDXL main model field.
Args:
ui_type: Optionally provides an extra type hint for the UI. In some situations, the field's type is not enough
to infer the correct UI type. For example, Scheduler fields are enums, but we want to render a special scheduler
dropdown in the UI. Use `UIType.Scheduler` to indicate this.
:param bool ui_hidden: [False] Specifies whether or not this field should be hidden in the UI. \
ui_hidden: Specifies whether or not this field should be hidden in the UI.
:param int ui_order: [None] Specifies the order in which this field should be rendered in the UI. \
ui_order: Specifies the order in which this field should be rendered in the UI. If omitted, the field will be
rendered after all fields with an explicit order, in the order they are defined in the Invocation class.
"""
return Field(
default=default,
title=title,
@@ -688,9 +835,9 @@ def OutputField(
min_length=min_length,
max_length=max_length,
json_schema_extra=OutputFieldJSONSchemaExtra(
ui_type=ui_type,
ui_hidden=ui_hidden,
ui_order=ui_order,
ui_type=ui_type,
field_kind=FieldKind.Output,
).model_dump(exclude_none=True),
)

View File

@@ -0,0 +1,530 @@
"""Flux2 Klein Denoise Invocation.
Run denoising process with a FLUX.2 Klein transformer model.
Uses Qwen3 conditioning instead of CLIP+T5.
"""
from contextlib import ExitStack
from typing import Callable, Iterator, Optional, Tuple
import torch
import torchvision.transforms as tv_transforms
from torchvision.transforms.functional import resize as tv_resize
from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
from invokeai.app.invocations.fields import (
DenoiseMaskField,
FieldDescriptions,
FluxConditioningField,
FluxKontextConditioningField,
Input,
InputField,
LatentsField,
)
from invokeai.app.invocations.model import TransformerField, VAEField
from invokeai.app.invocations.primitives import LatentsOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.flux.sampling_utils import clip_timestep_schedule_fractional
from invokeai.backend.flux.schedulers import FLUX_SCHEDULER_LABELS, FLUX_SCHEDULER_MAP, FLUX_SCHEDULER_NAME_VALUES
from invokeai.backend.flux2.denoise import denoise
from invokeai.backend.flux2.ref_image_extension import Flux2RefImageExtension
from invokeai.backend.flux2.sampling_utils import (
compute_empirical_mu,
generate_img_ids_flux2,
get_noise_flux2,
get_schedule_flux2,
pack_flux2,
unpack_flux2,
)
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelFormat, ModelType
from invokeai.backend.patches.layer_patcher import LayerPatcher
from invokeai.backend.patches.lora_conversions.flux_bfl_peft_lora_conversion_utils import (
convert_bfl_lora_patch_to_diffusers,
)
from invokeai.backend.patches.lora_conversions.flux_lora_constants import FLUX_LORA_TRANSFORMER_PREFIX
from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
from invokeai.backend.rectified_flow.rectified_flow_inpaint_extension import RectifiedFlowInpaintExtension
from invokeai.backend.stable_diffusion.diffusers_pipeline import PipelineIntermediateState
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import FLUXConditioningInfo
from invokeai.backend.util.devices import TorchDevice
@invocation(
"flux2_denoise",
title="FLUX2 Denoise",
tags=["image", "flux", "flux2", "klein", "denoise"],
category="image",
version="1.4.0",
classification=Classification.Prototype,
)
class Flux2DenoiseInvocation(BaseInvocation):
"""Run denoising process with a FLUX.2 Klein transformer model.
This node is designed for FLUX.2 Klein models which use Qwen3 as the text encoder.
It does not support ControlNet, IP-Adapters, or regional prompting.
"""
latents: Optional[LatentsField] = InputField(
default=None,
description=FieldDescriptions.latents,
input=Input.Connection,
)
denoise_mask: Optional[DenoiseMaskField] = InputField(
default=None,
description=FieldDescriptions.denoise_mask,
input=Input.Connection,
)
denoising_start: float = InputField(
default=0.0,
ge=0,
le=1,
description=FieldDescriptions.denoising_start,
)
denoising_end: float = InputField(
default=1.0,
ge=0,
le=1,
description=FieldDescriptions.denoising_end,
)
add_noise: bool = InputField(default=True, description="Add noise based on denoising start.")
transformer: TransformerField = InputField(
description=FieldDescriptions.flux_model,
input=Input.Connection,
title="Transformer",
)
positive_text_conditioning: FluxConditioningField = InputField(
description=FieldDescriptions.positive_cond,
input=Input.Connection,
)
negative_text_conditioning: Optional[FluxConditioningField] = InputField(
default=None,
description="Negative conditioning tensor. Can be None if cfg_scale is 1.0.",
input=Input.Connection,
)
cfg_scale: float = InputField(
default=1.0,
description=FieldDescriptions.cfg_scale,
title="CFG Scale",
)
width: int = InputField(default=1024, multiple_of=16, description="Width of the generated image.")
height: int = InputField(default=1024, multiple_of=16, description="Height of the generated image.")
num_steps: int = InputField(
default=4,
description="Number of diffusion steps. Use 4 for distilled models, 28+ for base models.",
)
scheduler: FLUX_SCHEDULER_NAME_VALUES = InputField(
default="euler",
description="Scheduler (sampler) for the denoising process. 'euler' is fast and standard. "
"'heun' is 2nd-order (better quality, 2x slower). 'lcm' is optimized for few steps.",
ui_choice_labels=FLUX_SCHEDULER_LABELS,
)
seed: int = InputField(default=0, description="Randomness seed for reproducibility.")
vae: VAEField = InputField(
description="FLUX.2 VAE model (required for BN statistics).",
input=Input.Connection,
)
kontext_conditioning: FluxKontextConditioningField | list[FluxKontextConditioningField] | None = InputField(
default=None,
description="FLUX Kontext conditioning (reference images for multi-reference image editing).",
input=Input.Connection,
title="Reference Images",
)
def _get_bn_stats(self, context: InvocationContext) -> Optional[Tuple[torch.Tensor, torch.Tensor]]:
"""Extract BN statistics from the FLUX.2 VAE.
The FLUX.2 VAE uses batch normalization on the patchified 128-channel representation.
IMPORTANT: BFL FLUX.2 VAE uses affine=False, so there are NO learnable weight/bias.
BN formula (affine=False): y = (x - mean) / std
Inverse: x = y * std + mean
Returns:
Tuple of (bn_mean, bn_std) tensors of shape (128,), or None if BN layer not found.
"""
with context.models.load(self.vae.vae).model_on_device() as (_, vae):
# Ensure VAE is in eval mode to prevent BN stats from being updated
vae.eval()
# Try to find the BN layer - it may be at different locations depending on model format
bn_layer = None
if hasattr(vae, "bn"):
bn_layer = vae.bn
elif hasattr(vae, "batch_norm"):
bn_layer = vae.batch_norm
elif hasattr(vae, "encoder") and hasattr(vae.encoder, "bn"):
bn_layer = vae.encoder.bn
if bn_layer is None:
return None
# Verify running statistics are initialized
if bn_layer.running_mean is None or bn_layer.running_var is None:
return None
# Get BN running statistics from VAE
bn_mean = bn_layer.running_mean.clone() # Shape: (128,)
bn_var = bn_layer.running_var.clone() # Shape: (128,)
bn_eps = bn_layer.eps if hasattr(bn_layer, "eps") else 1e-4 # BFL uses 1e-4
bn_std = torch.sqrt(bn_var + bn_eps)
return bn_mean, bn_std
def _bn_normalize(
self,
x: torch.Tensor,
bn_mean: torch.Tensor,
bn_std: torch.Tensor,
) -> torch.Tensor:
"""Apply BN normalization to packed latents.
BN formula (affine=False): y = (x - mean) / std
Args:
x: Packed latents of shape (B, seq, 128).
bn_mean: BN running mean of shape (128,).
bn_std: BN running std of shape (128,).
Returns:
Normalized latents of same shape.
"""
# x: (B, seq, 128), params: (128,) -> broadcast over batch and sequence dims
bn_mean = bn_mean.to(x.device, x.dtype)
bn_std = bn_std.to(x.device, x.dtype)
return (x - bn_mean) / bn_std
def _bn_denormalize(
self,
x: torch.Tensor,
bn_mean: torch.Tensor,
bn_std: torch.Tensor,
) -> torch.Tensor:
"""Apply BN denormalization to packed latents (inverse of normalization).
Inverse BN (affine=False): x = y * std + mean
Args:
x: Packed latents of shape (B, seq, 128).
bn_mean: BN running mean of shape (128,).
bn_std: BN running std of shape (128,).
Returns:
Denormalized latents of same shape.
"""
# x: (B, seq, 128), params: (128,) -> broadcast over batch and sequence dims
bn_mean = bn_mean.to(x.device, x.dtype)
bn_std = bn_std.to(x.device, x.dtype)
return x * bn_std + bn_mean
@torch.no_grad()
def invoke(self, context: InvocationContext) -> LatentsOutput:
latents = self._run_diffusion(context)
latents = latents.detach().to("cpu")
name = context.tensors.save(tensor=latents)
return LatentsOutput.build(latents_name=name, latents=latents, seed=None)
def _run_diffusion(self, context: InvocationContext) -> torch.Tensor:
inference_dtype = torch.bfloat16
device = TorchDevice.choose_torch_device()
# Get BN statistics from VAE for latent denormalization (optional)
# BFL FLUX.2 VAE uses affine=False, so only mean/std are needed
# Some VAE formats (e.g. diffusers) may not expose BN stats directly
bn_stats = self._get_bn_stats(context)
bn_mean, bn_std = bn_stats if bn_stats is not None else (None, None)
# Load the input latents, if provided
init_latents = context.tensors.load(self.latents.latents_name) if self.latents else None
if init_latents is not None:
init_latents = init_latents.to(device=device, dtype=inference_dtype)
# Prepare input noise (FLUX.2 uses 32 channels)
noise = get_noise_flux2(
num_samples=1,
height=self.height,
width=self.width,
device=device,
dtype=inference_dtype,
seed=self.seed,
)
b, _c, latent_h, latent_w = noise.shape
packed_h = latent_h // 2
packed_w = latent_w // 2
# Load the conditioning data
pos_cond_data = context.conditioning.load(self.positive_text_conditioning.conditioning_name)
assert len(pos_cond_data.conditionings) == 1
pos_flux_conditioning = pos_cond_data.conditionings[0]
assert isinstance(pos_flux_conditioning, FLUXConditioningInfo)
pos_flux_conditioning = pos_flux_conditioning.to(dtype=inference_dtype, device=device)
# Qwen3 stacked embeddings (stored in t5_embeds field for compatibility)
txt = pos_flux_conditioning.t5_embeds
# Generate text position IDs (4D format for FLUX.2: T, H, W, L)
# FLUX.2 uses 4D position coordinates for its rotary position embeddings
# IMPORTANT: Position IDs must be int64 (long) dtype
# Diffusers uses: T=0, H=0, W=0, L=0..seq_len-1
seq_len = txt.shape[1]
txt_ids = torch.zeros(1, seq_len, 4, device=device, dtype=torch.long)
txt_ids[..., 3] = torch.arange(seq_len, device=device, dtype=torch.long) # L coordinate varies
# Load negative conditioning if provided
neg_txt = None
neg_txt_ids = None
if self.negative_text_conditioning is not None:
neg_cond_data = context.conditioning.load(self.negative_text_conditioning.conditioning_name)
assert len(neg_cond_data.conditionings) == 1
neg_flux_conditioning = neg_cond_data.conditionings[0]
assert isinstance(neg_flux_conditioning, FLUXConditioningInfo)
neg_flux_conditioning = neg_flux_conditioning.to(dtype=inference_dtype, device=device)
neg_txt = neg_flux_conditioning.t5_embeds
# For text tokens: T=0, H=0, W=0, L=0..seq_len-1 (only L varies per token)
neg_seq_len = neg_txt.shape[1]
neg_txt_ids = torch.zeros(1, neg_seq_len, 4, device=device, dtype=torch.long)
neg_txt_ids[..., 3] = torch.arange(neg_seq_len, device=device, dtype=torch.long)
# Validate transformer config
transformer_config = context.models.get_config(self.transformer.transformer)
assert transformer_config.base == BaseModelType.Flux2 and transformer_config.type == ModelType.Main
# Calculate the timestep schedule using FLUX.2 specific schedule
# This matches diffusers' Flux2Pipeline implementation
# Note: Schedule shifting is handled by the scheduler via mu parameter
image_seq_len = packed_h * packed_w
timesteps = get_schedule_flux2(
num_steps=self.num_steps,
image_seq_len=image_seq_len,
)
# Compute mu for dynamic schedule shifting (used by FlowMatchEulerDiscreteScheduler)
mu = compute_empirical_mu(image_seq_len=image_seq_len, num_steps=self.num_steps)
# Clip the timesteps schedule based on denoising_start and denoising_end
timesteps = clip_timestep_schedule_fractional(timesteps, self.denoising_start, self.denoising_end)
# Prepare input latent image
if init_latents is not None:
if self.add_noise:
t_0 = timesteps[0]
x = t_0 * noise + (1.0 - t_0) * init_latents
else:
x = init_latents
else:
if self.denoising_start > 1e-5:
raise ValueError("denoising_start should be 0 when initial latents are not provided.")
x = noise
# If len(timesteps) == 1, then short-circuit
if len(timesteps) <= 1:
return x
# Generate image position IDs (FLUX.2 uses 4D coordinates)
# Position IDs use int64 dtype like diffusers
img_ids = generate_img_ids_flux2(h=latent_h, w=latent_w, batch_size=b, device=device)
# Prepare inpaint mask
inpaint_mask = self._prep_inpaint_mask(context, x)
# Pack all latent tensors
init_latents_packed = pack_flux2(init_latents) if init_latents is not None else None
inpaint_mask_packed = pack_flux2(inpaint_mask) if inpaint_mask is not None else None
noise_packed = pack_flux2(noise)
x = pack_flux2(x)
# BN normalization for img2img/inpainting:
# - The init_latents from VAE encode are NOT BN-normalized
# - The transformer operates in BN-normalized space
# - We must normalize x, init_latents, AND noise for InpaintExtension
# - Output MUST be denormalized after denoising before VAE decode
#
# This ensures that:
# 1. x starts in the correct normalized space for the transformer
# 2. When InpaintExtension merges intermediate_latents with noised_init_latents,
# both are in the same scale/space (noise and init_latents must be in same space
# for the linear interpolation: noised = noise * t + init * (1-t))
if bn_mean is not None and bn_std is not None:
if init_latents_packed is not None:
init_latents_packed = self._bn_normalize(init_latents_packed, bn_mean, bn_std)
# Also normalize noise for InpaintExtension - it's used to compute
# noised_init_latents = noise * t + init_latents * (1-t)
# Both operands must be in the same normalized space
noise_packed = self._bn_normalize(noise_packed, bn_mean, bn_std)
# For img2img/inpainting, x is computed from init_latents and must also be normalized
# For txt2img, x is pure noise (already N(0,1)) - normalizing it would be incorrect
# We detect img2img by checking if init_latents was provided
if init_latents is not None:
x = self._bn_normalize(x, bn_mean, bn_std)
# Verify packed dimensions
assert packed_h * packed_w == x.shape[1]
# Prepare inpaint extension
inpaint_extension: Optional[RectifiedFlowInpaintExtension] = None
if inpaint_mask_packed is not None:
assert init_latents_packed is not None
inpaint_extension = RectifiedFlowInpaintExtension(
init_latents=init_latents_packed,
inpaint_mask=inpaint_mask_packed,
noise=noise_packed,
)
# Prepare CFG scale list
num_steps = len(timesteps) - 1
cfg_scale_list = [self.cfg_scale] * num_steps
# Check if we're doing inpainting (have a mask or a clipped schedule)
is_inpainting = self.denoise_mask is not None or self.denoising_start > 1e-5
# Create scheduler with FLUX.2 Klein configuration
# For inpainting/img2img, use manual Euler stepping to preserve the exact timestep schedule
# For txt2img, use the scheduler with dynamic shifting for optimal results
scheduler = None
if self.scheduler in FLUX_SCHEDULER_MAP and not is_inpainting:
# Only use scheduler for txt2img - use manual Euler for inpainting to preserve exact timesteps
scheduler_class = FLUX_SCHEDULER_MAP[self.scheduler]
# FlowMatchHeunDiscreteScheduler only supports num_train_timesteps and shift parameters
# FlowMatchEulerDiscreteScheduler and FlowMatchLCMScheduler support dynamic shifting
if self.scheduler == "heun":
scheduler = scheduler_class(
num_train_timesteps=1000,
shift=3.0,
)
else:
scheduler = scheduler_class(
num_train_timesteps=1000,
shift=3.0,
use_dynamic_shifting=True,
base_shift=0.5,
max_shift=1.15,
base_image_seq_len=256,
max_image_seq_len=4096,
time_shift_type="exponential",
)
# Prepare reference image extension for FLUX.2 Klein built-in editing
ref_image_extension = None
if self.kontext_conditioning:
ref_image_extension = Flux2RefImageExtension(
context=context,
ref_image_conditioning=self.kontext_conditioning
if isinstance(self.kontext_conditioning, list)
else [self.kontext_conditioning],
vae_field=self.vae,
device=device,
dtype=inference_dtype,
bn_mean=bn_mean,
bn_std=bn_std,
)
with ExitStack() as exit_stack:
# Load the transformer model
(cached_weights, transformer) = exit_stack.enter_context(
context.models.load(self.transformer.transformer).model_on_device()
)
config = transformer_config
# Determine if the model is quantized
if config.format in [ModelFormat.Diffusers]:
model_is_quantized = False
elif config.format in [
ModelFormat.BnbQuantizedLlmInt8b,
ModelFormat.BnbQuantizednf4b,
ModelFormat.GGUFQuantized,
]:
model_is_quantized = True
else:
model_is_quantized = False
# Apply LoRA models to the transformer
exit_stack.enter_context(
LayerPatcher.apply_smart_model_patches(
model=transformer,
patches=self._lora_iterator(context),
prefix=FLUX_LORA_TRANSFORMER_PREFIX,
dtype=inference_dtype,
cached_weights=cached_weights,
force_sidecar_patching=model_is_quantized,
)
)
# Prepare reference image conditioning if provided
img_cond_seq = None
img_cond_seq_ids = None
if ref_image_extension is not None:
# Ensure batch sizes match
ref_image_extension.ensure_batch_size(x.shape[0])
img_cond_seq, img_cond_seq_ids = (
ref_image_extension.ref_image_latents,
ref_image_extension.ref_image_ids,
)
x = denoise(
model=transformer,
img=x,
img_ids=img_ids,
txt=txt,
txt_ids=txt_ids,
timesteps=timesteps,
step_callback=self._build_step_callback(context),
cfg_scale=cfg_scale_list,
neg_txt=neg_txt,
neg_txt_ids=neg_txt_ids,
scheduler=scheduler,
mu=mu,
inpaint_extension=inpaint_extension,
img_cond_seq=img_cond_seq,
img_cond_seq_ids=img_cond_seq_ids,
)
# Apply BN denormalization if BN stats are available
# The diffusers Flux2KleinPipeline applies: latents = latents * bn_std + bn_mean
# This transforms latents from normalized space to VAE's expected input space
if bn_mean is not None and bn_std is not None:
x = self._bn_denormalize(x, bn_mean, bn_std)
x = unpack_flux2(x.float(), self.height, self.width)
return x
def _prep_inpaint_mask(self, context: InvocationContext, latents: torch.Tensor) -> Optional[torch.Tensor]:
"""Prepare the inpaint mask."""
if self.denoise_mask is None:
return None
mask = context.tensors.load(self.denoise_mask.mask_name)
mask = 1.0 - mask
_, _, latent_height, latent_width = latents.shape
mask = tv_resize(
img=mask,
size=[latent_height, latent_width],
interpolation=tv_transforms.InterpolationMode.BILINEAR,
antialias=False,
)
mask = mask.to(device=latents.device, dtype=latents.dtype)
return mask.expand_as(latents)
def _lora_iterator(self, context: InvocationContext) -> Iterator[Tuple[ModelPatchRaw, float]]:
"""Iterate over LoRA models to apply.
Converts BFL-format LoRA keys to diffusers format if needed, since FLUX.2 Klein
uses Flux2Transformer2DModel (diffusers naming) but LoRAs may have been loaded
with BFL naming (e.g. when a Klein 4B LoRA is misidentified as FLUX.1).
"""
for lora in self.transformer.loras:
lora_info = context.models.load(lora.lora)
assert isinstance(lora_info.model, ModelPatchRaw)
converted = convert_bfl_lora_patch_to_diffusers(lora_info.model)
yield (converted, lora.weight)
del lora_info
def _build_step_callback(self, context: InvocationContext) -> Callable[[PipelineIntermediateState], None]:
"""Build a callback for step progress updates."""
def step_callback(state: PipelineIntermediateState) -> None:
latents = state.latents.float()
state.latents = unpack_flux2(latents, self.height, self.width).squeeze()
context.util.flux2_step_callback(state)
return step_callback

View File

@@ -0,0 +1,182 @@
"""FLUX.2 Klein LoRA Loader Invocation.
Applies LoRA models to a FLUX.2 Klein transformer and/or Qwen3 text encoder.
Unlike standard FLUX which uses CLIP+T5, Klein uses only Qwen3 for text encoding.
"""
from typing import Optional
from invokeai.app.invocations.baseinvocation import (
BaseInvocation,
BaseInvocationOutput,
Classification,
invocation,
invocation_output,
)
from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField
from invokeai.app.invocations.model import LoRAField, ModelIdentifierField, Qwen3EncoderField, TransformerField
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
@invocation_output("flux2_klein_lora_loader_output")
class Flux2KleinLoRALoaderOutput(BaseInvocationOutput):
"""FLUX.2 Klein LoRA Loader Output"""
transformer: Optional[TransformerField] = OutputField(
default=None, description=FieldDescriptions.transformer, title="Transformer"
)
qwen3_encoder: Optional[Qwen3EncoderField] = OutputField(
default=None, description=FieldDescriptions.qwen3_encoder, title="Qwen3 Encoder"
)
@invocation(
"flux2_klein_lora_loader",
title="Apply LoRA - Flux2 Klein",
tags=["lora", "model", "flux", "klein", "flux2"],
category="model",
version="1.0.0",
classification=Classification.Prototype,
)
class Flux2KleinLoRALoaderInvocation(BaseInvocation):
"""Apply a LoRA model to a FLUX.2 Klein transformer and/or Qwen3 text encoder."""
lora: ModelIdentifierField = InputField(
description=FieldDescriptions.lora_model,
title="LoRA",
ui_model_base=BaseModelType.Flux2,
ui_model_type=ModelType.LoRA,
)
weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
transformer: TransformerField | None = InputField(
default=None,
description=FieldDescriptions.transformer,
input=Input.Connection,
title="Transformer",
)
qwen3_encoder: Qwen3EncoderField | None = InputField(
default=None,
title="Qwen3 Encoder",
description=FieldDescriptions.qwen3_encoder,
input=Input.Connection,
)
def invoke(self, context: InvocationContext) -> Flux2KleinLoRALoaderOutput:
lora_key = self.lora.key
if not context.models.exists(lora_key):
raise ValueError(f"Unknown lora: {lora_key}!")
# Warn if LoRA variant doesn't match transformer variant
lora_config = context.models.get_config(lora_key)
lora_variant = getattr(lora_config, "variant", None)
if lora_variant and self.transformer is not None:
transformer_config = context.models.get_config(self.transformer.transformer.key)
transformer_variant = getattr(transformer_config, "variant", None)
if transformer_variant and lora_variant != transformer_variant:
context.logger.warning(
f"LoRA variant mismatch: LoRA '{lora_config.name}' is for {lora_variant.value} "
f"but transformer is {transformer_variant.value}. This may cause shape errors."
)
# Check for existing LoRAs with the same key.
if self.transformer and any(lora.lora.key == lora_key for lora in self.transformer.loras):
raise ValueError(f'LoRA "{lora_key}" already applied to transformer.')
if self.qwen3_encoder and any(lora.lora.key == lora_key for lora in self.qwen3_encoder.loras):
raise ValueError(f'LoRA "{lora_key}" already applied to Qwen3 encoder.')
output = Flux2KleinLoRALoaderOutput()
# Attach LoRA layers to the models.
if self.transformer is not None:
output.transformer = self.transformer.model_copy(deep=True)
output.transformer.loras.append(
LoRAField(
lora=self.lora,
weight=self.weight,
)
)
if self.qwen3_encoder is not None:
output.qwen3_encoder = self.qwen3_encoder.model_copy(deep=True)
output.qwen3_encoder.loras.append(
LoRAField(
lora=self.lora,
weight=self.weight,
)
)
return output
@invocation(
"flux2_klein_lora_collection_loader",
title="Apply LoRA Collection - Flux2 Klein",
tags=["lora", "model", "flux", "klein", "flux2"],
category="model",
version="1.0.0",
classification=Classification.Prototype,
)
class Flux2KleinLoRACollectionLoader(BaseInvocation):
"""Applies a collection of LoRAs to a FLUX.2 Klein transformer and/or Qwen3 text encoder."""
loras: Optional[LoRAField | list[LoRAField]] = InputField(
default=None, description="LoRA models and weights. May be a single LoRA or collection.", title="LoRAs"
)
transformer: Optional[TransformerField] = InputField(
default=None,
description=FieldDescriptions.transformer,
input=Input.Connection,
title="Transformer",
)
qwen3_encoder: Qwen3EncoderField | None = InputField(
default=None,
title="Qwen3 Encoder",
description=FieldDescriptions.qwen3_encoder,
input=Input.Connection,
)
def invoke(self, context: InvocationContext) -> Flux2KleinLoRALoaderOutput:
output = Flux2KleinLoRALoaderOutput()
loras = self.loras if isinstance(self.loras, list) else [self.loras]
added_loras: list[str] = []
if self.transformer is not None:
output.transformer = self.transformer.model_copy(deep=True)
if self.qwen3_encoder is not None:
output.qwen3_encoder = self.qwen3_encoder.model_copy(deep=True)
for lora in loras:
if lora is None:
continue
if lora.lora.key in added_loras:
continue
if not context.models.exists(lora.lora.key):
raise Exception(f"Unknown lora: {lora.lora.key}!")
assert lora.lora.base in (BaseModelType.Flux, BaseModelType.Flux2)
# Warn if LoRA variant doesn't match transformer variant
lora_config = context.models.get_config(lora.lora.key)
lora_variant = getattr(lora_config, "variant", None)
if lora_variant and self.transformer is not None:
transformer_config = context.models.get_config(self.transformer.transformer.key)
transformer_variant = getattr(transformer_config, "variant", None)
if transformer_variant and lora_variant != transformer_variant:
context.logger.warning(
f"LoRA variant mismatch: LoRA '{lora_config.name}' is for {lora_variant.value} "
f"but transformer is {transformer_variant.value}. This may cause shape errors."
)
added_loras.append(lora.lora.key)
if self.transformer is not None and output.transformer is not None:
output.transformer.loras.append(lora)
if self.qwen3_encoder is not None and output.qwen3_encoder is not None:
output.qwen3_encoder.loras.append(lora)
return output

View File

@@ -0,0 +1,222 @@
"""Flux2 Klein Model Loader Invocation.
Loads a Flux2 Klein model with its Qwen3 text encoder and VAE.
Unlike standard FLUX which uses CLIP+T5, Klein uses only Qwen3.
"""
from typing import Literal, Optional
from invokeai.app.invocations.baseinvocation import (
BaseInvocation,
BaseInvocationOutput,
Classification,
invocation,
invocation_output,
)
from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField
from invokeai.app.invocations.model import (
ModelIdentifierField,
Qwen3EncoderField,
TransformerField,
VAEField,
)
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.taxonomy import (
BaseModelType,
Flux2VariantType,
ModelFormat,
ModelType,
Qwen3VariantType,
SubModelType,
)
@invocation_output("flux2_klein_model_loader_output")
class Flux2KleinModelLoaderOutput(BaseInvocationOutput):
"""Flux2 Klein model loader output."""
transformer: TransformerField = OutputField(description=FieldDescriptions.transformer, title="Transformer")
qwen3_encoder: Qwen3EncoderField = OutputField(description=FieldDescriptions.qwen3_encoder, title="Qwen3 Encoder")
vae: VAEField = OutputField(description=FieldDescriptions.vae, title="VAE")
max_seq_len: Literal[256, 512] = OutputField(
description="The max sequence length for the Qwen3 encoder.",
title="Max Seq Length",
)
@invocation(
"flux2_klein_model_loader",
title="Main Model - Flux2 Klein",
tags=["model", "flux", "klein", "qwen3"],
category="model",
version="1.0.0",
classification=Classification.Prototype,
)
class Flux2KleinModelLoaderInvocation(BaseInvocation):
"""Loads a Flux2 Klein model, outputting its submodels.
Flux2 Klein uses Qwen3 as the text encoder instead of CLIP+T5.
It uses a 32-channel VAE (AutoencoderKLFlux2) instead of the 16-channel FLUX.1 VAE.
When using a Diffusers format model, both VAE and Qwen3 encoder are extracted
automatically from the main model. You can override with standalone models:
- Transformer: Always from Flux2 Klein main model
- VAE: From main model (Diffusers) or standalone VAE
- Qwen3 Encoder: From main model (Diffusers) or standalone Qwen3 model
"""
model: ModelIdentifierField = InputField(
description=FieldDescriptions.flux_model,
input=Input.Direct,
ui_model_base=BaseModelType.Flux2,
ui_model_type=ModelType.Main,
title="Transformer",
)
vae_model: Optional[ModelIdentifierField] = InputField(
default=None,
description="Standalone VAE model. Flux2 Klein uses the same VAE as FLUX (16-channel). "
"If not provided, VAE will be loaded from the Qwen3 Source model.",
input=Input.Direct,
ui_model_base=[BaseModelType.Flux, BaseModelType.Flux2],
ui_model_type=ModelType.VAE,
title="VAE",
)
qwen3_encoder_model: Optional[ModelIdentifierField] = InputField(
default=None,
description="Standalone Qwen3 Encoder model. "
"If not provided, encoder will be loaded from the Qwen3 Source model.",
input=Input.Direct,
ui_model_type=ModelType.Qwen3Encoder,
title="Qwen3 Encoder",
)
qwen3_source_model: Optional[ModelIdentifierField] = InputField(
default=None,
description="Diffusers Flux2 Klein model to extract VAE and/or Qwen3 encoder from. "
"Use this if you don't have separate VAE/Qwen3 models. "
"Ignored if both VAE and Qwen3 Encoder are provided separately.",
input=Input.Direct,
ui_model_base=BaseModelType.Flux2,
ui_model_type=ModelType.Main,
ui_model_format=ModelFormat.Diffusers,
title="Qwen3 Source (Diffusers)",
)
max_seq_len: Literal[256, 512] = InputField(
default=512,
description="Max sequence length for the Qwen3 encoder.",
title="Max Seq Length",
)
def invoke(self, context: InvocationContext) -> Flux2KleinModelLoaderOutput:
# Transformer always comes from the main model
transformer = self.model.model_copy(update={"submodel_type": SubModelType.Transformer})
# Check if main model is Diffusers format (can extract VAE directly)
main_config = context.models.get_config(self.model)
main_is_diffusers = main_config.format == ModelFormat.Diffusers
# Determine VAE source
# IMPORTANT: FLUX.2 Klein uses a 32-channel VAE (AutoencoderKLFlux2), not the 16-channel FLUX.1 VAE.
# The VAE should come from the FLUX.2 Klein Diffusers model, not a separate FLUX VAE.
if self.vae_model is not None:
# Use standalone VAE (user explicitly selected one)
vae = self.vae_model.model_copy(update={"submodel_type": SubModelType.VAE})
elif main_is_diffusers:
# Extract VAE from main model (recommended for FLUX.2)
vae = self.model.model_copy(update={"submodel_type": SubModelType.VAE})
elif self.qwen3_source_model is not None:
# Extract from Qwen3 source Diffusers model
self._validate_diffusers_format(context, self.qwen3_source_model, "Qwen3 Source")
vae = self.qwen3_source_model.model_copy(update={"submodel_type": SubModelType.VAE})
else:
raise ValueError(
"No VAE source provided. Standalone safetensors/GGUF models require a separate VAE. "
"Options:\n"
" 1. Set 'VAE' to a standalone FLUX VAE model\n"
" 2. Set 'Qwen3 Source' to a Diffusers Flux2 Klein model to extract the VAE from"
)
# Determine Qwen3 Encoder source
if self.qwen3_encoder_model is not None:
# Use standalone Qwen3 Encoder - validate it matches the FLUX.2 Klein variant
self._validate_qwen3_encoder_variant(context, main_config)
qwen3_tokenizer = self.qwen3_encoder_model.model_copy(update={"submodel_type": SubModelType.Tokenizer})
qwen3_encoder = self.qwen3_encoder_model.model_copy(update={"submodel_type": SubModelType.TextEncoder})
elif main_is_diffusers:
# Extract from main model (recommended for FLUX.2 Klein)
qwen3_tokenizer = self.model.model_copy(update={"submodel_type": SubModelType.Tokenizer})
qwen3_encoder = self.model.model_copy(update={"submodel_type": SubModelType.TextEncoder})
elif self.qwen3_source_model is not None:
# Extract from separate Diffusers model
self._validate_diffusers_format(context, self.qwen3_source_model, "Qwen3 Source")
qwen3_tokenizer = self.qwen3_source_model.model_copy(update={"submodel_type": SubModelType.Tokenizer})
qwen3_encoder = self.qwen3_source_model.model_copy(update={"submodel_type": SubModelType.TextEncoder})
else:
raise ValueError(
"No Qwen3 Encoder source provided. Standalone safetensors/GGUF models require a separate text encoder. "
"Options:\n"
" 1. Set 'Qwen3 Encoder' to a standalone Qwen3 text encoder model "
"(Klein 4B needs Qwen3 4B, Klein 9B needs Qwen3 8B)\n"
" 2. Set 'Qwen3 Source' to a Diffusers Flux2 Klein model to extract the encoder from"
)
return Flux2KleinModelLoaderOutput(
transformer=TransformerField(transformer=transformer, loras=[]),
qwen3_encoder=Qwen3EncoderField(tokenizer=qwen3_tokenizer, text_encoder=qwen3_encoder),
vae=VAEField(vae=vae),
max_seq_len=self.max_seq_len,
)
def _validate_diffusers_format(
self, context: InvocationContext, model: ModelIdentifierField, model_name: str
) -> None:
"""Validate that a model is in Diffusers format."""
config = context.models.get_config(model)
if config.format != ModelFormat.Diffusers:
raise ValueError(
f"The {model_name} model must be a Diffusers format model. "
f"The selected model '{config.name}' is in {config.format.value} format."
)
def _validate_qwen3_encoder_variant(self, context: InvocationContext, main_config) -> None:
"""Validate that the standalone Qwen3 encoder variant matches the FLUX.2 Klein variant.
- FLUX.2 Klein 4B requires Qwen3 4B encoder
- FLUX.2 Klein 9B requires Qwen3 8B encoder
"""
if self.qwen3_encoder_model is None:
return
# Get the Qwen3 encoder config
qwen3_config = context.models.get_config(self.qwen3_encoder_model)
# Check if the config has a variant field
if not hasattr(qwen3_config, "variant"):
# Can't validate, skip
return
qwen3_variant = qwen3_config.variant
# Get the FLUX.2 Klein variant from the main model config
if not hasattr(main_config, "variant"):
return
flux2_variant = main_config.variant
# Validate the variants match
# Klein4B requires Qwen3_4B, Klein9B/Klein9BBase requires Qwen3_8B
expected_qwen3_variant = None
if flux2_variant == Flux2VariantType.Klein4B:
expected_qwen3_variant = Qwen3VariantType.Qwen3_4B
elif flux2_variant in (Flux2VariantType.Klein9B, Flux2VariantType.Klein9BBase):
expected_qwen3_variant = Qwen3VariantType.Qwen3_8B
if expected_qwen3_variant is not None and qwen3_variant != expected_qwen3_variant:
raise ValueError(
f"Qwen3 encoder variant mismatch: FLUX.2 Klein {flux2_variant.value} requires "
f"{expected_qwen3_variant.value} encoder, but {qwen3_variant.value} was selected. "
f"Please select a matching Qwen3 encoder or use a Diffusers format model which includes the correct encoder."
)

View File

@@ -0,0 +1,200 @@
"""Flux2 Klein Text Encoder Invocation.
Flux2 Klein uses Qwen3 as the text encoder instead of CLIP+T5.
The key difference is that it extracts hidden states from layers (9, 18, 27)
and stacks them together for richer text representations.
This implementation matches the diffusers Flux2KleinPipeline exactly.
"""
from contextlib import ExitStack
from typing import Iterator, Literal, Optional, Tuple
import torch
from transformers import PreTrainedModel, PreTrainedTokenizerBase
from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
from invokeai.app.invocations.fields import (
FieldDescriptions,
FluxConditioningField,
Input,
InputField,
TensorField,
UIComponent,
)
from invokeai.app.invocations.model import Qwen3EncoderField
from invokeai.app.invocations.primitives import FluxConditioningOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.load.model_cache.utils import get_effective_device
from invokeai.backend.patches.layer_patcher import LayerPatcher
from invokeai.backend.patches.lora_conversions.flux_lora_constants import FLUX_LORA_T5_PREFIX
from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import ConditioningFieldData, FLUXConditioningInfo
from invokeai.backend.util.devices import TorchDevice
# FLUX.2 Klein extracts hidden states from these specific layers
# Matching diffusers Flux2KleinPipeline: (9, 18, 27)
# hidden_states[0] is embedding layer, so layer N is at index N
KLEIN_EXTRACTION_LAYERS = (9, 18, 27)
# Default max sequence length for Klein models
KLEIN_MAX_SEQ_LEN = 512
@invocation(
"flux2_klein_text_encoder",
title="Prompt - Flux2 Klein",
tags=["prompt", "conditioning", "flux", "klein", "qwen3"],
category="conditioning",
version="1.1.1",
classification=Classification.Prototype,
)
class Flux2KleinTextEncoderInvocation(BaseInvocation):
"""Encodes and preps a prompt for Flux2 Klein image generation.
Flux2 Klein uses Qwen3 as the text encoder, extracting hidden states from
layers (9, 18, 27) and stacking them for richer text representations.
This matches the diffusers Flux2KleinPipeline implementation exactly.
"""
prompt: str = InputField(description="Text prompt to encode.", ui_component=UIComponent.Textarea)
qwen3_encoder: Qwen3EncoderField = InputField(
title="Qwen3 Encoder",
description=FieldDescriptions.qwen3_encoder,
input=Input.Connection,
)
max_seq_len: Literal[256, 512] = InputField(
default=512,
description="Max sequence length for the Qwen3 encoder.",
)
mask: Optional[TensorField] = InputField(
default=None,
description="A mask defining the region that this conditioning prompt applies to.",
)
@torch.no_grad()
def invoke(self, context: InvocationContext) -> FluxConditioningOutput:
# Open the exitstack here to lock models for the duration of the node
with ExitStack() as exit_stack:
# Pass the locked stack down to the helper function
qwen3_embeds, pooled_embeds = self._encode_prompt(context, exit_stack)
conditioning_data = ConditioningFieldData(
conditionings=[FLUXConditioningInfo(clip_embeds=pooled_embeds, t5_embeds=qwen3_embeds)]
)
# The models are still locked while we save the data
conditioning_name = context.conditioning.save(conditioning_data)
return FluxConditioningOutput(
conditioning=FluxConditioningField(conditioning_name=conditioning_name, mask=self.mask)
)
def _encode_prompt(self, context: InvocationContext, exit_stack: ExitStack) -> Tuple[torch.Tensor, torch.Tensor]:
prompt = self.prompt
# Reordered loading to prevent the annoying cache drop issue
# This prevents it from being evicted while we look up the tokenizer
text_encoder_info = context.models.load(self.qwen3_encoder.text_encoder)
(cached_weights, text_encoder) = exit_stack.enter_context(text_encoder_info.model_on_device())
# Now it is safe to load and lock the tokenizer
tokenizer_info = context.models.load(self.qwen3_encoder.tokenizer)
(_, tokenizer) = exit_stack.enter_context(tokenizer_info.model_on_device())
repaired_tensors = text_encoder_info.repair_required_tensors_on_device()
device = get_effective_device(text_encoder)
if repaired_tensors > 0:
context.logger.warning(
f"Recovered {repaired_tensors} required Qwen3 tensor(s) onto {device} after a partial device mismatch."
)
# Apply LoRA models
lora_dtype = TorchDevice.choose_bfloat16_safe_dtype(device)
exit_stack.enter_context(
LayerPatcher.apply_smart_model_patches(
model=text_encoder,
patches=self._lora_iterator(context),
prefix=FLUX_LORA_T5_PREFIX,
dtype=lora_dtype,
cached_weights=cached_weights,
)
)
context.util.signal_progress("Running Qwen3 text encoder (Klein)")
if not isinstance(text_encoder, PreTrainedModel):
raise TypeError(
f"Expected PreTrainedModel for text encoder, got {type(text_encoder).__name__}. "
"The Qwen3 encoder model may be corrupted or incompatible."
)
if not isinstance(tokenizer, PreTrainedTokenizerBase):
raise TypeError(
f"Expected PreTrainedTokenizerBase for tokenizer, got {type(tokenizer).__name__}. "
"The Qwen3 tokenizer may be corrupted or incompatible."
)
messages = [{"role": "user", "content": prompt}]
text: str = tokenizer.apply_chat_template( # type: ignore[assignment]
messages,
tokenize=False,
add_generation_prompt=True,
enable_thinking=False,
)
inputs = tokenizer(
text,
return_tensors="pt",
padding="max_length",
truncation=True,
max_length=self.max_seq_len,
)
input_ids = inputs["input_ids"].to(device)
attention_mask = inputs["attention_mask"].to(device)
# Forward pass through the model
outputs = text_encoder(
input_ids=input_ids,
attention_mask=attention_mask,
output_hidden_states=True,
use_cache=False,
)
if not hasattr(outputs, "hidden_states") or outputs.hidden_states is None:
raise RuntimeError(
"Text encoder did not return hidden_states. "
"Ensure output_hidden_states=True is supported by this model."
)
num_hidden_layers = len(outputs.hidden_states)
hidden_states_list = []
for layer_idx in KLEIN_EXTRACTION_LAYERS:
if layer_idx >= num_hidden_layers:
layer_idx = num_hidden_layers - 1
hidden_states_list.append(outputs.hidden_states[layer_idx])
out = torch.stack(hidden_states_list, dim=1)
out = out.to(dtype=text_encoder.dtype, device=device)
batch_size, num_channels, seq_len, hidden_dim = out.shape
prompt_embeds = out.permute(0, 2, 1, 3).reshape(batch_size, seq_len, num_channels * hidden_dim)
last_hidden_state = outputs.hidden_states[-1]
expanded_mask = attention_mask.unsqueeze(-1).expand_as(last_hidden_state).float()
sum_embeds = (last_hidden_state * expanded_mask).sum(dim=1)
num_tokens = expanded_mask.sum(dim=1).clamp(min=1)
pooled_embeds = sum_embeds / num_tokens
return prompt_embeds, pooled_embeds
def _lora_iterator(self, context: InvocationContext) -> Iterator[Tuple[ModelPatchRaw, float]]:
"""Iterate over LoRA models to apply to the Qwen3 text encoder."""
for lora in self.qwen3_encoder.loras:
lora_info = context.models.load(lora.lora)
if not isinstance(lora_info.model, ModelPatchRaw):
raise TypeError(
f"Expected ModelPatchRaw for LoRA '{lora.lora.key}', got {type(lora_info.model).__name__}. "
"The LoRA model may be corrupted or incompatible."
)
yield (lora_info.model, lora.weight)
del lora_info

View File

@@ -0,0 +1,92 @@
"""Flux2 Klein VAE Decode Invocation.
Decodes latents to images using the FLUX.2 32-channel VAE (AutoencoderKLFlux2).
"""
import torch
from einops import rearrange
from PIL import Image
from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
from invokeai.app.invocations.fields import (
FieldDescriptions,
Input,
InputField,
LatentsField,
WithBoard,
WithMetadata,
)
from invokeai.app.invocations.model import VAEField
from invokeai.app.invocations.primitives import ImageOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.load.load_base import LoadedModel
from invokeai.backend.util.devices import TorchDevice
@invocation(
"flux2_vae_decode",
title="Latents to Image - FLUX2",
tags=["latents", "image", "vae", "l2i", "flux2", "klein"],
category="latents",
version="1.0.0",
classification=Classification.Prototype,
)
class Flux2VaeDecodeInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Generates an image from latents using FLUX.2 Klein's 32-channel VAE."""
latents: LatentsField = InputField(
description=FieldDescriptions.latents,
input=Input.Connection,
)
vae: VAEField = InputField(
description=FieldDescriptions.vae,
input=Input.Connection,
)
def _vae_decode(self, vae_info: LoadedModel, latents: torch.Tensor) -> Image.Image:
"""Decode latents to image using FLUX.2 VAE.
Input latents should already be in the correct space after BN denormalization
was applied in the denoiser. The VAE expects (B, 32, H, W) format.
"""
with vae_info.model_on_device() as (_, vae):
vae_dtype = next(iter(vae.parameters())).dtype
device = TorchDevice.choose_torch_device()
latents = latents.to(device=device, dtype=vae_dtype)
# Decode using diffusers API
decoded = vae.decode(latents, return_dict=False)[0]
# Convert from [-1, 1] to [0, 1] then to [0, 255] PIL image
img = (decoded / 2 + 0.5).clamp(0, 1)
img = rearrange(img[0], "c h w -> h w c")
img_np = (img * 255).byte().cpu().numpy()
# Explicitly create RGB image (not grayscale)
img_pil = Image.fromarray(img_np, mode="RGB")
return img_pil
@torch.no_grad()
def invoke(self, context: InvocationContext) -> ImageOutput:
latents = context.tensors.load(self.latents.latents_name)
# Log latent statistics for debugging black image issues
context.logger.debug(
f"FLUX.2 VAE decode input: shape={latents.shape}, "
f"min={latents.min().item():.4f}, max={latents.max().item():.4f}, "
f"mean={latents.mean().item():.4f}"
)
# Warn if input latents are all zeros or very small (would cause black images)
if latents.abs().max() < 1e-6:
context.logger.warning(
"FLUX.2 VAE decode received near-zero latents! This will cause black images. "
"The latent cache may be corrupted - try clearing the cache."
)
vae_info = context.models.load(self.vae.vae)
context.util.signal_progress("Running VAE")
image = self._vae_decode(vae_info=vae_info, latents=latents)
TorchDevice.empty_cache()
image_dto = context.images.save(image=image)
return ImageOutput.build(image_dto)

View File

@@ -0,0 +1,88 @@
"""Flux2 Klein VAE Encode Invocation.
Encodes images to latents using the FLUX.2 32-channel VAE (AutoencoderKLFlux2).
"""
import einops
import torch
from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
from invokeai.app.invocations.fields import (
FieldDescriptions,
ImageField,
Input,
InputField,
)
from invokeai.app.invocations.model import VAEField
from invokeai.app.invocations.primitives import LatentsOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.load.load_base import LoadedModel
from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
from invokeai.backend.util.devices import TorchDevice
@invocation(
"flux2_vae_encode",
title="Image to Latents - FLUX2",
tags=["latents", "image", "vae", "i2l", "flux2", "klein"],
category="latents",
version="1.0.0",
classification=Classification.Prototype,
)
class Flux2VaeEncodeInvocation(BaseInvocation):
"""Encodes an image into latents using FLUX.2 Klein's 32-channel VAE."""
image: ImageField = InputField(
description="The image to encode.",
)
vae: VAEField = InputField(
description=FieldDescriptions.vae,
input=Input.Connection,
)
def _vae_encode(self, vae_info: LoadedModel, image_tensor: torch.Tensor) -> torch.Tensor:
"""Encode image to latents using FLUX.2 VAE.
The VAE encodes to 32-channel latent space.
Output latents shape: (B, 32, H/8, W/8).
"""
with vae_info.model_on_device() as (_, vae):
vae_dtype = next(iter(vae.parameters())).dtype
device = TorchDevice.choose_torch_device()
image_tensor = image_tensor.to(device=device, dtype=vae_dtype)
# Encode using diffusers API
# The VAE.encode() returns a DiagonalGaussianDistribution-like object
latent_dist = vae.encode(image_tensor, return_dict=False)[0]
# Sample from the distribution (or use mode for deterministic output)
# Using mode() for deterministic encoding
if hasattr(latent_dist, "mode"):
latents = latent_dist.mode()
elif hasattr(latent_dist, "sample"):
# Fall back to sampling if mode is not available
generator = torch.Generator(device=device).manual_seed(0)
latents = latent_dist.sample(generator=generator)
else:
# Direct tensor output (some VAE implementations)
latents = latent_dist
return latents
@torch.no_grad()
def invoke(self, context: InvocationContext) -> LatentsOutput:
image = context.images.get_pil(self.image.image_name)
vae_info = context.models.load(self.vae.vae)
# Convert image to tensor (HWC -> CHW, normalize to [-1, 1])
image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB"))
if image_tensor.dim() == 3:
image_tensor = einops.rearrange(image_tensor, "c h w -> 1 c h w")
context.util.signal_progress("Running VAE Encode")
latents = self._vae_encode(vae_info=vae_info, image_tensor=image_tensor)
latents = latents.to("cpu")
name = context.tensors.save(tensor=latents)
return LatentsOutput.build(latents_name=name, latents=latents, seed=None)

View File

@@ -4,9 +4,10 @@ from invokeai.app.invocations.baseinvocation import (
invocation,
invocation_output,
)
from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, OutputField, UIType
from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, OutputField
from invokeai.app.invocations.model import ControlLoRAField, ModelIdentifierField
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
@invocation_output("flux_control_lora_loader_output")
@@ -29,7 +30,10 @@ class FluxControlLoRALoaderInvocation(BaseInvocation):
"""LoRA model and Image to use with FLUX transformer generation."""
lora: ModelIdentifierField = InputField(
description=FieldDescriptions.control_lora_model, title="Control LoRA", ui_type=UIType.ControlLoRAModel
description=FieldDescriptions.control_lora_model,
title="Control LoRA",
ui_model_base=BaseModelType.Flux,
ui_model_type=ModelType.ControlLoRa,
)
image: ImageField = InputField(description="The image to encode.")
weight: float = InputField(description="The weight of the LoRA.", default=1.0)

View File

@@ -6,11 +6,12 @@ from invokeai.app.invocations.baseinvocation import (
invocation,
invocation_output,
)
from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, OutputField, UIType
from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, OutputField
from invokeai.app.invocations.model import ModelIdentifierField
from invokeai.app.invocations.util import validate_begin_end_step, validate_weights
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.app.util.controlnet_utils import CONTROLNET_RESIZE_VALUES
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
class FluxControlNetField(BaseModel):
@@ -57,7 +58,9 @@ class FluxControlNetInvocation(BaseInvocation):
image: ImageField = InputField(description="The control image")
control_model: ModelIdentifierField = InputField(
description=FieldDescriptions.controlnet_model, ui_type=UIType.ControlNetModel
description=FieldDescriptions.controlnet_model,
ui_model_base=BaseModelType.Flux,
ui_model_type=ModelType.ControlNet,
)
control_weight: float | list[float] = InputField(
default=1.0, ge=-1, le=2, description="The weight given to the ControlNet"

View File

@@ -32,6 +32,13 @@ from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.flux.controlnet.instantx_controlnet_flux import InstantXControlNetFlux
from invokeai.backend.flux.controlnet.xlabs_controlnet_flux import XLabsControlNetFlux
from invokeai.backend.flux.denoise import denoise
from invokeai.backend.flux.dype.presets import (
DYPE_PRESET_LABELS,
DYPE_PRESET_OFF,
DyPEPreset,
get_dype_config_from_preset,
)
from invokeai.backend.flux.extensions.dype_extension import DyPEExtension
from invokeai.backend.flux.extensions.instantx_controlnet_extension import InstantXControlNetExtension
from invokeai.backend.flux.extensions.kontext_extension import KontextExtension
from invokeai.backend.flux.extensions.regional_prompting_extension import RegionalPromptingExtension
@@ -47,8 +54,9 @@ from invokeai.backend.flux.sampling_utils import (
pack,
unpack,
)
from invokeai.backend.flux.schedulers import FLUX_SCHEDULER_LABELS, FLUX_SCHEDULER_MAP, FLUX_SCHEDULER_NAME_VALUES
from invokeai.backend.flux.text_conditioning import FluxReduxConditioning, FluxTextConditioning
from invokeai.backend.model_manager.taxonomy import ModelFormat, ModelVariantType
from invokeai.backend.model_manager.taxonomy import BaseModelType, FluxVariantType, ModelFormat, ModelType
from invokeai.backend.patches.layer_patcher import LayerPatcher
from invokeai.backend.patches.lora_conversions.flux_lora_constants import FLUX_LORA_TRANSFORMER_PREFIX
from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
@@ -63,7 +71,7 @@ from invokeai.backend.util.devices import TorchDevice
title="FLUX Denoise",
tags=["image", "flux"],
category="image",
version="4.0.0",
version="4.5.1",
)
class FluxDenoiseInvocation(BaseInvocation):
"""Run denoising process with a FLUX transformer model."""
@@ -132,6 +140,12 @@ class FluxDenoiseInvocation(BaseInvocation):
num_steps: int = InputField(
default=4, description="Number of diffusion steps. Recommended values are schnell: 4, dev: 50."
)
scheduler: FLUX_SCHEDULER_NAME_VALUES = InputField(
default="euler",
description="Scheduler (sampler) for the denoising process. 'euler' is fast and standard. "
"'heun' is 2nd-order (better quality, 2x slower). 'lcm' is optimized for few steps.",
ui_choice_labels=FLUX_SCHEDULER_LABELS,
)
guidance: float = InputField(
default=4.0,
description="The guidance strength. Higher values adhere more strictly to the prompt, and will produce less diverse images. FLUX dev only, ignored for schnell.",
@@ -153,12 +167,37 @@ class FluxDenoiseInvocation(BaseInvocation):
description=FieldDescriptions.ip_adapter, title="IP-Adapter", default=None, input=Input.Connection
)
kontext_conditioning: Optional[FluxKontextConditioningField] = InputField(
kontext_conditioning: FluxKontextConditioningField | list[FluxKontextConditioningField] | None = InputField(
default=None,
description="FLUX Kontext conditioning (reference image).",
input=Input.Connection,
)
# DyPE (Dynamic Position Extrapolation) for high-resolution generation
dype_preset: DyPEPreset = InputField(
default=DYPE_PRESET_OFF,
description=(
"DyPE preset for high-resolution generation. 'auto' enables automatically for resolutions > 1536px. "
"'area' enables automatically based on image area. '4k' uses optimized settings for 4K output."
),
ui_order=100,
ui_choice_labels=DYPE_PRESET_LABELS,
)
dype_scale: Optional[float] = InputField(
default=None,
ge=0.0,
le=8.0,
description="DyPE magnitude (λs). Higher values = stronger extrapolation. Only used when dype_preset is not 'off'.",
ui_order=101,
)
dype_exponent: Optional[float] = InputField(
default=None,
ge=0.0,
le=1000.0,
description="DyPE decay speed (λt). Controls transition from low to high frequency detail. Only used when dype_preset is not 'off'.",
ui_order=102,
)
@torch.no_grad()
def invoke(self, context: InvocationContext) -> LatentsOutput:
latents = self._run_diffusion(context)
@@ -232,7 +271,14 @@ class FluxDenoiseInvocation(BaseInvocation):
)
transformer_config = context.models.get_config(self.transformer.transformer)
is_schnell = "schnell" in getattr(transformer_config, "config_path", "")
assert (
transformer_config.base in (BaseModelType.Flux, BaseModelType.Flux2)
and transformer_config.type is ModelType.Main
)
# Schnell is only for FLUX.1, FLUX.2 Klein behaves like Dev (with guidance)
is_schnell = (
transformer_config.base is BaseModelType.Flux and transformer_config.variant is FluxVariantType.Schnell
)
# Calculate the timestep schedule.
timesteps = get_schedule(
@@ -241,6 +287,12 @@ class FluxDenoiseInvocation(BaseInvocation):
shift=not is_schnell,
)
# Create scheduler if not using default euler
scheduler = None
if self.scheduler in FLUX_SCHEDULER_MAP:
scheduler_class = FLUX_SCHEDULER_MAP[self.scheduler]
scheduler = scheduler_class(num_train_timesteps=1000)
# Clip the timesteps schedule based on denoising_start and denoising_end.
timesteps = clip_timestep_schedule_fractional(timesteps, self.denoising_start, self.denoising_end)
@@ -277,7 +329,7 @@ class FluxDenoiseInvocation(BaseInvocation):
# Prepare the extra image conditioning tensor (img_cond) for either FLUX structural control or FLUX Fill.
img_cond: torch.Tensor | None = None
is_flux_fill = transformer_config.variant == ModelVariantType.Inpaint # type: ignore
is_flux_fill = transformer_config.variant is FluxVariantType.DevFill
if is_flux_fill:
img_cond = self._prep_flux_fill_img_cond(
context, device=TorchDevice.choose_torch_device(), dtype=inference_dtype
@@ -328,6 +380,21 @@ class FluxDenoiseInvocation(BaseInvocation):
cfg_scale_end_step=self.cfg_scale_end_step,
)
kontext_extension = None
if self.kontext_conditioning:
if not self.controlnet_vae:
raise ValueError("A VAE (e.g., controlnet_vae) must be provided to use Kontext conditioning.")
kontext_extension = KontextExtension(
context=context,
kontext_conditioning=self.kontext_conditioning
if isinstance(self.kontext_conditioning, list)
else [self.kontext_conditioning],
vae_field=self.controlnet_vae,
device=TorchDevice.choose_torch_device(),
dtype=inference_dtype,
)
with ExitStack() as exit_stack:
# Prepare ControlNet extensions.
# Note: We do this before loading the transformer model to minimize peak memory (see implementation).
@@ -385,19 +452,6 @@ class FluxDenoiseInvocation(BaseInvocation):
dtype=inference_dtype,
)
kontext_extension = None
if self.kontext_conditioning is not None:
if not self.controlnet_vae:
raise ValueError("A VAE (e.g., controlnet_vae) must be provided to use Kontext conditioning.")
kontext_extension = KontextExtension(
context=context,
kontext_conditioning=self.kontext_conditioning,
vae_field=self.controlnet_vae,
device=TorchDevice.choose_torch_device(),
dtype=inference_dtype,
)
# Prepare Kontext conditioning if provided
img_cond_seq = None
img_cond_seq_ids = None
@@ -406,6 +460,30 @@ class FluxDenoiseInvocation(BaseInvocation):
kontext_extension.ensure_batch_size(x.shape[0])
img_cond_seq, img_cond_seq_ids = kontext_extension.kontext_latents, kontext_extension.kontext_ids
# Prepare DyPE extension for high-resolution generation
dype_extension: DyPEExtension | None = None
dype_config = get_dype_config_from_preset(
preset=self.dype_preset,
width=self.width,
height=self.height,
custom_scale=self.dype_scale,
custom_exponent=self.dype_exponent,
)
if dype_config is not None:
dype_extension = DyPEExtension(
config=dype_config,
target_height=self.height,
target_width=self.width,
)
context.logger.info(
f"DyPE enabled: resolution={self.width}x{self.height}, preset={self.dype_preset}, "
f"method={dype_config.method}, scale={dype_config.dype_scale:.2f}, "
f"exponent={dype_config.dype_exponent:.2f}, start_sigma={dype_config.dype_start_sigma:.2f}, "
f"base_resolution={dype_config.base_resolution}"
)
else:
context.logger.debug(f"DyPE disabled: resolution={self.width}x{self.height}, preset={self.dype_preset}")
x = denoise(
model=transformer,
img=x,
@@ -423,6 +501,8 @@ class FluxDenoiseInvocation(BaseInvocation):
img_cond=img_cond,
img_cond_seq=img_cond_seq,
img_cond_seq_ids=img_cond_seq_ids,
dype_extension=dype_extension,
scheduler=scheduler,
)
x = unpack(x.float(), self.height, self.width)

View File

@@ -5,7 +5,7 @@ from pydantic import field_validator, model_validator
from typing_extensions import Self
from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
from invokeai.app.invocations.fields import InputField, UIType
from invokeai.app.invocations.fields import InputField
from invokeai.app.invocations.ip_adapter import (
CLIP_VISION_MODEL_MAP,
IPAdapterField,
@@ -16,10 +16,8 @@ from invokeai.app.invocations.model import ModelIdentifierField
from invokeai.app.invocations.primitives import ImageField
from invokeai.app.invocations.util import validate_begin_end_step, validate_weights
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.config import (
IPAdapterCheckpointConfig,
IPAdapterInvokeAIConfig,
)
from invokeai.backend.model_manager.configs.ip_adapter import IPAdapter_Checkpoint_FLUX_Config
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
@invocation(
@@ -36,7 +34,10 @@ class FluxIPAdapterInvocation(BaseInvocation):
image: ImageField = InputField(description="The IP-Adapter image prompt(s).")
ip_adapter_model: ModelIdentifierField = InputField(
description="The IP-Adapter model.", title="IP-Adapter Model", ui_type=UIType.IPAdapterModel
description="The IP-Adapter model.",
title="IP-Adapter Model",
ui_model_base=BaseModelType.Flux,
ui_model_type=ModelType.IPAdapter,
)
# Currently, the only known ViT model used by FLUX IP-Adapters is ViT-L.
clip_vision_model: Literal["ViT-L"] = InputField(description="CLIP Vision model to use.", default="ViT-L")
@@ -64,7 +65,7 @@ class FluxIPAdapterInvocation(BaseInvocation):
def invoke(self, context: InvocationContext) -> IPAdapterOutput:
# Lookup the CLIP Vision encoder that is intended to be used with the IP-Adapter model.
ip_adapter_info = context.models.get_config(self.ip_adapter_model.key)
assert isinstance(ip_adapter_info, (IPAdapterInvokeAIConfig, IPAdapterCheckpointConfig))
assert isinstance(ip_adapter_info, IPAdapter_Checkpoint_FLUX_Config)
# Note: There is a IPAdapterInvokeAIConfig.image_encoder_model_id field, but it isn't trustworthy.
image_encoder_starter_model = CLIP_VISION_MODEL_MAP[self.clip_vision_model]

View File

@@ -6,10 +6,10 @@ from invokeai.app.invocations.baseinvocation import (
invocation,
invocation_output,
)
from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, UIType
from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField
from invokeai.app.invocations.model import CLIPField, LoRAField, ModelIdentifierField, T5EncoderField, TransformerField
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.taxonomy import BaseModelType
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
@invocation_output("flux_lora_loader_output")
@@ -36,7 +36,10 @@ class FluxLoRALoaderInvocation(BaseInvocation):
"""Apply a LoRA model to a FLUX transformer and/or text encoder."""
lora: ModelIdentifierField = InputField(
description=FieldDescriptions.lora_model, title="LoRA", ui_type=UIType.LoRAModel
description=FieldDescriptions.lora_model,
title="LoRA",
ui_model_base=BaseModelType.Flux,
ui_model_type=ModelType.LoRA,
)
weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
transformer: TransformerField | None = InputField(
@@ -159,7 +162,7 @@ class FLUXLoRACollectionLoader(BaseInvocation):
if not context.models.exists(lora.lora.key):
raise Exception(f"Unknown lora: {lora.lora.key}!")
assert lora.lora.base is BaseModelType.Flux
assert lora.lora.base in (BaseModelType.Flux, BaseModelType.Flux2)
added_loras.append(lora.lora.key)

View File

@@ -6,18 +6,16 @@ from invokeai.app.invocations.baseinvocation import (
invocation,
invocation_output,
)
from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, UIType
from invokeai.app.invocations.fields import FieldDescriptions, InputField, OutputField
from invokeai.app.invocations.model import CLIPField, ModelIdentifierField, T5EncoderField, TransformerField, VAEField
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.app.util.t5_model_identifier import (
preprocess_t5_encoder_model_identifier,
preprocess_t5_tokenizer_model_identifier,
)
from invokeai.backend.flux.util import max_seq_lengths
from invokeai.backend.model_manager.config import (
CheckpointConfigBase,
)
from invokeai.backend.model_manager.taxonomy import SubModelType
from invokeai.backend.flux.util import get_flux_max_seq_length
from invokeai.backend.model_manager.configs.base import Checkpoint_Config_Base
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType, SubModelType
@invocation_output("flux_model_loader_output")
@@ -39,30 +37,34 @@ class FluxModelLoaderOutput(BaseInvocationOutput):
title="Main Model - FLUX",
tags=["model", "flux"],
category="model",
version="1.0.6",
version="1.0.7",
)
class FluxModelLoaderInvocation(BaseInvocation):
"""Loads a flux base model, outputting its submodels."""
model: ModelIdentifierField = InputField(
description=FieldDescriptions.flux_model,
ui_type=UIType.FluxMainModel,
input=Input.Direct,
ui_model_base=BaseModelType.Flux,
ui_model_type=ModelType.Main,
)
t5_encoder_model: ModelIdentifierField = InputField(
description=FieldDescriptions.t5_encoder, ui_type=UIType.T5EncoderModel, input=Input.Direct, title="T5 Encoder"
description=FieldDescriptions.t5_encoder,
title="T5 Encoder",
ui_model_type=ModelType.T5Encoder,
)
clip_embed_model: ModelIdentifierField = InputField(
description=FieldDescriptions.clip_embed_model,
ui_type=UIType.CLIPEmbedModel,
input=Input.Direct,
title="CLIP Embed",
ui_model_type=ModelType.CLIPEmbed,
)
vae_model: ModelIdentifierField = InputField(
description=FieldDescriptions.vae_model, ui_type=UIType.FluxVAEModel, title="VAE"
description=FieldDescriptions.vae_model,
title="VAE",
ui_model_base=BaseModelType.Flux,
ui_model_type=ModelType.VAE,
)
def invoke(self, context: InvocationContext) -> FluxModelLoaderOutput:
@@ -80,12 +82,12 @@ class FluxModelLoaderInvocation(BaseInvocation):
t5_encoder = preprocess_t5_encoder_model_identifier(self.t5_encoder_model)
transformer_config = context.models.get_config(transformer)
assert isinstance(transformer_config, CheckpointConfigBase)
assert isinstance(transformer_config, Checkpoint_Config_Base)
return FluxModelLoaderOutput(
transformer=TransformerField(transformer=transformer, loras=[]),
clip=CLIPField(tokenizer=tokenizer, text_encoder=clip_encoder, loras=[], skipped_layers=0),
t5_encoder=T5EncoderField(tokenizer=tokenizer2, text_encoder=t5_encoder, loras=[]),
vae=VAEField(vae=vae),
max_seq_len=max_seq_lengths[transformer_config.config_path],
max_seq_len=get_flux_max_seq_length(transformer_config.variant),
)

View File

@@ -18,16 +18,15 @@ from invokeai.app.invocations.fields import (
InputField,
OutputField,
TensorField,
UIType,
)
from invokeai.app.invocations.model import ModelIdentifierField
from invokeai.app.invocations.primitives import ImageField
from invokeai.app.services.model_records.model_records_base import ModelRecordChanges
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.flux.redux.flux_redux_model import FluxReduxModel
from invokeai.backend.model_manager import BaseModelType, ModelType
from invokeai.backend.model_manager.config import AnyModelConfig
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
from invokeai.backend.model_manager.starter_models import siglip
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType
from invokeai.backend.sig_lip.sig_lip_pipeline import SigLipPipeline
from invokeai.backend.util.devices import TorchDevice
@@ -64,7 +63,8 @@ class FluxReduxInvocation(BaseInvocation):
redux_model: ModelIdentifierField = InputField(
description="The FLUX Redux model to use.",
title="FLUX Redux Model",
ui_type=UIType.FluxReduxModel,
ui_model_base=BaseModelType.Flux,
ui_model_type=ModelType.FluxRedux,
)
downsampling_factor: int = InputField(
ge=1,

View File

@@ -17,7 +17,7 @@ from invokeai.app.invocations.model import CLIPField, T5EncoderField
from invokeai.app.invocations.primitives import FluxConditioningOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.flux.modules.conditioner import HFEncoder
from invokeai.backend.model_manager import ModelFormat
from invokeai.backend.model_manager.taxonomy import ModelFormat
from invokeai.backend.patches.layer_patcher import LayerPatcher
from invokeai.backend.patches.lora_conversions.flux_lora_constants import FLUX_LORA_CLIP_PREFIX, FLUX_LORA_T5_PREFIX
from invokeai.backend.patches.model_patch_raw import ModelPatchRaw
@@ -58,6 +58,12 @@ class FluxTextEncoderInvocation(BaseInvocation):
# scoped. This ensures that the T5 model can be freed and gc'd before loading the CLIP model (if necessary).
t5_embeddings = self._t5_encode(context)
clip_embeddings = self._clip_encode(context)
# Move embeddings to CPU for storage to save VRAM
# They will be moved to the appropriate device when used by the denoiser
t5_embeddings = t5_embeddings.detach().to("cpu")
clip_embeddings = clip_embeddings.detach().to("cpu")
conditioning_data = ConditioningFieldData(
conditionings=[FLUXConditioningInfo(clip_embeds=clip_embeddings, t5_embeds=t5_embeddings)]
)

View File

@@ -3,7 +3,6 @@ from einops import rearrange
from PIL import Image
from invokeai.app.invocations.baseinvocation import BaseInvocation, invocation
from invokeai.app.invocations.constants import LATENT_SCALE_FACTOR
from invokeai.app.invocations.fields import (
FieldDescriptions,
Input,
@@ -18,6 +17,7 @@ from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.flux.modules.autoencoder import AutoEncoder
from invokeai.backend.model_manager.load.load_base import LoadedModel
from invokeai.backend.util.devices import TorchDevice
from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_flux
@invocation(
@@ -39,17 +39,11 @@ class FluxVaeDecodeInvocation(BaseInvocation, WithMetadata, WithBoard):
input=Input.Connection,
)
def _estimate_working_memory(self, latents: torch.Tensor, vae: AutoEncoder) -> int:
"""Estimate the working memory required by the invocation in bytes."""
out_h = LATENT_SCALE_FACTOR * latents.shape[-2]
out_w = LATENT_SCALE_FACTOR * latents.shape[-1]
element_size = next(vae.parameters()).element_size()
scaling_constant = 2200 # Determined experimentally.
working_memory = out_h * out_w * element_size * scaling_constant
return int(working_memory)
def _vae_decode(self, vae_info: LoadedModel, latents: torch.Tensor) -> Image.Image:
estimated_working_memory = self._estimate_working_memory(latents, vae_info.model)
assert isinstance(vae_info.model, AutoEncoder)
estimated_working_memory = estimate_vae_working_memory_flux(
operation="decode", image_tensor=latents, vae=vae_info.model
)
with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
assert isinstance(vae, AutoEncoder)
vae_dtype = next(iter(vae.parameters())).dtype

View File

@@ -12,9 +12,10 @@ from invokeai.app.invocations.model import VAEField
from invokeai.app.invocations.primitives import LatentsOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.flux.modules.autoencoder import AutoEncoder
from invokeai.backend.model_manager import LoadedModel
from invokeai.backend.model_manager.load.load_base import LoadedModel
from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
from invokeai.backend.util.devices import TorchDevice
from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_flux
@invocation(
@@ -41,8 +42,12 @@ class FluxVaeEncodeInvocation(BaseInvocation):
# TODO(ryand): Write a util function for generating random tensors that is consistent across devices / dtypes.
# There's a starting point in get_noise(...), but it needs to be extracted and generalized. This function
# should be used for VAE encode sampling.
assert isinstance(vae_info.model, AutoEncoder)
estimated_working_memory = estimate_vae_working_memory_flux(
operation="encode", image_tensor=image_tensor, vae=vae_info.model
)
generator = torch.Generator(device=TorchDevice.choose_torch_device()).manual_seed(0)
with vae_info as vae:
with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
assert isinstance(vae, AutoEncoder)
vae_dtype = next(iter(vae.parameters())).dtype
image_tensor = image_tensor.to(device=TorchDevice.choose_torch_device(), dtype=vae_dtype)

View File

@@ -46,7 +46,12 @@ class IdealSizeInvocation(BaseInvocation):
dimension = 512
elif unet_config.base == BaseModelType.StableDiffusion2:
dimension = 768
elif unet_config.base in (BaseModelType.StableDiffusionXL, BaseModelType.Flux, BaseModelType.StableDiffusion3):
elif unet_config.base in (
BaseModelType.StableDiffusionXL,
BaseModelType.Flux,
BaseModelType.Flux2,
BaseModelType.StableDiffusion3,
):
dimension = 1024
else:
raise ValueError(f"Unsupported model type: {unet_config.base}")

View File

@@ -649,102 +649,104 @@ class MaskCombineInvocation(BaseInvocation, WithMetadata, WithBoard):
title="Color Correct",
tags=["image", "color"],
category="image",
version="1.2.2",
version="2.0.0",
)
class ColorCorrectInvocation(BaseInvocation, WithMetadata, WithBoard):
"""
Shifts the colors of a target image to match the reference image, optionally
using a mask to only color-correct certain regions of the target image.
Matches the color histogram of a base image to a reference image, optionally
using a mask to only color-correct certain regions of the base image.
"""
image: ImageField = InputField(description="The image to color-correct")
reference: ImageField = InputField(description="Reference image for color-correction")
mask: Optional[ImageField] = InputField(default=None, description="Mask to use when applying color-correction")
mask_blur_radius: float = InputField(default=8, description="Mask blur radius")
base_image: ImageField = InputField(description="The image to color-correct")
color_reference: ImageField = InputField(description="Reference image for color-correction")
mask: Optional[ImageField] = InputField(default=None, description="Optional mask to limit color correction area")
colorspace: Literal["RGB", "YCbCr", "YCbCr-Chroma", "YCbCr-Luma"] = InputField(
default="RGB", description="Colorspace in which to apply histogram matching", title="Color Space"
)
def _match_histogram_channel(self, source: numpy.ndarray, reference: numpy.ndarray) -> numpy.ndarray:
"""Match histogram of source channel to reference channel using cumulative distribution functions."""
# Compute histograms
source_hist, _ = numpy.histogram(source.flatten(), bins=256, range=(0, 256))
reference_hist, _ = numpy.histogram(reference.flatten(), bins=256, range=(0, 256))
# Compute cumulative distribution functions
source_cdf = source_hist.cumsum()
reference_cdf = reference_hist.cumsum()
# Normalize CDFs (avoid division by zero)
if source_cdf[-1] > 0:
source_cdf = source_cdf / source_cdf[-1]
if reference_cdf[-1] > 0:
reference_cdf = reference_cdf / reference_cdf[-1]
# Create lookup table using linear interpolation
lookup_table = numpy.interp(source_cdf, reference_cdf, numpy.arange(256))
# Apply lookup table to source image
return lookup_table[source].astype(numpy.uint8)
def invoke(self, context: InvocationContext) -> ImageOutput:
pil_init_mask = None
# Load images as RGBA
base_image = context.images.get_pil(self.base_image.image_name, "RGBA")
# Store original alpha channel
original_alpha = base_image.getchannel("A")
# Convert to working colorspace
if self.colorspace == "RGB":
base_array = numpy.asarray(base_image.convert("RGB"), dtype=numpy.uint8)
ref_rgb = context.images.get_pil(self.color_reference.image_name, "RGB")
ref_array = numpy.asarray(ref_rgb, dtype=numpy.uint8)
channels_to_match = [0, 1, 2] # R, G, B
else:
# Convert to YCbCr colorspace
base_ycbcr = base_image.convert("YCbCr")
ref_ycbcr = context.images.get_pil(self.color_reference.image_name, "YCbCr")
base_array = numpy.asarray(base_ycbcr, dtype=numpy.uint8)
ref_array = numpy.asarray(ref_ycbcr, dtype=numpy.uint8)
# Determine which channels to match based on mode
if self.colorspace == "YCbCr":
channels_to_match = [0, 1, 2] # Y, Cb, Cr
elif self.colorspace == "YCbCr-Chroma":
channels_to_match = [1, 2] # Cb, Cr only
else: # YCbCr-Luma
channels_to_match = [0] # Y only
# Apply histogram matching to selected channels
corrected_array = base_array.copy()
for channel_idx in channels_to_match:
corrected_array[:, :, channel_idx] = self._match_histogram_channel(
base_array[:, :, channel_idx], ref_array[:, :, channel_idx]
)
# Convert back to RGB if we were in YCbCr
if self.colorspace != "RGB":
corrected_image = Image.fromarray(corrected_array, mode="YCbCr").convert("RGB")
else:
corrected_image = Image.fromarray(corrected_array, mode="RGB")
# Apply mask if provided (white = original, black = result)
if self.mask is not None:
pil_init_mask = context.images.get_pil(self.mask.image_name).convert("L")
init_image = context.images.get_pil(self.reference.image_name)
result = context.images.get_pil(self.image.image_name).convert("RGBA")
# if init_image is None or init_mask is None:
# return result
# Get the original alpha channel of the mask if there is one.
# Otherwise it is some other black/white image format ('1', 'L' or 'RGB')
# pil_init_mask = (
# init_mask.getchannel("A")
# if init_mask.mode == "RGBA"
# else init_mask.convert("L")
# )
pil_init_image = init_image.convert("RGBA") # Add an alpha channel if one doesn't exist
# Build an image with only visible pixels from source to use as reference for color-matching.
init_rgb_pixels = numpy.asarray(init_image.convert("RGB"), dtype=numpy.uint8)
init_a_pixels = numpy.asarray(pil_init_image.getchannel("A"), dtype=numpy.uint8)
init_mask_pixels = numpy.asarray(pil_init_mask, dtype=numpy.uint8)
# Get numpy version of result
np_image = numpy.asarray(result.convert("RGB"), dtype=numpy.uint8)
# Mask and calculate mean and standard deviation
mask_pixels = init_a_pixels * init_mask_pixels > 0
np_init_rgb_pixels_masked = init_rgb_pixels[mask_pixels, :]
np_image_masked = np_image[mask_pixels, :]
if np_init_rgb_pixels_masked.size > 0:
init_means = np_init_rgb_pixels_masked.mean(axis=0)
init_std = np_init_rgb_pixels_masked.std(axis=0)
gen_means = np_image_masked.mean(axis=0)
gen_std = np_image_masked.std(axis=0)
# Color correct
np_matched_result = np_image.copy()
np_matched_result[:, :, :] = (
(
(
(np_matched_result[:, :, :].astype(numpy.float32) - gen_means[None, None, :])
/ gen_std[None, None, :]
)
* init_std[None, None, :]
+ init_means[None, None, :]
)
.clip(0, 255)
.astype(numpy.uint8)
)
matched_result = Image.fromarray(np_matched_result, mode="RGB")
# Load mask as grayscale
mask_image = context.images.get_pil(self.mask.image_name, "L")
# Start with corrected image, paste base image where mask is white
result = corrected_image.copy()
if mask_image.size != result.size:
raise ValueError("Mask size must match base image size.")
else:
result.paste(base_image.convert("RGB"), mask=mask_image)
else:
matched_result = Image.fromarray(np_image, mode="RGB")
result = corrected_image
# Blur the mask out (into init image) by specified amount
if self.mask_blur_radius > 0:
nm = numpy.asarray(pil_init_mask, dtype=numpy.uint8)
inverted_nm = 255 - nm
dilation_size = int(round(self.mask_blur_radius) + 20)
dilating_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (dilation_size, dilation_size))
inverted_dilated_nm = cv2.dilate(inverted_nm, dilating_kernel)
dilated_nm = 255 - inverted_dilated_nm
nmd = cv2.erode(
dilated_nm,
kernel=numpy.ones((3, 3), dtype=numpy.uint8),
iterations=int(self.mask_blur_radius / 2),
)
pmd = Image.fromarray(nmd, mode="L")
blurred_init_mask = pmd.filter(ImageFilter.BoxBlur(self.mask_blur_radius))
else:
blurred_init_mask = pil_init_mask
multiplied_blurred_init_mask = ImageChops.multiply(blurred_init_mask, result.split()[-1])
# Paste original on color-corrected generation (using blurred mask)
matched_result.paste(init_image, (0, 0), mask=multiplied_blurred_init_mask)
image_dto = context.images.save(image=matched_result)
# Convert to RGBA and restore original alpha
result = result.convert("RGBA")
result.putalpha(original_alpha)
# Save and return
image_dto = context.images.save(image=result)
return ImageOutput.build(image_dto)
@@ -1347,3 +1349,96 @@ class PasteImageIntoBoundingBoxInvocation(BaseInvocation, WithMetadata, WithBoar
image_dto = context.images.save(image=target_image)
return ImageOutput.build(image_dto)
@invocation(
"flux_kontext_image_prep",
title="FLUX Kontext Image Prep",
tags=["image", "concatenate", "flux", "kontext"],
category="image",
version="1.0.0",
)
class FluxKontextConcatenateImagesInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Prepares an image or images for use with FLUX Kontext. The first/single image is resized to the nearest
preferred Kontext resolution. All other images are concatenated horizontally, maintaining their aspect ratio."""
images: list[ImageField] = InputField(
description="The images to concatenate",
min_length=1,
max_length=10,
)
use_preferred_resolution: bool = InputField(
default=True, description="Use FLUX preferred resolutions for the first image"
)
def invoke(self, context: InvocationContext) -> ImageOutput:
from invokeai.backend.flux.util import PREFERED_KONTEXT_RESOLUTIONS
# Step 1: Load all images
pil_images = []
for image_field in self.images:
image = context.images.get_pil(image_field.image_name, mode="RGBA")
pil_images.append(image)
# Step 2: Determine target resolution for the first image
first_image = pil_images[0]
width, height = first_image.size
if self.use_preferred_resolution:
aspect_ratio = width / height
# Find the closest preferred resolution for the first image
_, target_width, target_height = min(
((abs(aspect_ratio - w / h), w, h) for w, h in PREFERED_KONTEXT_RESOLUTIONS), key=lambda x: x[0]
)
# Apply BFL's scaling formula
scaled_height = 2 * int(target_height / 16)
final_height = 8 * scaled_height # This will be consistent for all images
scaled_width = 2 * int(target_width / 16)
first_width = 8 * scaled_width
else:
# Use original dimensions of first image, ensuring divisibility by 16
final_height = 16 * (height // 16)
first_width = 16 * (width // 16)
# Ensure minimum dimensions
if final_height < 16:
final_height = 16
if first_width < 16:
first_width = 16
# Step 3: Process and resize all images with consistent height
processed_images = []
total_width = 0
for i, image in enumerate(pil_images):
if i == 0:
# First image uses the calculated dimensions
final_width = first_width
else:
# Subsequent images maintain aspect ratio with the same height
img_aspect_ratio = image.width / image.height
# Calculate width that maintains aspect ratio at the target height
calculated_width = int(final_height * img_aspect_ratio)
# Ensure width is divisible by 16 for proper VAE encoding
final_width = 16 * (calculated_width // 16)
# Ensure minimum width
if final_width < 16:
final_width = 16
# Resize image to calculated dimensions
resized_image = image.resize((final_width, final_height), Image.Resampling.LANCZOS)
processed_images.append(resized_image)
total_width += final_width
# Step 4: Concatenate images horizontally
concatenated_image = Image.new("RGB", (total_width, final_height))
x_offset = 0
for img in processed_images:
concatenated_image.paste(img, (x_offset, 0))
x_offset += img.width
# Save the concatenated image
image_dto = context.images.save(image=concatenated_image)
return ImageOutput.build(image_dto)

View File

@@ -1,5 +1,6 @@
from contextlib import nullcontext
from functools import singledispatchmethod
from typing import Literal
import einops
import torch
@@ -20,13 +21,22 @@ from invokeai.app.invocations.fields import (
Input,
InputField,
)
from invokeai.app.invocations.model import VAEField
from invokeai.app.invocations.model import BaseModelType, VAEField
from invokeai.app.invocations.primitives import LatentsOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager import LoadedModel
from invokeai.backend.model_manager.load.load_base import LoadedModel
from invokeai.backend.stable_diffusion.diffusers_pipeline import image_resized_to_grid_as_tensor
from invokeai.backend.stable_diffusion.vae_tiling import patch_vae_tiling_params
from invokeai.backend.util.devices import TorchDevice
from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_sd15_sdxl
"""
SDXL VAE color compensation values determined experimentally to reduce color drift.
If more reliable values are found in the future (e.g. individual color channels), they can be updated.
SD1.5, TAESD, TAESDXL VAEs distort in less predictable ways, so no compensation is offered at this time.
"""
COMPENSATION_OPTIONS = Literal["None", "SDXL"]
COLOR_COMPENSATION_MAP = {"None": [1, 0], "SDXL": [1.015, -0.002]}
@invocation(
@@ -34,7 +44,7 @@ from invokeai.backend.util.devices import TorchDevice
title="Image to Latents - SD1.5, SDXL",
tags=["latents", "image", "vae", "i2l"],
category="latents",
version="1.1.1",
version="1.2.0",
)
class ImageToLatentsInvocation(BaseInvocation):
"""Encodes an image into latents."""
@@ -51,13 +61,30 @@ class ImageToLatentsInvocation(BaseInvocation):
# offer a way to directly set None values.
tile_size: int = InputField(default=0, multiple_of=8, description=FieldDescriptions.vae_tile_size)
fp32: bool = InputField(default=False, description=FieldDescriptions.fp32)
color_compensation: COMPENSATION_OPTIONS = InputField(
default="None",
description="Apply VAE scaling compensation when encoding images (reduces color drift).",
)
@staticmethod
@classmethod
def vae_encode(
vae_info: LoadedModel, upcast: bool, tiled: bool, image_tensor: torch.Tensor, tile_size: int = 0
cls,
vae_info: LoadedModel,
upcast: bool,
tiled: bool,
image_tensor: torch.Tensor,
tile_size: int = 0,
) -> torch.Tensor:
with vae_info as vae:
assert isinstance(vae, (AutoencoderKL, AutoencoderTiny))
assert isinstance(vae_info.model, (AutoencoderKL, AutoencoderTiny)), "VAE must be of type SD-1.5 or SDXL"
estimated_working_memory = estimate_vae_working_memory_sd15_sdxl(
operation="encode",
image_tensor=image_tensor,
vae=vae_info.model,
tile_size=tile_size if tiled else None,
fp32=upcast,
)
with vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae):
assert isinstance(vae, (AutoencoderKL, AutoencoderTiny)), "VAE must be of type SD-1.5 or SDXL"
orig_dtype = vae.dtype
if upcast:
vae.to(dtype=torch.float32)
@@ -113,14 +140,24 @@ class ImageToLatentsInvocation(BaseInvocation):
image = context.images.get_pil(self.image.image_name)
vae_info = context.models.load(self.vae.vae)
assert isinstance(vae_info.model, (AutoencoderKL, AutoencoderTiny)), "VAE must be of type SD-1.5 or SDXL"
image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB"))
if self.color_compensation != "None" and vae_info.config.base == BaseModelType.StableDiffusionXL:
scale, bias = COLOR_COMPENSATION_MAP[self.color_compensation]
image_tensor = image_tensor * scale + bias
if image_tensor.dim() == 3:
image_tensor = einops.rearrange(image_tensor, "c h w -> 1 c h w")
context.util.signal_progress("Running VAE encoder")
latents = self.vae_encode(
vae_info=vae_info, upcast=self.fp32, tiled=self.tiled, image_tensor=image_tensor, tile_size=self.tile_size
vae_info=vae_info,
upcast=self.fp32,
tiled=self.tiled or context.config.get().force_tiled_decode,
image_tensor=image_tensor,
tile_size=self.tile_size,
)
latents = latents.to("cpu")

View File

@@ -5,16 +5,16 @@ from pydantic import BaseModel, Field, field_validator, model_validator
from typing_extensions import Self
from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output
from invokeai.app.invocations.fields import FieldDescriptions, InputField, OutputField, TensorField, UIType
from invokeai.app.invocations.fields import FieldDescriptions, InputField, OutputField, TensorField
from invokeai.app.invocations.model import ModelIdentifierField
from invokeai.app.invocations.primitives import ImageField
from invokeai.app.invocations.util import validate_begin_end_step, validate_weights
from invokeai.app.services.model_records.model_records_base import ModelRecordChanges
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.config import (
AnyModelConfig,
IPAdapterCheckpointConfig,
IPAdapterInvokeAIConfig,
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
from invokeai.backend.model_manager.configs.ip_adapter import (
IPAdapter_Checkpoint_Config_Base,
IPAdapter_InvokeAI_Config_Base,
)
from invokeai.backend.model_manager.starter_models import (
StarterModel,
@@ -85,7 +85,8 @@ class IPAdapterInvocation(BaseInvocation):
description="The IP-Adapter model.",
title="IP-Adapter Model",
ui_order=-1,
ui_type=UIType.IPAdapterModel,
ui_model_base=[BaseModelType.StableDiffusion1, BaseModelType.StableDiffusionXL],
ui_model_type=ModelType.IPAdapter,
)
clip_vision_model: Literal["ViT-H", "ViT-G", "ViT-L"] = InputField(
description="CLIP Vision model to use. Overrides model settings. Mandatory for checkpoint models.",
@@ -122,9 +123,9 @@ class IPAdapterInvocation(BaseInvocation):
def invoke(self, context: InvocationContext) -> IPAdapterOutput:
# Lookup the CLIP Vision encoder that is intended to be used with the IP-Adapter model.
ip_adapter_info = context.models.get_config(self.ip_adapter_model.key)
assert isinstance(ip_adapter_info, (IPAdapterInvokeAIConfig, IPAdapterCheckpointConfig))
assert isinstance(ip_adapter_info, (IPAdapter_InvokeAI_Config_Base, IPAdapter_Checkpoint_Config_Base))
if isinstance(ip_adapter_info, IPAdapterInvokeAIConfig):
if isinstance(ip_adapter_info, IPAdapter_InvokeAI_Config_Base):
image_encoder_model_id = ip_adapter_info.image_encoder_model_id
image_encoder_model_name = image_encoder_model_id.split("/")[-1].strip()
else:

View File

@@ -2,12 +2,6 @@ from contextlib import nullcontext
import torch
from diffusers.image_processor import VaeImageProcessor
from diffusers.models.attention_processor import (
AttnProcessor2_0,
LoRAAttnProcessor2_0,
LoRAXFormersAttnProcessor,
XFormersAttnProcessor,
)
from diffusers.models.autoencoders.autoencoder_kl import AutoencoderKL
from diffusers.models.autoencoders.autoencoder_tiny import AutoencoderTiny
@@ -27,6 +21,7 @@ from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.stable_diffusion.extensions.seamless import SeamlessExt
from invokeai.backend.stable_diffusion.vae_tiling import patch_vae_tiling_params
from invokeai.backend.util.devices import TorchDevice
from invokeai.backend.util.vae_working_memory import estimate_vae_working_memory_sd15_sdxl
@invocation(
@@ -53,39 +48,6 @@ class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
tile_size: int = InputField(default=0, multiple_of=8, description=FieldDescriptions.vae_tile_size)
fp32: bool = InputField(default=False, description=FieldDescriptions.fp32)
def _estimate_working_memory(
self, latents: torch.Tensor, use_tiling: bool, vae: AutoencoderKL | AutoencoderTiny
) -> int:
"""Estimate the working memory required by the invocation in bytes."""
# It was found experimentally that the peak working memory scales linearly with the number of pixels and the
# element size (precision). This estimate is accurate for both SD1 and SDXL.
element_size = 4 if self.fp32 else 2
scaling_constant = 2200 # Determined experimentally.
if use_tiling:
tile_size = self.tile_size
if tile_size == 0:
tile_size = vae.tile_sample_min_size
assert isinstance(tile_size, int)
out_h = tile_size
out_w = tile_size
working_memory = out_h * out_w * element_size * scaling_constant
# We add 25% to the working memory estimate when tiling is enabled to account for factors like tile overlap
# and number of tiles. We could make this more precise in the future, but this should be good enough for
# most use cases.
working_memory = working_memory * 1.25
else:
out_h = LATENT_SCALE_FACTOR * latents.shape[-2]
out_w = LATENT_SCALE_FACTOR * latents.shape[-1]
working_memory = out_h * out_w * element_size * scaling_constant
if self.fp32:
# If we are running in FP32, then we should account for the likely increase in model size (~250MB).
working_memory += 250 * 2**20
return int(working_memory)
@torch.no_grad()
def invoke(self, context: InvocationContext) -> ImageOutput:
latents = context.tensors.load(self.latents.latents_name)
@@ -94,8 +56,13 @@ class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
vae_info = context.models.load(self.vae.vae)
assert isinstance(vae_info.model, (AutoencoderKL, AutoencoderTiny))
estimated_working_memory = self._estimate_working_memory(latents, use_tiling, vae_info.model)
estimated_working_memory = estimate_vae_working_memory_sd15_sdxl(
operation="decode",
image_tensor=latents,
vae=vae_info.model,
tile_size=self.tile_size if use_tiling else None,
fp32=self.fp32,
)
with (
SeamlessExt.static_patch_model(vae_info.model, self.vae.seamless_axes),
vae_info.model_on_device(working_mem_bytes=estimated_working_memory) as (_, vae),
@@ -104,26 +71,9 @@ class LatentsToImageInvocation(BaseInvocation, WithMetadata, WithBoard):
assert isinstance(vae, (AutoencoderKL, AutoencoderTiny))
latents = latents.to(TorchDevice.choose_torch_device())
if self.fp32:
# FP32 mode: convert everything to float32 for maximum precision
vae.to(dtype=torch.float32)
use_torch_2_0_or_xformers = hasattr(vae.decoder, "mid_block") and isinstance(
vae.decoder.mid_block.attentions[0].processor,
(
AttnProcessor2_0,
XFormersAttnProcessor,
LoRAXFormersAttnProcessor,
LoRAAttnProcessor2_0,
),
)
# if xformers or torch_2_0 is used attention block does not need
# to be in float32 which can save lots of memory
if use_torch_2_0_or_xformers:
vae.post_quant_conv.to(latents.dtype)
vae.decoder.conv_in.to(latents.dtype)
vae.decoder.mid_block.to(latents.dtype)
else:
latents = latents.float()
latents = latents.float()
else:
vae.to(dtype=torch.float16)
latents = latents.half()

View File

@@ -6,11 +6,12 @@ from pydantic import field_validator
from transformers import AutoProcessor, LlavaOnevisionForConditionalGeneration, LlavaOnevisionProcessor
from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, invocation
from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, UIComponent, UIType
from invokeai.app.invocations.fields import FieldDescriptions, ImageField, InputField, UIComponent
from invokeai.app.invocations.model import ModelIdentifierField
from invokeai.app.invocations.primitives import StringOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.llava_onevision_pipeline import LlavaOnevisionPipeline
from invokeai.backend.model_manager.taxonomy import ModelType
from invokeai.backend.util.devices import TorchDevice
@@ -34,7 +35,7 @@ class LlavaOnevisionVllmInvocation(BaseInvocation):
vllm_model: ModelIdentifierField = InputField(
title="LLaVA Model Type",
description=FieldDescriptions.vllm_model,
ui_type=UIType.LlavaOnevisionModel,
ui_model_type=ModelType.LlavaOnevision,
)
@field_validator("images", mode="before")

View File

@@ -150,6 +150,10 @@ GENERATION_MODES = Literal[
"flux_img2img",
"flux_inpaint",
"flux_outpaint",
"flux2_txt2img",
"flux2_img2img",
"flux2_inpaint",
"flux2_outpaint",
"sd3_txt2img",
"sd3_img2img",
"sd3_inpaint",
@@ -158,6 +162,10 @@ GENERATION_MODES = Literal[
"cogview4_img2img",
"cogview4_inpaint",
"cogview4_outpaint",
"z_image_txt2img",
"z_image_img2img",
"z_image_inpaint",
"z_image_outpaint",
]
@@ -166,7 +174,7 @@ GENERATION_MODES = Literal[
title="Core Metadata",
tags=["metadata"],
category="metadata",
version="2.0.0",
version="2.1.0",
classification=Classification.Internal,
)
class CoreMetadataInvocation(BaseInvocation):
@@ -217,6 +225,10 @@ class CoreMetadataInvocation(BaseInvocation):
default=None,
description="The VAE used for decoding, if the main model's default was not used",
)
qwen3_encoder: Optional[ModelIdentifierField] = InputField(
default=None,
description="The Qwen3 text encoder model used for Z-Image inference",
)
# High resolution fix metadata.
hrf_enabled: Optional[bool] = InputField(

View File

@@ -52,8 +52,9 @@ from invokeai.app.invocations.primitives import (
)
from invokeai.app.invocations.scheduler import SchedulerOutput
from invokeai.app.invocations.t2i_adapter import T2IAdapterField, T2IAdapterInvocation
from invokeai.app.invocations.z_image_denoise import ZImageDenoiseInvocation
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.model_manager.taxonomy import ModelType, SubModelType
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType, SubModelType
from invokeai.backend.stable_diffusion.schedulers.schedulers import SCHEDULER_NAME_VALUES
from invokeai.version import __version__
@@ -473,7 +474,6 @@ class MetadataToModelOutput(BaseInvocationOutput):
model: ModelIdentifierField = OutputField(
description=FieldDescriptions.main_model,
title="Model",
ui_type=UIType.MainModel,
)
name: str = OutputField(description="Model Name", title="Name")
unet: UNetField = OutputField(description=FieldDescriptions.unet, title="UNet")
@@ -488,7 +488,6 @@ class MetadataToSDXLModelOutput(BaseInvocationOutput):
model: ModelIdentifierField = OutputField(
description=FieldDescriptions.main_model,
title="Model",
ui_type=UIType.SDXLMainModel,
)
name: str = OutputField(description="Model Name", title="Name")
unet: UNetField = OutputField(description=FieldDescriptions.unet, title="UNet")
@@ -519,8 +518,7 @@ class MetadataToModelInvocation(BaseInvocation, WithMetadata):
input=Input.Direct,
)
default_value: ModelIdentifierField = InputField(
description="The default model to use if not found in the metadata",
ui_type=UIType.MainModel,
description="The default model to use if not found in the metadata", ui_model_type=ModelType.Main
)
_validate_custom_label = model_validator(mode="after")(validate_custom_label)
@@ -575,7 +573,8 @@ class MetadataToSDXLModelInvocation(BaseInvocation, WithMetadata):
)
default_value: ModelIdentifierField = InputField(
description="The default SDXL Model to use if not found in the metadata",
ui_type=UIType.SDXLMainModel,
ui_model_type=ModelType.Main,
ui_model_base=BaseModelType.StableDiffusionXL,
)
_validate_custom_label = model_validator(mode="after")(validate_custom_label)
@@ -731,6 +730,52 @@ class FluxDenoiseLatentsMetaInvocation(FluxDenoiseInvocation, WithMetadata):
return LatentsMetaOutput(**params, metadata=MetadataField.model_validate(md))
@invocation(
"z_image_denoise_meta",
title=f"{ZImageDenoiseInvocation.UIConfig.title} + Metadata",
tags=["z-image", "latents", "denoise", "txt2img", "t2i", "t2l", "img2img", "i2i", "l2l"],
category="latents",
version="1.0.0",
)
class ZImageDenoiseMetaInvocation(ZImageDenoiseInvocation, WithMetadata):
"""Run denoising process with a Z-Image transformer model + metadata."""
def invoke(self, context: InvocationContext) -> LatentsMetaOutput:
def _loras_to_json(obj: Union[Any, list[Any]]):
if not isinstance(obj, list):
obj = [obj]
output: list[dict[str, Any]] = []
for item in obj:
output.append(
LoRAMetadataField(
model=item.lora,
weight=item.weight,
).model_dump(exclude_none=True, exclude={"id", "type", "is_intermediate", "use_cache"})
)
return output
obj = super().invoke(context)
md: Dict[str, Any] = {} if self.metadata is None else self.metadata.root
md.update({"width": obj.width})
md.update({"height": obj.height})
md.update({"steps": self.steps})
md.update({"guidance": self.guidance_scale})
md.update({"denoising_start": self.denoising_start})
md.update({"denoising_end": self.denoising_end})
md.update({"scheduler": self.scheduler})
md.update({"model": self.transformer.transformer})
md.update({"seed": self.seed})
if len(self.transformer.loras) > 0:
md.update({"loras": _loras_to_json(self.transformer.loras)})
params = obj.__dict__.copy()
del params["type"]
return LatentsMetaOutput(**params, metadata=MetadataField.model_validate(md))
@invocation(
"metadata_to_vae",
title="Metadata To VAE",

View File

@@ -9,12 +9,10 @@ from invokeai.app.invocations.baseinvocation import (
invocation,
invocation_output,
)
from invokeai.app.invocations.fields import FieldDescriptions, ImageField, Input, InputField, OutputField, UIType
from invokeai.app.invocations.fields import FieldDescriptions, ImageField, Input, InputField, OutputField
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.app.shared.models import FreeUConfig
from invokeai.backend.model_manager.config import (
AnyModelConfig,
)
from invokeai.backend.model_manager.configs.factory import AnyModelConfig
from invokeai.backend.model_manager.taxonomy import BaseModelType, ModelType, SubModelType
@@ -24,8 +22,9 @@ class ModelIdentifierField(BaseModel):
name: str = Field(description="The model's name")
base: BaseModelType = Field(description="The model's base model type")
type: ModelType = Field(description="The model's type")
submodel_type: Optional[SubModelType] = Field(
description="The submodel to load, if this is a main model", default=None
submodel_type: SubModelType | None = Field(
description="The submodel to load, if this is a main model",
default=None,
)
@classmethod
@@ -73,6 +72,14 @@ class GlmEncoderField(BaseModel):
text_encoder: ModelIdentifierField = Field(description="Info to load text_encoder submodel")
class Qwen3EncoderField(BaseModel):
"""Field for Qwen3 text encoder used by Z-Image models."""
tokenizer: ModelIdentifierField = Field(description="Info to load tokenizer submodel")
text_encoder: ModelIdentifierField = Field(description="Info to load text_encoder submodel")
loras: List[LoRAField] = Field(default_factory=list, description="LoRAs to apply on model loading")
class VAEField(BaseModel):
vae: ModelIdentifierField = Field(description="Info to load vae submodel")
seamless_axes: List[str] = Field(default_factory=list, description='Axes("x" and "y") to which apply seamless')
@@ -145,7 +152,7 @@ class ModelIdentifierInvocation(BaseInvocation):
@invocation(
"main_model_loader",
title="Main Model - SD1.5",
title="Main Model - SD1.5, SD2",
tags=["model"],
category="model",
version="1.0.4",
@@ -153,7 +160,11 @@ class ModelIdentifierInvocation(BaseInvocation):
class MainModelLoaderInvocation(BaseInvocation):
"""Loads a main model, outputting its submodels."""
model: ModelIdentifierField = InputField(description=FieldDescriptions.main_model, ui_type=UIType.MainModel)
model: ModelIdentifierField = InputField(
description=FieldDescriptions.main_model,
ui_model_base=[BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2],
ui_model_type=ModelType.Main,
)
# TODO: precision?
def invoke(self, context: InvocationContext) -> ModelLoaderOutput:
@@ -187,7 +198,10 @@ class LoRALoaderInvocation(BaseInvocation):
"""Apply selected lora to unet and text_encoder."""
lora: ModelIdentifierField = InputField(
description=FieldDescriptions.lora_model, title="LoRA", ui_type=UIType.LoRAModel
description=FieldDescriptions.lora_model,
title="LoRA",
ui_model_base=BaseModelType.StableDiffusion1,
ui_model_type=ModelType.LoRA,
)
weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
unet: Optional[UNetField] = InputField(
@@ -250,7 +264,9 @@ class LoRASelectorInvocation(BaseInvocation):
"""Selects a LoRA model and weight."""
lora: ModelIdentifierField = InputField(
description=FieldDescriptions.lora_model, title="LoRA", ui_type=UIType.LoRAModel
description=FieldDescriptions.lora_model,
title="LoRA",
ui_model_type=ModelType.LoRA,
)
weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
@@ -332,7 +348,10 @@ class SDXLLoRALoaderInvocation(BaseInvocation):
"""Apply selected lora to unet and text_encoder."""
lora: ModelIdentifierField = InputField(
description=FieldDescriptions.lora_model, title="LoRA", ui_type=UIType.LoRAModel
description=FieldDescriptions.lora_model,
title="LoRA",
ui_model_base=BaseModelType.StableDiffusionXL,
ui_model_type=ModelType.LoRA,
)
weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
unet: Optional[UNetField] = InputField(
@@ -473,13 +492,27 @@ class SDXLLoRACollectionLoader(BaseInvocation):
@invocation(
"vae_loader", title="VAE Model - SD1.5, SDXL, SD3, FLUX", tags=["vae", "model"], category="model", version="1.0.4"
"vae_loader",
title="VAE Model - SD1.5, SD2, SDXL, SD3, FLUX",
tags=["vae", "model"],
category="model",
version="1.0.4",
)
class VAELoaderInvocation(BaseInvocation):
"""Loads a VAE model, outputting a VaeLoaderOutput"""
vae_model: ModelIdentifierField = InputField(
description=FieldDescriptions.vae_model, title="VAE", ui_type=UIType.VAEModel
description=FieldDescriptions.vae_model,
title="VAE",
ui_model_base=[
BaseModelType.StableDiffusion1,
BaseModelType.StableDiffusion2,
BaseModelType.StableDiffusionXL,
BaseModelType.StableDiffusion3,
BaseModelType.Flux,
BaseModelType.Flux2,
],
ui_model_type=ModelType.VAE,
)
def invoke(self, context: InvocationContext) -> VAEOutput:

View File

@@ -0,0 +1,59 @@
import pathlib
from typing import Literal
from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output
from invokeai.app.invocations.fields import ImageField, InputField, OutputField, WithBoard, WithMetadata
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.image_util.pbr_maps.architecture.pbr_rrdb_net import PBR_RRDB_Net
from invokeai.backend.image_util.pbr_maps.pbr_maps import NORMAL_MAP_MODEL, OTHER_MAP_MODEL, PBRMapsGenerator
from invokeai.backend.util.devices import TorchDevice
@invocation_output("pbr_maps-output")
class PBRMapsOutput(BaseInvocationOutput):
normal_map: ImageField = OutputField(default=None, description="The generated normal map")
roughness_map: ImageField = OutputField(default=None, description="The generated roughness map")
displacement_map: ImageField = OutputField(default=None, description="The generated displacement map")
@invocation("pbr_maps", title="PBR Maps", tags=["image", "material"], category="image", version="1.0.0")
class PBRMapsInvocation(BaseInvocation, WithMetadata, WithBoard):
"""Generate Normal, Displacement and Roughness Map from a given image"""
image: ImageField = InputField(description="Input image")
tile_size: int = InputField(default=512, description="Tile size")
border_mode: Literal["none", "seamless", "mirror", "replicate"] = InputField(
default="none", description="Border mode to apply to eliminate any artifacts or seams"
)
def invoke(self, context: InvocationContext) -> PBRMapsOutput:
image_pil = context.images.get_pil(self.image.image_name, mode="RGB")
def loader(model_path: pathlib.Path):
return PBRMapsGenerator.load_model(model_path, TorchDevice.choose_torch_device())
torch_device = TorchDevice.choose_torch_device()
with (
context.models.load_remote_model(NORMAL_MAP_MODEL, loader) as normal_map_model,
context.models.load_remote_model(OTHER_MAP_MODEL, loader) as other_map_model,
):
assert isinstance(normal_map_model, PBR_RRDB_Net)
assert isinstance(other_map_model, PBR_RRDB_Net)
pbr_pipeline = PBRMapsGenerator(normal_map_model, other_map_model, torch_device)
normal_map, roughness_map, displacement_map = pbr_pipeline.generate_maps(
image_pil, self.tile_size, self.border_mode
)
normal_map = context.images.save(normal_map)
normal_map_field = ImageField(image_name=normal_map.image_name)
roughness_map = context.images.save(roughness_map)
roughness_map_field = ImageField(image_name=roughness_map.image_name)
displacement_map = context.images.save(displacement_map)
displacement_map_field = ImageField(image_name=displacement_map.image_name)
return PBRMapsOutput(
normal_map=normal_map_field, roughness_map=roughness_map_field, displacement_map=displacement_map_field
)

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