Compare commits

..

649 Commits

Author SHA1 Message Date
psychedelicious
d4165317aa chore: release v4.2.9.dev12 2024-09-05 22:41:00 +10:00
psychedelicious
92482bf50d fix(ui): missing translation 2024-09-05 22:41:00 +10:00
psychedelicious
0ad118f1e9 fix(ui): save to gallery uses auto-add board 2024-09-05 22:41:00 +10:00
psychedelicious
da6d0c139b fix(ui): cancel transform/filter when deleting entity 2024-09-05 22:41:00 +10:00
psychedelicious
3103b3e440 chore(ui): lint 2024-09-05 22:41:00 +10:00
psychedelicious
c9ac44b061 feat(ui): iterate on state flow and rendering 2
- Rely on redux + reselect more
- Remove all nanostores that simply "mirrored" redux state in favor of direct subscriptions to redux store
- Add abstractions for creating redux subs and running selectors
- Add `initialize` method to CanvasModuleBase, for post-instantiation tasks
- Reduce local caching of state in modules to a minimum
2024-09-05 22:41:00 +10:00
psychedelicious
edfbf11a1c feat(ui): iterate on state flow and rendering 2024-09-05 22:41:00 +10:00
psychedelicious
4807657ac9 feat(ui): slight layout change for staging area toolbar 2024-09-05 22:41:00 +10:00
psychedelicious
e665ca0743 feat(ui): clean up adapter API 2024-09-05 22:41:00 +10:00
psychedelicious
6ec6d978ac feat(ui): streamlined state flow 2024-09-05 22:41:00 +10:00
psychedelicious
94da066d2d fix(ui): handle optimal dimension when resetting canvas 2024-09-05 22:41:00 +10:00
psychedelicious
78c070ade1 feat(ui): background and staging area modules have own store subscription and render themselves 2024-09-05 22:41:00 +10:00
psychedelicious
f587d236ed feat(ui): make rendering methods not need args
They should pull from the entity's state directly. This allows more freedom with updating the canvas.
2024-09-05 22:41:00 +10:00
psychedelicious
129cd91267 feat(ui): restore size of invoke button 2024-09-05 22:41:00 +10:00
psychedelicious
8fb8916027 tidy(ui): remove unnecessary awaits in rendering module 2024-09-05 22:41:00 +10:00
psychedelicious
139cf29e32 tidy(ui): rename some classes to better represent their responsibilities 2024-09-05 22:41:00 +10:00
psychedelicious
7cc9aa5b99 feat(ui): abstract out CanvasEntityAdapterBase
Things were getting to complex to reason about & classes a bit complicated. Trying to simplify...
2024-09-05 22:41:00 +10:00
psychedelicious
a2ba8700d4 feat(ui): revise entity rendering flow 2024-09-05 22:41:00 +10:00
psychedelicious
facd007d1e tidy(ui): remove unused id on konva nodes 2024-09-05 22:41:00 +10:00
psychedelicious
f63aab9730 tidy(ui): remove commented code 2024-09-05 22:41:00 +10:00
psychedelicious
eb230feb57 tidy(ui): remove extraneous docstrings 2024-09-05 22:41:00 +10:00
psychedelicious
438fba478c feat(ui): clean up unused tool module state 2024-09-05 22:41:00 +10:00
psychedelicious
4b65891b65 tidy(ui): disable isDebugging flag on root component 2024-09-05 22:41:00 +10:00
psychedelicious
b3c2d4d4b2 fix(ui): unable to drag while transforming after switching tools 2024-09-05 22:41:00 +10:00
psychedelicious
d535ea6119 feat(ui): prevent layer interactions when transforming or filtering 2024-09-05 22:41:00 +10:00
psychedelicious
c4ab0c9c96 feat(ui): add compositeMaskedRegions setting 2024-09-05 22:41:00 +10:00
psychedelicious
4b79d54b4f tidy(ui): merge tool slice, sendToCanvas into settings slice 2024-09-05 22:41:00 +10:00
psychedelicious
9226165530 build(ui): add csstype dev dependency 2024-09-05 22:41:00 +10:00
psychedelicious
292770e188 feat(ui): clean up tool preview rendering 2024-09-05 22:41:00 +10:00
psychedelicious
3347094254 feat(ui): tool buttons are only disabled when currently selected 2024-09-05 22:41:00 +10:00
psychedelicious
491b049e12 feat(ui): better types on CanvasStateApiModule.getEntity 2024-09-05 22:41:00 +10:00
psychedelicious
6f8fac3f73 feat(ui): update default logging context path to be string 2024-09-05 22:41:00 +10:00
psychedelicious
96ecf492cc tidy(ui): mark canvas module attrs readonly 2024-09-05 22:41:00 +10:00
psychedelicious
22597f5e0e chore: release v4.2.9.dev11 2024-09-05 22:41:00 +10:00
psychedelicious
42e2812ed2 feat(ui): tidy stateApi atoms & add docstrings 2024-09-05 22:41:00 +10:00
psychedelicious
689dd24296 feat(ui): streamline manager -> react transform interface 2024-09-05 22:41:00 +10:00
psychedelicious
f2bb078a48 tidy(ui): remove unused $isProcessingTransform atom 2024-09-05 22:41:00 +10:00
psychedelicious
0ae1004520 docs(ui): docstrings for $canvasCache 2024-09-05 22:41:00 +10:00
psychedelicious
89f3a8b91b feat(ui): tweak bookmark verbiage 2024-09-05 22:41:00 +10:00
psychedelicious
3edef0fc73 feat(ui): move transformer state to nanostores
This provides some free reactivity for this canvas-manager-managed state.
2024-09-05 22:41:00 +10:00
psychedelicious
aa0942e527 fix(ui): transform should ignore konva filters (e.g. transparency effect) 2024-09-05 22:41:00 +10:00
psychedelicious
7145c91bd2 feat(ui): add fit to bbox as transform helper 2024-09-05 22:41:00 +10:00
psychedelicious
846a88c0b8 tidy(ui): transformer organisation 2024-09-05 22:41:00 +10:00
psychedelicious
abc75e6b1b fix(ui): disable merge visible when 1 or fewer layers of type 2024-09-05 22:41:00 +10:00
psychedelicious
dc6bd98266 feat(ui): brush preview opacity at 0.5 when drawing on mask 2024-09-05 22:41:00 +10:00
psychedelicious
8df9c43079 chore(ui): lint 2024-09-05 22:41:00 +10:00
psychedelicious
ef95fee63a fix(ui): edge cases in quick switch, simpler logic 2024-09-05 22:41:00 +10:00
psychedelicious
9674485723 chore(ui): lint 2024-09-05 22:41:00 +10:00
psychedelicious
0f70989f19 feat(ui): add bookmark for quick switch 2024-09-05 22:41:00 +10:00
psychedelicious
fa691fc8d0 fix(ui): force dims on scaled bbox when manual scaling + locked aspect ratio
Closes #5590
2024-09-05 22:41:00 +10:00
psychedelicious
4a6d901a2b feat(ui): "Control Layers" -> "Layers" 2024-09-05 22:41:00 +10:00
psychedelicious
2ea8f87d82 feat(ui): "IP Adapter" -> "Global IP Adapter" 2024-09-05 22:41:00 +10:00
psychedelicious
f4b654d37c tidy(ui): canvas hotkey hooks 2024-09-05 22:41:00 +10:00
psychedelicious
7f4eab2400 feat(ui): add alt+[ and alt+] hotkeys to cycle through layers 2024-09-05 22:41:00 +10:00
psychedelicious
c7c32d67ea feat(ui): add layer quick switch
Q toggles between the last-selected layers.
2024-09-05 22:41:00 +10:00
psychedelicious
571a5f9865 feat(ui): bbox hotkey is c 2024-09-05 22:41:00 +10:00
psychedelicious
784c3b0454 fix(ui): select nonexistent entity 2024-09-05 22:41:00 +10:00
psychedelicious
1b3d415c35 feat(ui): brush & eraser width ui/ux
Use same pattern as canvas scale & opacity sliders w/ scaled slider values for precision at low values.
2024-09-05 22:41:00 +10:00
psychedelicious
c43cc0814a tidy(ui): canvas scale & entity opacity sliders 2024-09-05 22:41:00 +10:00
psychedelicious
f0332efdf3 feat(ui): hotkeys for brush/eraser size 2024-09-05 22:41:00 +10:00
psychedelicious
ff0109db52 feat(ui): use default IP adapter when creating IP adapter 2024-09-05 22:41:00 +10:00
psychedelicious
d0f8f3995f tidy(ui): organise files 2024-09-05 22:41:00 +10:00
psychedelicious
4fd1d856b8 feat(ui): remove object count from entity title
This was used for troubleshooting only.
2024-09-05 22:41:00 +10:00
psychedelicious
7697525f04 tidy(ui): misc cleanup 2024-09-05 22:41:00 +10:00
psychedelicious
31fed50f11 docs(ui): docstrings for classes (wip) 2024-09-05 22:41:00 +10:00
psychedelicious
440a75fec6 feat(ui): revised canvas module base class
Big cleanup. Makes these classes easier to implement, lots of comments and docstrings to clarify how it all works.

- Add default implementations for `destroy`, `repr` and `getLoggingContext`
- Tidy individual module configs
- Update `CanvasManager.buildLogger` to accept a canvas module as the arg
- Add `CanvasManager.buildPath`
2024-09-05 22:41:00 +10:00
psychedelicious
72fd370ba6 feat(ui): split canvas tool previews into modules 2024-09-05 22:41:00 +10:00
psychedelicious
6f9085d2d9 fix(ui): reject on dataURLToImageData 2024-09-05 22:41:00 +10:00
psychedelicious
dd5de2dc95 fix(ui): correctly set last cursor pos to null 2024-09-05 22:41:00 +10:00
psychedelicious
0f9708373d chore: release v4.2.9.dev10 2024-09-05 22:41:00 +10:00
psychedelicious
5f7e6379ad feat(ui): remove entity list context menu (again)
stupid events
2024-09-05 22:41:00 +10:00
psychedelicious
26e9936240 fix(ui): entity groups not collapsing 2024-09-05 22:41:00 +10:00
psychedelicious
f863c08a55 chore: release v4.2.9.dev9 2024-09-05 22:41:00 +10:00
psychedelicious
ba7420c6e7 fix(ui): entity opacity number input focus prevents slider from opening 2024-09-05 22:41:00 +10:00
psychedelicious
263c251cb3 feat(ui): add merge visible for raster and inpaint mask layers
I don't think it makes sense to merge control layers or regional guidance layers because they have additional state.
2024-09-05 22:41:00 +10:00
psychedelicious
5f21d01f35 fix(ui): save to gallery rect too large
Was including all layer types in the rect - only want the raster layers.
2024-09-05 22:41:00 +10:00
psychedelicious
db333c1c6f fix(ui): canvasToBlob not raising error correctly 2024-09-05 22:41:00 +10:00
psychedelicious
f6f077d0b8 feat(ui): add save to gallery button 2024-09-05 22:41:00 +10:00
psychedelicious
5dfa5c9a48 fix(ui): fix getRectUnion util, add some tests 2024-09-05 22:41:00 +10:00
psychedelicious
bfc4f4a88b fix(ui): modals not staying open
TBH not sure exactly why this broke. Fixed by rollback back the use of a render prop in favor of global state. Also revised the API of `useBoolean` and `buildUseBoolean`.
2024-09-05 22:41:00 +10:00
psychedelicious
efb99695a7 fix(ui): correct labels for generation tab origin 2024-09-05 22:41:00 +10:00
psychedelicious
a72c38273c fix(ui): context menu doesn't work for new entities
I do not understand why this fixes the issue, doesn't seem like it should. But it does.
2024-09-05 22:41:00 +10:00
psychedelicious
7d06453086 tidy(ui): organise tool module 2024-09-05 22:41:00 +10:00
psychedelicious
35654c38dc fix(ui): staging hotkeys enabled at wrong times 2024-09-05 22:41:00 +10:00
psychedelicious
e2fde5c152 fix(ui): incorrect batch origin preventing progress/staging 2024-09-05 22:41:00 +10:00
psychedelicious
35a74f99d0 feat(ui): restore minimal HUD 2024-09-05 22:41:00 +10:00
psychedelicious
48b4e00373 feat(ui): remove unused asPreview for StageComponent 2024-09-05 22:41:00 +10:00
psychedelicious
c51cdbec35 chore(ui): lint 2024-09-05 22:41:00 +10:00
psychedelicious
64ac64e9f6 chore: release v4.2.9.dev8 2024-09-05 22:41:00 +10:00
psychedelicious
550842fb61 feat(ui): revise generation mode logic
- Canvas generation mode is replace with a boolean `sendToCanvas` flag. When off, images generated on the canvas go to the gallery. When on, they get added to the staging area.
- When an image result is received, if its destination is the canvas, staging is automatically started.
- Updated queue list to show the destination column.
- Added `IconSwitch` component to represent binary choices, used for the new `sendToCanvas` flag and image viewer toggle.
- Remove the queue actions menu in `QueueControls`. Move the queue count badge to the cancel button.
- Redo layout of `QueueControls` to prevent duplicate queue count badges.
- Fix issue where gallery and options panels could show thru transparent regions of queue tab.
- Disable panel hotkeys when on mm/queue tabs.
2024-09-05 22:41:00 +10:00
psychedelicious
647aae8dd1 chore(ui): typegen 2024-09-05 22:41:00 +10:00
psychedelicious
b18acdda6b feat(app): add destination column to session_queue
The frontend needs to know where queue items came from (i.e. which tab), and where results are going to (i.e. send images to gallery or canvas). The `origin` column is not quite enough to represent this cleanly.

A `destination` column provides the frontend what it needs to handle incoming generations.
2024-09-05 22:41:00 +10:00
psychedelicious
f501e6ea29 tidy(ui): ViewerToggleMenu -> ViewerToggle 2024-09-05 22:41:00 +10:00
psychedelicious
c3eb691e57 feat(ui): alt quick switches to color picker 2024-09-05 22:41:00 +10:00
psychedelicious
39a004c20e feat(ui): tweak add entity button layout 2024-09-05 22:41:00 +10:00
psychedelicious
6f5674659e feat(ui): restore context menu for entity list 2024-09-05 22:41:00 +10:00
psychedelicious
bbfaa60821 feat(ui): add delete button to each layer 2024-09-05 22:41:00 +10:00
psychedelicious
9aa3ffffee feat(ui): add + buttons to entity categories 2024-09-05 22:41:00 +10:00
psychedelicious
2b1d442269 feat(ui): tweak brush fill UI 2024-09-05 22:41:00 +10:00
psychedelicious
4433cd2749 feat(ui): do not select layer on staging accept 2024-09-05 22:40:59 +10:00
psychedelicious
05931cc06b fix(ui): more fiddly queue count layout stuff 2024-09-05 22:40:59 +10:00
psychedelicious
261dd0cb40 fix(ui): floating params panel invoke button loading state 2024-09-05 22:40:59 +10:00
psychedelicious
12298008c7 feat(ui): move canvas undo/redo to hook 2024-09-05 22:40:59 +10:00
psychedelicious
69f9932f37 fix(ui): queue count badge positioning 2024-09-05 22:40:59 +10:00
psychedelicious
a52060ca33 fix(ui): add node cmdk only enabled on workflows tab 2024-09-05 22:40:59 +10:00
psychedelicious
82e804ea2c chore: release v4.2.9.dev7 2024-09-05 22:40:59 +10:00
psychedelicious
ef4cff5113 fix(ui): pending node connection stuck 2024-09-05 22:40:59 +10:00
psychedelicious
9b2405f185 chore(ui): lint 2024-09-05 22:40:59 +10:00
psychedelicious
0359cb7365 chore: release v4.2.9.dev6 2024-09-05 22:40:59 +10:00
psychedelicious
57cb08a05b feat(ui): migrate add node popover to cmdk
Put this together as a way to figure out the library before moving on to the full app cmdk. Works great.
2024-09-05 22:40:59 +10:00
psychedelicious
29ae30b974 fix(ui): schema parsing now that node_pack is guaranteed to be present 2024-09-05 22:40:59 +10:00
psychedelicious
4a74f67258 chore(ui): typegen 2024-09-05 22:40:59 +10:00
psychedelicious
b02c4d6bf8 fix(app): node_pack not added to openapi schema correctly 2024-09-05 22:40:59 +10:00
psychedelicious
e7a8992f59 fix(ui): unnecessary z-index on invoke button 2024-09-05 22:40:59 +10:00
psychedelicious
6875e72b40 feat(ui): split settings modal 2024-09-05 22:40:59 +10:00
psychedelicious
7ca732b9bf perf(ui): disable useInert on modals
This hook forcibly updates _all_ portals with `data-hidden=true` when the modal opens - then reverts it when the modal closes. It's intended to help screen readers. Unfortunately, this absolutely tanks performance because we have many portals. React needs to do alot of layout calculations (not re-renders).

IMO this behaviour is a bug in chakra. The modals which generated the portals are hidden by default, so this data attr should really be set by default. Dunno why it isn't.
2024-09-05 22:40:59 +10:00
psychedelicious
0e6a11f53d feat(ui): fix queue item count badge positioning
Previously this badge, floating over the queue menu button next to the invoke button, was rendered within the existing layout. When I initially positioned it, the app layout interfered - it would extend into an area reserved for a flex gap, which cut off the badge.

As a (bad) workaround, I had shifted the whole app down a few pixels to make room for it. What I should have done is what I've done in this commit - render the badge in a portal to take it out of the layout so we don't need that extra vertical padding.

Sleekified some styling a bit too.
2024-09-05 22:40:59 +10:00
psychedelicious
1681ae0d49 fix(ui): transparency effect not updating 2024-09-05 22:40:59 +10:00
psychedelicious
0a6c63f10b feat(ui): tidy canvas toolbar buttons 2024-09-05 22:40:59 +10:00
psychedelicious
2cb218e69a feat(ui): revised viewer toggle @joshistoast 2024-09-05 22:40:59 +10:00
psychedelicious
4ea1622260 fix(ui): opacity reset value incorrect 2024-09-05 22:40:59 +10:00
psychedelicious
78fff1c7bc revert(ui): roll back flip, doesn't work with rotate yet 2024-09-05 22:40:59 +10:00
psychedelicious
8a860eeecd fix(ui): disable opacity slider fully when no valid entity selected 2024-09-05 22:40:59 +10:00
psychedelicious
ba5fef621a fix(ui): layer preview image sometimes not rendering
The canvas size was dynamic based on the container div's size. When the div was hidden (e.g. when selecting another tab), the container's effective size is 0. This resulted in the preview image canvas being drawn at a scale of 0.

Fixed by using an absolute size for the canvas container.
2024-09-05 22:40:59 +10:00
psychedelicious
0920a8f28f feat(ui): tweak regional prompt box styles 2024-09-05 22:40:59 +10:00
psychedelicious
fbc6680773 feat(ui): tweak enabled/locked toggle styles 2024-09-05 22:40:59 +10:00
psychedelicious
1b945d2d42 feat(ui): tweak filter styling 2024-09-05 22:40:59 +10:00
psychedelicious
4a934305f5 feat(ui): add flip & reset to transform 2024-09-05 22:40:59 +10:00
psychedelicious
829b680b4d tidy(ui): use helper to sync scaled bbox size on model change 2024-09-05 22:40:59 +10:00
psychedelicious
abb02ecdb7 fix(ui): randomize seed toggle linked to prompt concat 2024-09-05 22:40:59 +10:00
psychedelicious
db2003b3b6 chore: release v4.2.9.dev5 2024-09-05 22:40:59 +10:00
psychedelicious
86d3b60f54 chore(ui): lint 2024-09-05 22:40:59 +10:00
psychedelicious
2493d3f841 feat(ui): generalize mask fill, add to action bar 2024-09-05 22:40:59 +10:00
psychedelicious
63c61c7fa6 feat(ui): implement interaction locking on layers 2024-09-05 22:40:59 +10:00
psychedelicious
a584453fb2 feat(ui): iterate on layer actions
- Add lock toggle
- Tweak lock and enabled styles
- Update entity list action bar w/ delete & delete all
- Move add layer menu to action bar
- Adjust opacity slider style
2024-09-05 22:40:59 +10:00
psychedelicious
c2dd0bed17 feat(ui): collapsible entity groups 2024-09-05 22:40:59 +10:00
psychedelicious
4f793d750d tidy(ui): rename some classes to be consistent 2024-09-05 22:40:59 +10:00
psychedelicious
6c49921c76 feat(ui): tuned canvas undo/redo
- Throttle pushing to history for actions of the same type, starting with 1000ms throttle.
- History has a limit of 64 items, same as workflow editor
- Add clear history button
- Fix an issue where entity transformers would reset the entity state when the entity is fully transparent, resetting the redo stack. This could happen when you undo to the starting state of a layer
2024-09-05 22:40:59 +10:00
psychedelicious
41ece76d61 tidy(ui): move all undoable reducers back to canvas slice 2024-09-05 22:40:59 +10:00
psychedelicious
6c4c58206d fix(ui): dnd image count 2024-09-05 22:40:59 +10:00
psychedelicious
ee71ab3330 fix(ui): canvas entity opacity scale 2024-09-05 22:40:59 +10:00
psychedelicious
e83069ed94 perf(ui): optimize all selectors 2
Mostly selector optimization. Still a few places to tidy up but I'll get to that later.
2024-09-05 22:40:59 +10:00
psychedelicious
4ad748514e perf(ui): optimize all selectors 1
I learned that the inline selector syntax recreates the selector function on every render:

```ts
const val = useAppSelector((s) => s.slice.val)
```

Not good! Better is to create a selector outside the function and use it. Doing that for all selectors now, most of the way through now. Feels snappier.
2024-09-05 22:40:59 +10:00
psychedelicious
b649bf2556 feat(ui): rough out undo/redo on canvas 2024-09-05 22:40:59 +10:00
psychedelicious
fbbbef4aef chore: release v4.2.9.dev4
Canvas dev build.
2024-09-05 22:40:59 +10:00
psychedelicious
734fca622c fix(ui): handle error from internal konva method
We are dipping into konva's private API for preview images and it appears to be unsafe (got an error once). Wrapped in a try/catch.
2024-09-05 22:40:59 +10:00
psychedelicious
958fae1370 feat(ui): split out loras state from canvas rendering state 2024-09-05 22:40:59 +10:00
psychedelicious
eac0bdcd9b feat(ui): split out session state from canvas rendering state 2024-09-05 22:40:59 +10:00
psychedelicious
29b7d1f7a6 feat(ui): split out settings state from canvas rendering state 2024-09-05 22:40:59 +10:00
psychedelicious
aff8209764 feat(ui): split out tool state from canvas rendering state 2024-09-05 22:40:59 +10:00
psychedelicious
e9ec9840f1 feat(ui): split out params/compositing state from canvas rendering state
First step to restoring undo/redo - the undoable state must be in its own slice. So params and settings must be isolated.
2024-09-05 22:40:59 +10:00
psychedelicious
afbe5d7e07 feat(ui): add CanvasModuleBase class to standardize canvas APIs
I did this ages ago but undid it for some reason, not sure why. Caught a few issues related to subscriptions.
2024-09-05 22:40:59 +10:00
psychedelicious
71c7dabb48 feat(ui): move selected tool and tool buffer out of redux
This ephemeral state can live in the canvas classes.
2024-09-05 22:40:59 +10:00
psychedelicious
733266fdf7 feat(ui): move ephemeral state into canvas classes
Things like `$lastCursorPos` are now created within the canvas drawing classes. Consumers in react access them via `useCanvasManager`.

For example:
```tsx
const canvasManager = useCanvasManager();
const lastCursorPos = useStore(canvasManager.stateApi.$lastCursorPos);
```
2024-09-05 22:40:59 +10:00
psychedelicious
2fb79a10be feat(ui): normalize all actions to accept an entityIdentifier
Previously, canvas actions specific to an entity type only needed the id of that entity type. This allowed you to pass in the id of an entity of the wrong type.

All actions for a specific entity now take a full entity identifier, and the entity identifier type can be narrowed.

`selectEntity` and `selectEntityOrThrow` now need a full entity identifier, and narrow their return values to a specific entity type _if_ the entity identifier is narrowed.

The types for canvas entities are updated with optional type parameters for this purpose.

All reducers, actions and components have been updated.
2024-09-05 22:40:59 +10:00
psychedelicious
3dde01d642 feat(ui): move events into modules who care about them 2024-09-05 22:40:59 +10:00
psychedelicious
0e95d7f729 fix(ui): color picker resets brush opacity 2024-09-05 22:40:59 +10:00
psychedelicious
5d76a3cb4f fix(ui): scaled bbox loses sync 2024-09-05 22:40:59 +10:00
psychedelicious
67531e0dc4 feat(ui): add context menu to entity list 2024-09-05 22:40:59 +10:00
psychedelicious
a903e6eab5 chore(ui): bump @invoke-ai/ui-library 2024-09-05 22:40:59 +10:00
psychedelicious
2ea921c2ca fix(ui): missing vae precision in graph builders 2024-09-05 22:40:59 +10:00
psychedelicious
14caa82bc2 chore: release v4.2.9.dev3
Instead of using dates, just going to increment.
2024-09-05 22:40:59 +10:00
psychedelicious
a8d2670622 feat(ui): use new Result utils for enqueueing 2024-09-05 22:40:59 +10:00
psychedelicious
708f2f2814 fix(ui): graph building issue w/ controlnet 2024-09-05 22:40:59 +10:00
psychedelicious
a92f82f06f feat(ui): add Result type & helpers
Wrappers to capture errors and turn into results:
- `withResult` wraps a sync function
- `withResultAsync` wraps an async function

Comments, tests.
2024-09-05 22:40:59 +10:00
psychedelicious
45e6c5523d chore: release v4.2.9.dev20240824 2024-09-05 22:40:59 +10:00
psychedelicious
247378ed73 fix(ui): lint & fix issues with adding regional ip adapters 2024-09-05 22:40:59 +10:00
psychedelicious
51146f760c feat(ui): add knipignore tag
I'm not ready to delete some things but still want to build the app.
2024-09-05 22:40:59 +10:00
psychedelicious
6ee8de882b feat(ui): duplicate entity 2024-09-05 22:40:59 +10:00
psychedelicious
73804abb55 feat(ui): autocomplete on getPrefixeId 2024-09-05 22:40:59 +10:00
psychedelicious
f075c1dcc1 feat(ui): paste canvas gens back on source in generate mode 2024-09-05 22:40:59 +10:00
psychedelicious
b1dd3adddc chore(ui): typegen 2024-09-05 22:40:59 +10:00
psychedelicious
0a10bba783 feat(nodes): CanvasV2MaskAndCropInvocation can paste generated image back on source
This is needed for `Generate` mode.
2024-09-05 22:40:59 +10:00
psychedelicious
92c670c454 fix(ui): extraneous entity preview updates 2024-09-05 22:40:59 +10:00
psychedelicious
5b709dd458 fix(ui): newly-added entities are selected 2024-09-05 22:40:59 +10:00
psychedelicious
bbdc736e1b feat(ui): add crosshair to color picker 2024-09-05 22:40:59 +10:00
psychedelicious
c8e330101d fix(ui): color picker ignores alpha 2024-09-05 22:40:59 +10:00
psychedelicious
ea6cd090c2 fix(ui): calculate renderable entities correctly in tool module 2024-09-05 22:40:59 +10:00
psychedelicious
f50945ec89 feat(ui): better color picker 2024-09-05 22:40:59 +10:00
psychedelicious
6cffca5283 feat(ui): colored mask preview image 2024-09-05 22:40:59 +10:00
psychedelicious
07c1b5b680 fix(ui): new rectangles don't trigger rerender 2024-09-05 22:40:59 +10:00
psychedelicious
a55eb2fca9 chore: bump version v4.2.9.dev20240823 2024-09-05 22:40:59 +10:00
psychedelicious
f9e801782b feat(ui): disable most interaction while filtering 2024-09-05 22:40:43 +10:00
psychedelicious
7cd8beda56 fix(ui): filter preview offset 2024-09-05 22:40:43 +10:00
psychedelicious
8d1095bd72 feat(ui): tweak layout of staging area toolbar 2024-09-05 22:40:43 +10:00
psychedelicious
9317831648 chore(ui): typegen 2024-09-05 22:40:43 +10:00
psychedelicious
2de16d970c tidy(app): clean up app changes for canvas v2 2024-09-05 22:40:43 +10:00
psychedelicious
e99e1f3464 feat(ui): use singleton for clear q confirm dialog 2024-09-05 22:40:43 +10:00
psychedelicious
5f044f1eda fix(ui): rip out broken recall logic, NO TS ERRORS 2024-09-05 22:40:43 +10:00
psychedelicious
d443afd1fc chore(ui): lint 2024-09-05 22:40:43 +10:00
psychedelicious
28ef63991c fix(ui): staging area interaction scopes 2024-09-05 22:40:43 +10:00
psychedelicious
b60692d1ac fix(ui): staging area actions 2024-09-05 22:40:43 +10:00
psychedelicious
4cffb7df6e tidy(ui): more cleanup 2024-09-05 22:40:43 +10:00
psychedelicious
1ce52dba41 fix(ui): upscale tab graph 2024-09-05 22:40:43 +10:00
psychedelicious
047fa8a135 fix(ui): sdxl graph builder 2024-09-05 22:40:43 +10:00
psychedelicious
e664d6a6e0 fix(ui): select next entity in the list when deleting 2024-09-05 22:40:43 +10:00
psychedelicious
3532c3414f feat(ui): fix delete layer hotkey 2024-09-05 22:40:43 +10:00
psychedelicious
d8447abd64 tidy(ui): "eye dropper" -> "color picker" 2024-09-05 22:40:43 +10:00
psychedelicious
b06d4e25e1 tidy(ui): regional guidance buttons 2024-09-05 22:40:43 +10:00
psychedelicious
aeac1edb0b feat(ui): update entity list menu 2024-09-05 22:40:43 +10:00
psychedelicious
594aa9da61 feat(ui): add log debug button 2024-09-05 22:40:43 +10:00
psychedelicious
0918732f36 chore(ui): lint 2024-09-05 22:40:43 +10:00
psychedelicious
0a9bd3f691 chore(ui): prettier 2024-09-05 22:40:43 +10:00
psychedelicious
12616cd073 chore(ui): eslint 2024-09-05 22:40:43 +10:00
psychedelicious
19378199d4 tidy(ui): remove unused stuff 4 2024-09-05 22:40:43 +10:00
psychedelicious
36c2409dd6 tidy(ui): remove unused stuff 3 2024-09-05 22:40:43 +10:00
psychedelicious
849356485f tidy(ui): remove unused pkg @chakra-ui/react-use-size 2024-09-05 22:40:43 +10:00
psychedelicious
f68f98e5cd feat(ui): revise graph building for control layers, fix issues w/ invocation complete events 2024-09-05 22:40:43 +10:00
psychedelicious
c8abcd6f66 feat(ui): use unique id for metadata in Graph class 2024-09-05 22:40:43 +10:00
psychedelicious
f81c87b685 tidy(ui): remove unused stuff 2 2024-09-05 22:40:43 +10:00
psychedelicious
a807957967 tidy(ui): remove unused stuff 2024-09-05 22:40:43 +10:00
psychedelicious
314f650b45 tidy(ui): reduce use of parseify util 2024-09-05 22:40:43 +10:00
psychedelicious
2a96554935 feat(ui): refine canvas entity list items & menus 2024-09-05 22:40:43 +10:00
psychedelicious
3dbe5b3755 feat(ui): canvas layer preview, revised reactivity for adapters 2024-09-05 22:40:43 +10:00
psychedelicious
eca4a2dec7 feat(ui): add SyncableMap
Can be used with useSyncExternal store to make a `Map` reactive.
2024-09-05 22:40:43 +10:00
psychedelicious
e8cb0b0971 tidy(ui): removed unused transform methods from canvasmanager 2024-09-05 22:40:43 +10:00
psychedelicious
90799d6f1b feat(ui): transform tool ux 2024-09-05 22:40:43 +10:00
psychedelicious
86791a0701 feat(ui): rough out canvas mode 2024-09-05 22:40:43 +10:00
psychedelicious
81052d9a18 feat(ui): add canvas autosave checkbox 2024-09-05 22:40:43 +10:00
psychedelicious
f0baabf735 fix(ui): memory leak when getting image DTO
must unsubscribe!
2024-09-05 22:40:43 +10:00
psychedelicious
815d938cf6 feat(ui): rework settings menu 2024-09-05 22:40:43 +10:00
psychedelicious
81baa1e2fd feat(ui): no entities fallback buttons 2024-09-05 22:40:43 +10:00
psychedelicious
151ee00273 perf(ui): optimize gallery image delete button rendering 2024-09-05 22:40:43 +10:00
psychedelicious
949d3b016d feat(ui): remove "solid" background option 2024-09-05 22:40:43 +10:00
psychedelicious
49a2f3d7d7 tidy(ui): organise files and classes 2024-09-05 22:40:43 +10:00
psychedelicious
22d0a02a66 tidy(ui): abstract compositing logic to module 2024-09-05 22:40:43 +10:00
psychedelicious
ea5454f6b2 fix(ui): fix canvas cache property access 2024-09-05 22:40:43 +10:00
psychedelicious
8fc881080f tidy(ui): clean up CanvasFilter class 2024-09-05 22:40:43 +10:00
psychedelicious
c5ba513873 tidy(ui): clean up a few bits and bobs 2024-09-05 22:40:43 +10:00
psychedelicious
53370b6580 tidy(ui): abstract canvas rendering logic to module 2024-09-05 22:40:43 +10:00
psychedelicious
527de60428 tidy(ui): abstract caching logic to module 2024-09-05 22:40:43 +10:00
psychedelicious
af048a134e tidy(ui): abstract worker logic to module 2024-09-05 22:40:43 +10:00
psychedelicious
40682b9695 tidy(ui): abstract stage logic into module 2024-09-05 22:40:43 +10:00
psychedelicious
0487c80615 feat(ui): add entity group hiding 2024-09-05 22:40:43 +10:00
psychedelicious
303352dd1c feat(ui): move all caching out of redux
While we lose the benefit of the caches persisting across reloads, this is a much simpler way to handle things. If we need a persistent cache, we can explore it in the future.
2024-09-05 22:40:43 +10:00
psychedelicious
01b34100b3 feat(ui): revised rasterization caching
- use `stable-hash` to generate stable, non-crypto hashes for cache entries, instead of using deep object comparisons
- use an object to store image name caches
2024-09-05 22:40:43 +10:00
psychedelicious
0dcfad50ec feat(ui): revise filter implementation 2024-09-05 22:40:43 +10:00
psychedelicious
1f99426180 fix(ui): add button to delete inpaint mask 2024-09-05 22:40:43 +10:00
psychedelicious
0b898906a5 feat(ui): add contexts/hooks to access entity adapters directly 2024-09-05 22:40:43 +10:00
psychedelicious
0c46e694c8 feat(ui): add CanvasManagerProviderGate
This context waits to render its children its until the canvas manager is available. Then its children have access to the manager directly via hook.
2024-09-05 22:40:43 +10:00
psychedelicious
80e71bd1f1 feat(ui) do not set $canvasManager until ready 2024-09-05 22:40:43 +10:00
psychedelicious
5013169170 fix(ui): inpaint mask naming 2024-09-05 22:40:43 +10:00
psychedelicious
59e0c86211 feat(ui): efficient canvas compositing
Also solves issue of exporting layers at different opacities than what is visible
2024-09-05 22:40:43 +10:00
psychedelicious
82cefce743 feat(ui): allow multiple inpaint masks
This is easier than making it a nullable singleton
2024-09-05 22:40:43 +10:00
psychedelicious
8f942603c6 fix(ui): missing rasterization cache invalidations 2024-09-05 22:40:43 +10:00
psychedelicious
228cea3e29 feat(ui): iterate on filter UI, flow 2024-09-05 22:40:43 +10:00
psychedelicious
71639631c8 fix(ui): rehydration data loss 2024-09-05 22:40:43 +10:00
psychedelicious
7f0d73fe3d feat(ui): sort log namespaces 2024-09-05 22:40:43 +10:00
psychedelicious
51efa27514 fix(ui): do not merge arrays by index during rehydration 2024-09-05 22:40:43 +10:00
psychedelicious
25cf5239da fix(ui): clone parsed data during state rehydration
Without this, the objects and arrays in `parsed` could be mutated, and the log statment would show the mutated data.
2024-09-05 22:40:43 +10:00
psychedelicious
3f0ade8bff fix(ui): fix logger filter
was accidetnally replacing the filter instead of appending to it.
2024-09-05 22:40:43 +10:00
psychedelicious
8cfbb0083a fix(ui): race condition queue status
Sequence of events causing the race condition:
- Enqueue batch
- Invalidate `SessionQueueStatus` tag
- Request updated queue status via HTTP - batch still processing at this point
- Batch completes
- Event emitted saying so
- Optimistically update the queue status cache, it is correct
- HTTP request makes it back and overwrites the optimistic update, indicating the batch is still in progress

FIxed by not invalidating the cache.
2024-09-05 22:40:43 +10:00
psychedelicious
af840b85bd fix(ui): handle opacity for masks 2024-09-05 22:40:43 +10:00
psychedelicious
b8a316acf7 feat(ui): default background to checkerboard 2024-09-05 22:40:43 +10:00
psychedelicious
f2b60ddfc3 feat(ui): clean up logging namespaces, allow skipping namespaces 2024-09-05 22:40:43 +10:00
psychedelicious
8ba0293444 chore(ui): bump ui library 2024-09-05 22:40:43 +10:00
psychedelicious
99e81d88c4 fix(ui): do not allow drawing if layer disabled 2024-09-05 22:40:43 +10:00
psychedelicious
bb3812b4a3 fix(ui): stale state causing race conditions & extraneous renders 2024-09-05 22:40:43 +10:00
psychedelicious
1eee342b48 fix(ui): do not clear buffer when rendering "real" objects 2024-09-05 22:40:43 +10:00
psychedelicious
5c57c2af37 tidy(ui): remove "filter" from CanvasImageState 2024-09-05 22:40:43 +10:00
psychedelicious
48907cce32 feat(ui): better editable title 2024-09-05 22:40:43 +10:00
psychedelicious
15e4106cc0 fix(ui): stroke eraserline 2024-09-05 22:40:43 +10:00
psychedelicious
949ee5a758 feat(ui): restore transparency effect for control layers 2024-09-05 22:40:43 +10:00
psychedelicious
28fa9ca731 feat(ui): use text cursor for entity title 2024-09-05 22:40:43 +10:00
psychedelicious
8592e7bc77 tidy(ui): remove extraneous logging in CanvasStateApi 2024-09-05 22:40:43 +10:00
psychedelicious
82a8995c98 feat(ui): better buffer commit logic 2024-09-05 22:40:43 +10:00
psychedelicious
c8d1a894fc feat(ui): render buffer separately from "real" objects 2024-09-05 22:40:43 +10:00
psychedelicious
06f5b7980a fix(ui): pixelRect should always be integer 2024-09-05 22:40:43 +10:00
psychedelicious
f2d8c851c1 fix(ui): only update stage attrs when stage itself is dragged 2024-09-05 22:40:43 +10:00
psychedelicious
76b29e90b2 feat(ui): add line simplification
This fixes some awkward issues where line segments stack up.
2024-09-05 22:40:43 +10:00
psychedelicious
a87642950d fix(ui): various things listening when they need not listen 2024-09-05 22:40:43 +10:00
psychedelicious
b092817193 feat(ui): layer opacity via caching 2024-09-05 22:40:43 +10:00
psychedelicious
ecbf1712b0 feat(ui): reset view fits all visible objects 2024-09-05 22:40:43 +10:00
psychedelicious
f80c667f30 fix(ui): rerenders when changing canvas scale 2024-09-05 22:40:43 +10:00
psychedelicious
93f5e3c3a4 fix(ui): do not render rasterized layer unless renderObjects=true 2024-09-05 22:40:43 +10:00
psychedelicious
327bbcaa64 feat(ui): revise app layout strategy, add interaction scopes for hotkeys 2024-09-05 22:40:43 +10:00
psychedelicious
6e964e21ba feat(ui): tweak mask patterns 2024-09-05 22:40:43 +10:00
psychedelicious
355dd86994 fix(ui): dynamic prompts recalcs when presets are loaded 2024-09-05 22:40:43 +10:00
psychedelicious
15c0c4dc54 fix(ui): use style preset prompts correctly 2024-09-05 22:40:43 +10:00
psychedelicious
69219219e3 fix(ui): discard selected staging image not all other images 2024-09-05 22:40:43 +10:00
psychedelicious
d18682b230 fix(ui): respect image size in staging preview 2024-09-05 22:40:43 +10:00
psychedelicious
60a9d8a8a6 tidy(ui): cleanup after events change 2024-09-05 22:40:43 +10:00
psychedelicious
0d6a022730 feat(ui): move socket event handling out of redux
Download events and invocation status events (including progress images) are very frequent. There's no real need for these to pass through redux. Handling them outside redux is a significant performance win - far fewer store subscription calls, far fewer trips through middleware.

All event handling is moved outside middleware. Cleanup of unused actions and listeners to follow.
2024-09-05 22:40:43 +10:00
psychedelicious
af1df11bec fix(ui): rebase conflicts 2024-09-05 22:40:43 +10:00
psychedelicious
fe6538bf9e fix(ui): update compositing rect when fill changes 2024-09-05 22:40:43 +10:00
psychedelicious
2e4a2a77a3 feat(ui): add canvas background style 2024-09-05 22:40:43 +10:00
psychedelicious
456a6cdb8d feat(ui): mask layers choose own opacity 2024-09-05 22:40:43 +10:00
psychedelicious
62db00f5b2 feat(ui): mask fill patterns 2024-09-05 22:40:43 +10:00
psychedelicious
c6a15bfb1a build(ui): add vite types to tsconfig 2024-09-05 22:40:43 +10:00
psychedelicious
de9c72f7d5 fix(ui): do not smooth pixel data when using eyeDropper 2024-09-05 22:40:42 +10:00
psychedelicious
29cb2a30ad tidy(ui): tool components & translations 2024-09-05 22:40:42 +10:00
psychedelicious
9971ece2e5 feat(ui): rough out eyedropper tool
It's a bit slow bc we are converting the stage to canvas on every mouse move. Also need to improve the visual but it works.
2024-09-05 22:40:42 +10:00
psychedelicious
4e7ae3e120 fix(ui): ip adapters work 2024-09-05 22:40:42 +10:00
psychedelicious
7b799ee51c feat(ui): rename layers 2024-09-05 22:40:42 +10:00
psychedelicious
e948d8454a feat(ui): revise entity menus 2024-09-05 22:40:42 +10:00
psychedelicious
eaf6fe571d feat(ui): split control layers from raster layers for UI and internal state, same rendering as raster layers 2024-09-05 22:40:42 +10:00
psychedelicious
13c607470d feat(ui): implement cache for image rasterization, rip out some old controladapters code 2024-09-05 22:40:42 +10:00
psychedelicious
582e8be8b9 feat(ui, app): use layer as control (wip) 2024-09-05 22:40:42 +10:00
psychedelicious
3239ba1a1c feat(ui): add contextmenu for canvas entities 2024-09-05 22:40:42 +10:00
psychedelicious
ae5d1e035a feat(ui): more better logging & naming 2024-09-05 22:40:42 +10:00
psychedelicious
d3e245fd78 feat(ui): better logging w/ path 2024-09-05 22:40:42 +10:00
psychedelicious
aea7efb031 feat(ui): always show marks on canvas scale slider 2024-09-05 22:40:42 +10:00
psychedelicious
3e61f9b405 fix(ui): do not import button from chakra 2024-09-05 22:40:42 +10:00
psychedelicious
840707606f fix(ui): scaled bbox preview 2024-09-05 22:40:42 +10:00
psychedelicious
68b97193cb feat(ui): tidy up atoms 2024-09-05 22:40:42 +10:00
psychedelicious
00d73598d2 feat(ui): convert all my pubsubs to atoms
its the same but better
2024-09-05 22:40:42 +10:00
psychedelicious
f9726dc904 feat(ui): add trnalsation 2024-09-05 22:40:42 +10:00
psychedelicious
25e3fa5990 fix(ui): give up on thumbnail loading, causes flash during transformer 2024-09-05 22:40:42 +10:00
psychedelicious
b69d91f0ec fix(ui): depth anything v2 2024-09-05 22:40:42 +10:00
psychedelicious
6a1e34a030 tidy(ui): remove unused code, comments 2024-09-05 22:40:42 +10:00
psychedelicious
2dde7d8925 fix(ui): staging area works 2024-09-05 22:40:42 +10:00
psychedelicious
1d284609f9 feat(nodes): temp disable canvas output crop 2024-09-05 22:40:42 +10:00
psychedelicious
3f6873f0d3 fix(ui): max scale 1 when reset view 2024-09-05 22:40:42 +10:00
psychedelicious
ae78e90d53 feat(ui): better scale changer component, reset view functionality 2024-09-05 22:40:42 +10:00
psychedelicious
7cca0a239b fix(ui): img2img 2024-09-05 22:40:42 +10:00
psychedelicious
ffd6164f06 feat(ui): add manual scale controls 2024-09-05 22:40:42 +10:00
psychedelicious
a3a370625b fix(ui): do not await clearBuffer 2024-09-05 22:40:42 +10:00
psychedelicious
ae3064fc67 feat(ui): dnd image into layer 2024-09-05 22:40:42 +10:00
psychedelicious
71c03b3b8b fix(ui): do not await commitBuffer 2024-09-05 22:40:42 +10:00
psychedelicious
70b58197f3 fix(ui): properly destroy entities in manager cleanup 2024-09-05 22:40:42 +10:00
psychedelicious
6600b4790b tidy(ui): clearer component names for regional guidance 2024-09-05 22:40:42 +10:00
psychedelicious
b0854dcb13 tidy(ui): clearer component names for ip adapter 2024-09-05 22:40:42 +10:00
psychedelicious
7f613eaa91 tidy(ui): clearer component names for inpaint mask 2024-09-05 22:40:42 +10:00
psychedelicious
56f731dce3 tidy(ui): clearer component names for control adapters 2024-09-05 22:40:42 +10:00
psychedelicious
4dea5d0cb0 feat(ui): simplify canvas list item headers 2024-09-05 22:40:42 +10:00
psychedelicious
421c82b534 fix(ui): ip adapter list item 2024-09-05 22:40:42 +10:00
psychedelicious
b5c86bf0dd tidy(ui): clean up unused logic 2024-09-05 22:40:42 +10:00
psychedelicious
ec01b1be31 feat(ui): clean up state, add mutex for image loading, add thumbnail loading 2024-09-05 22:40:42 +10:00
psychedelicious
1405fe8e2a chore(ui): add async-mutex dep 2024-09-05 22:40:42 +10:00
psychedelicious
51c40edf0a feat(ui): txt2img, img2img, inpaint & outpaint working 2024-09-05 22:40:42 +10:00
psychedelicious
3a61f3992a feat(ui): no padding on transformer outlines 2024-09-05 22:40:42 +10:00
psychedelicious
c31f36ab17 feat(ui): restore object count to layer titles 2024-09-05 22:40:42 +10:00
psychedelicious
270bb3c95a tidy(ui): "useIsEntitySelected" -> "useEntityIsSelected" 2024-09-05 22:40:42 +10:00
psychedelicious
18e5e62466 tidy(ui): move transformer statics into class 2024-09-05 22:40:42 +10:00
psychedelicious
b808df2aa0 tidy(ui): massive cleanup
- create a context for entity identifiers, massively simplifying UI for each entity int he list
- consolidate common redux actions
- remove now-unused code
2024-09-05 22:40:42 +10:00
psychedelicious
63d0ea6757 perf(ui): do not add duplicate points to lines 2024-09-05 22:40:42 +10:00
psychedelicious
dd49b6fa81 feat(ui): up line tension to 0.3 2024-09-05 22:40:42 +10:00
psychedelicious
0d3764a44b perf(ui): disable stroke, perfect draw on compositing rect 2024-09-05 22:40:42 +10:00
psychedelicious
626a404c44 tidy(ui): remove unused code, initial image 2024-09-05 22:40:42 +10:00
psychedelicious
b4e0581d2d tidy(ui): remove unused state & actions 2024-09-05 22:40:42 +10:00
psychedelicious
3372887352 feat(ui): region mask rendering 2024-09-05 22:40:42 +10:00
psychedelicious
66f15a8629 feat(ui): esc cancels drawing buffer
maybe this is not wanted? we'll see
2024-09-05 22:40:42 +10:00
psychedelicious
be4e21068d fix(ui): render transformer over objects, fix issue w/ inpaint rect color 2024-09-05 22:40:42 +10:00
psychedelicious
41f200ef7d fix(ui): brush preview fill for inpaint/region 2024-09-05 22:40:42 +10:00
psychedelicious
53fa36d71e fix(ui): no objects rendered until vis toggled 2024-09-05 22:40:42 +10:00
psychedelicious
9f661dc093 feat(ui): inpaint mask transform 2024-09-05 22:40:42 +10:00
psychedelicious
9b51dfb13a fix(ui): layer accidental early set isFirstRender=false 2024-09-05 22:40:42 +10:00
psychedelicious
39171eed76 fix(ui): inpaint mask rendering 2024-09-05 22:40:42 +10:00
psychedelicious
bab8432119 feat(ui): wip inpaint mask uses new API 2024-09-05 22:40:42 +10:00
psychedelicious
731efe7290 feat(ui): move updatePosition to transformer 2024-09-05 22:40:42 +10:00
psychedelicious
bb8815e5b3 feat(ui): move resetScale to transformer 2024-09-05 22:40:42 +10:00
psychedelicious
300e2045b1 tidy(ui): more imperative naming 2024-09-05 22:40:42 +10:00
psychedelicious
4514334bfc tidy(ui): use imperative names for setters in stateapi 2024-09-05 22:40:42 +10:00
psychedelicious
0a9c033d75 fix(ui): commit drawing buffer on tool change, fixing bbox not calculating 2024-09-05 22:40:42 +10:00
psychedelicious
060c14964b fix(ui): sync transformer when requesting bbox calc 2024-09-05 22:40:42 +10:00
psychedelicious
345b06bf19 tidy(ui): rename union CanvasEntity -> CanvasEntityState 2024-09-05 22:40:42 +10:00
psychedelicious
0e7c03c0d0 fix(ui): request rect calc immediately on transform, hiding rect 2024-09-05 22:40:42 +10:00
psychedelicious
1226855fc5 feat(ui): move bbox calculation to transformer 2024-09-05 22:40:42 +10:00
psychedelicious
732cb629b6 feat(ui): use set for transformer subscriptions 2024-09-05 22:40:42 +10:00
psychedelicious
7dbad20416 tidy(ui): clean up worker tasks when complete 2024-09-05 22:40:42 +10:00
psychedelicious
dcd2f78f64 tidy(ui): remove unused code in CanvasTool 2024-09-05 22:40:42 +10:00
psychedelicious
ad1623c385 feat(ui): use pubsub for isTransforming on manager 2024-09-05 22:40:42 +10:00
psychedelicious
8a5a5816f7 docs(ui): update transformer docstrings 2024-09-05 22:40:42 +10:00
psychedelicious
8f5bb55471 feat(ui): revised event pubsub, transformer logic split out 2024-09-05 22:40:42 +10:00
psychedelicious
30624f63c1 feat(ui): add simple pubsub 2024-09-05 22:40:42 +10:00
psychedelicious
5a787faca8 feat(ui): document & clean up object renderer 2024-09-05 22:40:42 +10:00
psychedelicious
f42efc9b26 feat(ui): split out object renderer 2024-09-05 22:40:42 +10:00
psychedelicious
5c531dc920 fix(ui): unable to hold shit while transforming to retain ratio 2024-09-05 22:40:42 +10:00
psychedelicious
85b96e3802 tidy(ui): rename canvas stuff 2024-09-05 22:40:42 +10:00
psychedelicious
ada3ab14fb tidy(ui): consolidate getLoggingContext builders 2024-09-05 22:40:42 +10:00
psychedelicious
1cbd19b7cd fix(ui): align all tools to 1px grid
- Offset brush tool by 0.5px when width is odd, ensuring each stroke edge is exactly on a pixel boundary
- Round the rect tool also
2024-09-05 22:40:42 +10:00
psychedelicious
bbbb22898d feat(ui): disable image smoothing on layers 2024-09-05 22:40:42 +10:00
psychedelicious
e68a670c36 fix(ui): round position when rasterizing layer 2024-09-05 22:40:42 +10:00
psychedelicious
09554c18dd feat(ui): continue modularizing transform 2024-09-05 22:40:42 +10:00
psychedelicious
d5e0a5f3de feat(ui): fix a few things that didn't unsubscribe correctly, add helper to manage subscriptions 2024-09-05 22:40:42 +10:00
psychedelicious
7bdec13226 feat(ui): merge bbox outline into transformer 2024-09-05 22:40:42 +10:00
psychedelicious
53e0b9bd14 fix(ui): update parent's pos not transformers 2024-09-05 22:40:42 +10:00
psychedelicious
f92a926ab8 feat(ui): merge interaction rect into transformer class 2024-09-05 22:40:42 +10:00
psychedelicious
b472535527 feat(ui): prepare staging area 2024-09-05 22:40:42 +10:00
psychedelicious
e7a9648a91 feat(ui): typing for logging context 2024-09-05 22:40:42 +10:00
psychedelicious
418786f82f feat(ui): remove inheritance of CanvasObject
JS is terrible
2024-09-05 22:40:42 +10:00
psychedelicious
a1ada23930 feat(ui): split & document transformer logic, iterate on class structures 2024-09-05 22:40:42 +10:00
psychedelicious
5d367cc0e1 feat(ui): rotation snap to nearest 45deg when holding shift 2024-09-05 22:40:42 +10:00
psychedelicious
332dc8b13c feat(ui): expose subscribe method for nanostores 2024-09-05 22:40:42 +10:00
psychedelicious
a8fa2c5ec5 tidy(ui): remove layer scaling reducers 2024-09-05 22:40:42 +10:00
psychedelicious
237af4007a fix(ui): pixel-perfect transforms 2024-09-05 22:40:42 +10:00
psychedelicious
8df59769a8 fix(ui): layer visibility toggle 2024-09-05 22:40:42 +10:00
psychedelicious
7ffa0e4345 fix(nodes): fix canvas mask erode
it wasn't eroding enough and caused incorrect transparency in result images
2024-09-05 22:40:42 +10:00
psychedelicious
b4483fde8c fix(ui): do not reset layer on first render 2024-09-05 22:40:42 +10:00
psychedelicious
8bb984f13a feat(ui): revised logging and naming setup, fix staging area 2024-09-05 22:40:42 +10:00
psychedelicious
7d9a8908c5 feat(ui): add repr methods to layer and object classes 2024-09-05 22:40:42 +10:00
psychedelicious
d6ca58992d feat(ui): use nanoid(10) instead of uuidv4 for canvas
Shorter ids makes it much more readable
2024-09-05 22:40:42 +10:00
psychedelicious
6d9817742f build(ui): add nanoid as explicit dep 2024-09-05 22:40:42 +10:00
psychedelicious
a2b2d83841 fix(ui): move CanvasImage's konva image to correct object 2024-09-05 22:40:42 +10:00
psychedelicious
daaa2f8d8e fix(ui): prevent flash when applying transform 2024-09-05 22:40:42 +10:00
psychedelicious
1c4099a53c build(ui): add eslint rules for async stuff 2024-09-05 22:40:42 +10:00
psychedelicious
09ad29a765 feat(ui): trying to fix flicker after transform 2024-09-05 22:40:42 +10:00
psychedelicious
94a66b7850 feat(ui): transform cleanup 2024-09-05 22:40:42 +10:00
psychedelicious
5cb4bc0902 feat(ui): fix transform when rotated 2024-09-05 22:40:42 +10:00
psychedelicious
6752a47d2b fix(ui): use pixel bbox when image is in layer 2024-09-05 22:40:42 +10:00
psychedelicious
f883f80409 fix(ui): transforming when axes flipped 2024-09-05 22:40:42 +10:00
psychedelicious
b5b4c20b4e feat(ui): hallelujah (???) 2024-09-05 22:40:42 +10:00
psychedelicious
25c270931c feat(ui): add debug button 2024-09-05 22:40:42 +10:00
psychedelicious
5a93c4efcb fix(ui): transformer padding 2024-09-05 22:40:42 +10:00
psychedelicious
1fbf2fad16 feat(ui): wip transform mode 2 2024-09-05 22:40:42 +10:00
psychedelicious
f9aa925a06 feat(ui): wip transform mode 2024-09-05 22:40:42 +10:00
psychedelicious
7b7c1c5af8 feat(ui): wip transform mode 2024-09-05 22:40:42 +10:00
psychedelicious
4024f83f73 fix(ui): dnd to canvas broke 2024-09-05 22:40:42 +10:00
psychedelicious
aae6e62031 fix(ui): conflicts after rebasing 2024-09-05 22:40:42 +10:00
psychedelicious
bf355fa602 fix(ui): imageDropped listener 2024-09-05 22:40:42 +10:00
psychedelicious
cd1d576ff1 wip 2024-09-05 22:40:42 +10:00
psychedelicious
0bc72149fe fix(ui): transform tool seems to be working 2024-09-05 22:40:42 +10:00
psychedelicious
75c0f03582 fix(ui): move tool fixes, add transform tool 2024-09-05 22:40:42 +10:00
psychedelicious
1b7288f437 feat(ui): move tool now only moves 2024-09-05 22:40:42 +10:00
psychedelicious
65e51634e3 feat(ui): layer bbox calc in worker 2024-09-05 22:40:42 +10:00
psychedelicious
638835f6f0 feat(ui): tweaked entity & group selection styles 2024-09-05 22:40:42 +10:00
psychedelicious
27e6d8372a feat(ui): canvas entity list headers 2024-09-05 22:40:42 +10:00
psychedelicious
f3b3121edc tidy(ui): CanvasRegion 2024-09-05 22:40:42 +10:00
psychedelicious
da32803aef tidy(ui): CanvasRect 2024-09-05 22:40:42 +10:00
psychedelicious
ef69a12532 tidy(ui): CanvasLayer 2024-09-05 22:40:42 +10:00
psychedelicious
b3d82838c6 tidy(ui): CanvasInpaintMask 2024-09-05 22:40:42 +10:00
psychedelicious
b66eeafa9a tidy(ui): CanvasInitialImage 2024-09-05 22:40:42 +10:00
psychedelicious
ae8a0b7c04 tidy(ui): CanvasImage 2024-09-05 22:40:42 +10:00
psychedelicious
655c0981eb tidy(ui): CanvasEraserLine 2024-09-05 22:40:42 +10:00
psychedelicious
d2d747869f tidy(ui): CanvasControlAdapter 2024-09-05 22:40:42 +10:00
psychedelicious
998bdadc8d tidy(ui): CanvasBrushLine 2024-09-05 22:40:42 +10:00
psychedelicious
71dcc58e33 tidy(ui): CanvasBbox 2024-09-05 22:40:42 +10:00
psychedelicious
7485d30858 tidy(ui): CanvasBackground 2024-09-05 22:40:42 +10:00
psychedelicious
443d7b1176 tidy(ui): update canvas classes, organise location of konva nodes 2024-09-05 22:40:42 +10:00
psychedelicious
6b494161ee feat(ui): add names to all konva objects
Makes troubleshooting much simpler
2024-09-05 22:40:42 +10:00
psychedelicious
90d3c8b630 fix(ui): do not await creating new canvas image
If you await this, it causes a race condition where multiple images are created.
2024-09-05 22:40:42 +10:00
psychedelicious
7016a15566 feat(ui): use position and dimensions instead of separate x,y,width,height attrs 2024-09-05 22:40:42 +10:00
psychedelicious
2bbc3138c6 fix(ui): remove weird rtkq hook wrapper
I do not understand why I did that initially but it doesn't work with TS.
2024-09-05 22:40:42 +10:00
psychedelicious
b80ffd3f02 feat(ui): rename types size and position to dimensions and coordinate 2024-09-05 22:40:42 +10:00
psychedelicious
d26e7095c5 tidy(ui): hide layer settings by default 2024-09-05 22:40:42 +10:00
psychedelicious
82a496f6f4 fix(ui): layer rendering when starting as disabled 2024-09-05 22:40:42 +10:00
psychedelicious
2a2667a20d feat(invocation): reduce canvas v2 mask & crop mask dilation 2024-09-05 22:40:42 +10:00
psychedelicious
c018a031a2 feat(ui): de-jank staging area and progress images 2024-09-05 22:40:42 +10:00
psychedelicious
f193200a88 feat(ui): update staging handling to work w/ cropped mask 2024-09-05 22:40:42 +10:00
psychedelicious
11c3eeecdc chore(ui): typegen 2024-09-05 22:40:41 +10:00
psychedelicious
979132d404 feat(app): update CanvasV2MaskAndCropInvocation 2024-09-05 22:40:41 +10:00
psychedelicious
e55e866baf feat(ui): use new canvas output node 2024-09-05 22:40:41 +10:00
psychedelicious
77b1315641 chore(ui): typegen 2024-09-05 22:40:41 +10:00
psychedelicious
3e2902cb1b feat(app): add CanvasV2MaskAndCropInvocation & CanvasV2MaskAndCropOutput
This handles some masking and cropping that the canvas needs.
2024-09-05 22:40:11 +10:00
psychedelicious
a48984c969 fix(ui): restore nodes output tracking 2024-09-05 22:40:11 +10:00
psychedelicious
a640fa7d9b feat(ui): rip out document size
barely knew ye
2024-09-05 22:40:11 +10:00
psychedelicious
0ed1e28084 feat(ui): convert initial image to layer when starting canvas session 2024-09-05 22:40:11 +10:00
psychedelicious
79789bbd20 fix(ui): fix layer transparency calculation 2024-09-05 22:40:11 +10:00
psychedelicious
4554a425d3 fix(ui): reset initial image when resetting canvas 2024-09-05 22:40:11 +10:00
psychedelicious
d153e5958e fix(ui): reset node executions states when loading workflow 2024-09-05 22:40:11 +10:00
psychedelicious
b4a7865cbb fix(ui): entity display list 2024-09-05 22:40:11 +10:00
psychedelicious
89baf9aa49 feat(ui): img2img working 2024-09-05 22:40:11 +10:00
psychedelicious
1ee37908f2 feat(ui): rough out img2img on canvas 2024-09-05 22:40:11 +10:00
psychedelicious
7fecf74368 UNDO ME WIP 2024-09-05 22:40:11 +10:00
psychedelicious
770e9a92d6 feat(ui): log invocation source id on socket event 2024-09-05 22:40:11 +10:00
psychedelicious
37658c59b7 feat(ui): restore document size overlay renderer 2024-09-05 22:40:11 +10:00
psychedelicious
70eadc52f1 feat(ui): make documnet size a rect 2024-09-05 22:40:11 +10:00
psychedelicious
eacf30a55e refactor(ui): remove modular imagesize components
This is no longer necessary with canvas v2 and added a ton of extraneous redux actions when changing the image size. Also renamed to document size
2024-09-05 22:40:11 +10:00
psychedelicious
33cf40b7a4 feat(ui): initialState is for generation mode 2024-09-05 22:40:11 +10:00
psychedelicious
19f8f0677e feat(ui): split out canvas entity list component 2024-09-05 22:40:11 +10:00
psychedelicious
f906fca4fc feat(ui): hide bbox button when no canvas session active 2024-09-05 22:40:11 +10:00
psychedelicious
2417a97b56 tidy(ui): remove unused naming objects/utils
The canvas manager means we don't need to worry about konva node names as we never directly select konva nodes.
2024-09-05 22:40:11 +10:00
psychedelicious
3b0438cc69 feat(ui): split up tool chooser buttons
Prep for distinct toolbars for generation vs canvas modes
2024-09-05 22:40:11 +10:00
psychedelicious
aa4fe73b56 feat(ui): add useAssertSingleton util hook
This simple hook asserts that it is only ever called once. Particularly useful for things like hotkeys hooks.
2024-09-05 22:40:11 +10:00
psychedelicious
58064d835e feat(ui): "stagingArea" -> "session" 2024-09-05 22:40:11 +10:00
psychedelicious
f7ae63e758 feat(ui): add reset button to canvas 2024-09-05 22:40:11 +10:00
psychedelicious
6c8d6175aa feat(ui): add snapToRect util 2024-09-05 22:40:11 +10:00
psychedelicious
348e4b1d38 fix(ui): fiddle with control adapter filters
some jank still
2024-09-05 22:40:11 +10:00
psychedelicious
822543c202 feat(ui): temp disable doc size overlay 2024-09-05 22:40:11 +10:00
psychedelicious
c3de34e7dc feat(ui): no animation on layer selection
Felt sluggish
2024-09-05 22:40:11 +10:00
psychedelicious
3ae61f2758 feat(ui): use canvas as source for control images (wip) 2024-09-05 22:40:11 +10:00
psychedelicious
19e6cf3311 fix(ui): control adapter translate & scale 2024-09-05 22:40:11 +10:00
psychedelicious
6f400846b8 tidy(ui): removed unused state related to non-buffered drawing 2024-09-05 22:40:10 +10:00
psychedelicious
c9f9b699e9 feat(ui): control adapter image rendering 2024-09-05 22:40:10 +10:00
psychedelicious
63bf4bd963 fix(ui): do not floor bbox calc, it cuts off the last pixels 2024-09-05 22:40:10 +10:00
psychedelicious
d03b3d4eb2 feat(ui): fix issue where creating line needs 2 points 2024-09-05 22:40:10 +10:00
psychedelicious
6e6852a604 fix(ui): edge cases when holding shift and drawing lines 2024-09-05 22:40:10 +10:00
psychedelicious
bb2b526b82 fix(ui): set buffered rect color to full alpha 2024-09-05 22:40:10 +10:00
psychedelicious
d6b6dae63f fix(ui): handle mouseup correctly 2024-09-05 22:40:10 +10:00
psychedelicious
fb02e72462 feat(ui): buffered rect drawing 2024-09-05 22:40:10 +10:00
psychedelicious
bc3568035b fix(ui): buffered drawing edge cases 2024-09-05 22:40:10 +10:00
psychedelicious
ea13ab4c9c perf(ui): do not use stage.find 2024-09-05 22:40:10 +10:00
psychedelicious
f9b2f363c7 perf(ui): object groups do not listen 2024-09-05 22:40:10 +10:00
psychedelicious
dba206ea98 perf(ui): buffered drawing (wip) 2024-09-05 22:40:10 +10:00
psychedelicious
6ca5a71a51 tidy(ui): organise files 2024-09-05 22:40:10 +10:00
psychedelicious
6e7022d006 tidy(ui): organise files 2024-09-05 22:40:10 +10:00
psychedelicious
8e2ca3b1a4 tidy(ui): organise files 2024-09-05 22:40:10 +10:00
psychedelicious
9d32629d5d fix(ui): background rendering 2024-09-05 22:40:10 +10:00
psychedelicious
0755734347 pkg(ui): remove unused deps react-konva & use-image 2024-09-05 22:40:10 +10:00
psychedelicious
62ffefe9d1 feat(ui): organize konva state and files 2024-09-05 22:40:10 +10:00
psychedelicious
a4e570e4a7 fix(ui): merge conflicts in image deletion listener 2024-09-05 22:40:10 +10:00
psychedelicious
d569d10e46 fix(ui): region rendering 2024-09-05 22:40:10 +10:00
psychedelicious
cb622df45e fix(ui): inpaint mask rendering 2024-09-05 22:40:10 +10:00
psychedelicious
6299214325 fix(ui): staging area rendering 2024-09-05 22:40:10 +10:00
psychedelicious
220ae6bef8 fix(ui): stale selected entity 2024-09-05 22:40:10 +10:00
psychedelicious
fd923f7e30 fix(ui): staging area image offset 2024-09-05 22:40:10 +10:00
psychedelicious
d62d63acfc feat(ui): tweak layer ui component 2024-09-05 22:40:10 +10:00
psychedelicious
3a2af003fe fix(ui): resetting layer resets position 2024-09-05 22:40:10 +10:00
psychedelicious
00d68ac460 feat(ui): updated layer list component styling 2024-09-05 22:40:10 +10:00
psychedelicious
fd1df7e8d7 feat(ui): transformable layers 2024-09-05 22:40:10 +10:00
psychedelicious
8b4e2ce1b2 feat(ui): move tool icon is pointer like in other apps 2024-09-05 22:40:10 +10:00
psychedelicious
8a0936f3dc feat(ui): do not floor cursor position 2024-09-05 22:40:10 +10:00
psychedelicious
9b5962b4ed feat(ui): disable gallery hotkeys while staging 2024-09-05 22:40:10 +10:00
psychedelicious
6c83153076 feat(ui): revised canvas progress & staging image handling 2024-09-05 22:40:10 +10:00
psychedelicious
9fa65e59b4 feat(ui): show queue item origin in queue list 2024-09-05 22:40:10 +10:00
psychedelicious
491ae852af chore(ui): typegen 2024-09-05 22:40:10 +10:00
psychedelicious
0f698f25bd feat(app): add origin to session queue
The origin is an optional field indicating the queue item's origin. For example, "canvas" when the queue item originated from the canvas or "workflows" when the queue item originated from the workflows tab. If omitted, we assume the queue item originated from the API directly.

- Add migration to add the nullable column to the `session_queue` table.
- Update relevant event payloads with the new field.
- Add `cancel_by_origin` method to `session_queue` service and corresponding route. This is required for the canvas to bail out early when staging images.
- Add `origin` to both `SessionQueueItem` and `Batch` - it needs to be provided initially via the batch and then passed onto the queue item.
-
2024-09-05 22:40:10 +10:00
psychedelicious
adde7138b3 fix(ui): denoise start on outpainting 2024-09-05 22:40:10 +10:00
psychedelicious
f808ff2830 feat(ui): add redux events for queue cleared & batch enqueued socket events 2024-09-05 22:40:10 +10:00
psychedelicious
0e1986e795 feat(ui): canvas staging area works 2024-09-05 22:40:10 +10:00
psychedelicious
d34a2e2160 feat(ui): switch to view tool when staging 2024-09-05 22:40:10 +10:00
psychedelicious
2752d45d2d tidy(ui): disable preview images on every enqueue 2024-09-05 22:40:10 +10:00
psychedelicious
f9b84555d2 feat(ui): rough out save staging image 2024-09-05 22:40:10 +10:00
psychedelicious
9a723b189f feat(ui): staging area image visibility toggle 2024-09-05 22:40:10 +10:00
psychedelicious
8c7ce9865a fix(ui): batch building after removing canvas files 2024-09-05 22:40:10 +10:00
psychedelicious
ee2f162a8e feat(ui): make Graph class's getMetadataNode public 2024-09-05 22:40:10 +10:00
psychedelicious
2770233592 tidy(ui): remove old canvas graphs 2024-09-05 22:40:10 +10:00
psychedelicious
46f31fdd32 fix(ui): do not select already-selected entity 2024-09-05 22:40:10 +10:00
psychedelicious
df1436cfac tidy(ui): naming things 2024-09-05 22:40:10 +10:00
psychedelicious
aced3754f3 tidy(ui): file organisation 2024-09-05 22:40:10 +10:00
psychedelicious
723a04029f fix(ui): reset cursor pos when fitting document 2024-09-05 22:40:10 +10:00
psychedelicious
6f9fe23a32 feat(ui): staging area works more better 2024-09-05 22:40:10 +10:00
psychedelicious
9dd3d18a7d feat(ui): staging area barely works 2024-09-05 22:40:10 +10:00
psychedelicious
99e7055469 feat(ui): consolidate konva API 2024-09-05 22:40:10 +10:00
psychedelicious
0c14bc5fed feat(ui): consolidate konva API 2024-09-05 22:40:10 +10:00
psychedelicious
aae98b675b feat(ui): staging area (rendering wip) 2024-09-05 22:40:10 +10:00
psychedelicious
e795026210 tidy(ui): type "Dimensions" -> "Size" 2024-09-05 22:40:10 +10:00
psychedelicious
120eaa5e82 feat(ui): add updateNode to Graph 2024-09-05 22:40:10 +10:00
psychedelicious
6561fffe30 feat(ui): sdxl graphs 2024-09-05 22:40:10 +10:00
psychedelicious
15e1046f99 feat(ui): sd1 outpaint graph 2024-09-05 22:40:10 +10:00
psychedelicious
930868466d tests(ui): add missing tests for Graph class 2024-09-05 22:40:10 +10:00
psychedelicious
813904d615 feat(ui): add Graph.getid() util 2024-09-05 22:40:10 +10:00
psychedelicious
1bb06ba90b feat(ui): outpaint graph, organize builder a bit 2024-09-05 22:40:10 +10:00
psychedelicious
9d1b0dfcda feat(ui): inpaint sd1 graph 2024-09-05 22:40:10 +10:00
psychedelicious
69b5637fcf feat(ui): temp disable image caching while testing 2024-09-05 22:40:10 +10:00
psychedelicious
eb0cc4fc9d feat(ui): txt2img & img2img graphs 2024-09-05 22:40:10 +10:00
psychedelicious
f9d9684237 feat(ui): minor change to canvas bbox state type 2024-09-05 22:40:10 +10:00
psychedelicious
7538a2b5ff feat(ui): simplified konva node to blob/imagedata utils 2024-09-05 22:40:10 +10:00
psychedelicious
c274d4bc43 feat(ui): node manager getter/setter 2024-09-05 22:40:10 +10:00
psychedelicious
8b0a06353a feat(ui): generation mode calculation, fudged graphs 2024-09-05 22:40:10 +10:00
psychedelicious
9d10ec763b feat(ui): add utils for getting images from canvas 2024-09-05 22:40:10 +10:00
psychedelicious
5c20b35bad feat(ui): even more simplified API - lean on the konva node manager to abstract imperative state API & rendering 2024-09-05 22:40:10 +10:00
psychedelicious
1df3197ded feat(ui): revised docstrings for renderers & simplified api 2024-09-05 22:40:10 +10:00
psychedelicious
5b6bae5113 feat(ui): inpaint mask UI components 2024-09-05 22:40:10 +10:00
psychedelicious
cdf5a61641 feat(ui): inpaint mask rendering (wip) 2024-09-05 22:40:10 +10:00
psychedelicious
95e73d8e1e fix(ui): models loaded handler 2024-09-05 22:40:10 +10:00
psychedelicious
6120de332e feat(ui): internal state for inpaint mask 2024-09-05 22:40:10 +10:00
psychedelicious
815b58d3c5 refactor(ui): divvy up canvas state a bit 2024-09-05 22:40:10 +10:00
psychedelicious
71befa4ce0 feat(ui): get region and base layer canvas to blob logic working 2024-09-05 22:40:10 +10:00
psychedelicious
10d3f3c2bf refactor(ui): node manager handles more tedious annoying stuff 2024-09-05 22:40:10 +10:00
psychedelicious
02c3c24f95 feat(ui): use node manager for addRegions 2024-09-05 22:40:10 +10:00
psychedelicious
47075acf39 feat(ui): persist bbox 2024-09-05 22:40:10 +10:00
psychedelicious
5c4438ed1b fix(ui): fix generation graphs 2024-09-05 22:40:10 +10:00
psychedelicious
78c08222f4 feat(ui): add toggle for clipToBbox 2024-09-05 22:40:10 +10:00
psychedelicious
edd2bd2184 feat(ui): rename konva node manager 2024-09-05 22:40:10 +10:00
psychedelicious
0fee32c5b3 refactor(ui): create classes to abstract mgmt of konva nodes 2024-09-05 22:40:10 +10:00
psychedelicious
1d5151839c tidy(ui): organise renderers 2024-09-05 22:40:10 +10:00
psychedelicious
a916cb7efb refactor(ui): create entity to konva node map abstraction (wip)
Instead of chaining konva `find` and `findOne` methods, all konva nodes are added to a mapping object. Finding and manipulating them is much simpler.

Done for regions and layers, wip for control adapters.
2024-09-05 22:40:10 +10:00
psychedelicious
88c95f6d8a perf(ui): fix lag w/ region rendering
Needed to memoize these selectors
2024-09-05 22:40:10 +10:00
psychedelicious
f7d71c3cd0 feat(ui): move canvas fill color picker to right 2024-09-05 22:40:10 +10:00
psychedelicious
20193028c3 refactor(ui): remove unused ellipse & polygon objects 2024-09-05 22:40:10 +10:00
psychedelicious
ae8ee6709c fix(ui): incorrect rect/brush/eraser positions 2024-09-05 22:40:10 +10:00
psychedelicious
4e298dae62 refactor(ui): enable global debugging flag 2024-09-05 22:40:10 +10:00
psychedelicious
cb72467d1f refactor(ui): disable the preview renderer for now 2024-09-05 22:40:10 +10:00
psychedelicious
d1596957c0 tweak(ui): canvas editor layout 2024-09-05 22:40:10 +10:00
psychedelicious
8bdec7cfba perf(ui): memoize layeractionsmenu valid actions 2024-09-05 22:40:10 +10:00
psychedelicious
66c4bf260f refactor(ui): decouple konva renderer from react
Subscribe to redux store directly, skipping all the react overhead.

With react in dev mode, a typical frame while using the brush tool on almost-empty canvas is reduced from ~7.5ms to ~3.5ms. All things considered, this still feels slow, but it's a massive improvement.
2024-09-05 22:40:10 +10:00
psychedelicious
1764df7446 feat(ui): clip lines to bbox 2024-09-05 22:40:10 +10:00
psychedelicious
ebf83518e3 fix(ui): document fit positioning 2024-09-05 22:40:10 +10:00
psychedelicious
4e6ff6033f feat(ui): document bounds overlay 2024-09-05 22:40:10 +10:00
psychedelicious
fa0b8f34f0 tidy(ui): background layer 2024-09-05 22:40:10 +10:00
psychedelicious
d7ad0e082e refactor(ui): use "entity" instead of "data" for canvas 2024-09-05 22:40:10 +10:00
psychedelicious
b5df668753 feat(ui): brush size border radius = 1 2024-09-05 22:40:10 +10:00
psychedelicious
2ff9803db0 fix(ui): canvas HUD doesn't interrupt tool 2024-09-05 22:40:10 +10:00
psychedelicious
5f6c155e4a refactor(ui): split up canvas entity renderers, temp disable preview 2024-09-05 22:40:10 +10:00
psychedelicious
8531f1b759 fix(ui): delete all layers button 2024-09-05 22:40:10 +10:00
psychedelicious
e9b1f9a87b fix(ui): ignore keyboard shortcuts in input/textarea elements 2024-09-05 22:40:10 +10:00
psychedelicious
5146150509 fix(ui): canvas entity ids getting clobbered 2024-09-05 22:40:10 +10:00
psychedelicious
e221c30249 fix(ui): move lora followup fixes 2024-09-05 22:40:10 +10:00
psychedelicious
9890efdb2e chore(ui): lint 2024-09-05 22:40:10 +10:00
psychedelicious
ac32d4ca8a refactor(ui): move loras to canvas slice 2024-09-05 22:40:10 +10:00
psychedelicious
88f0c0bf23 fix(ui): layer is selected when added 2024-09-05 22:40:10 +10:00
psychedelicious
198be69a7f feat(ui): r to center & fit stage on document 2024-09-05 22:40:10 +10:00
psychedelicious
ec1bb0e389 feat(ui): better HUD 2024-09-05 22:40:10 +10:00
psychedelicious
49aa7325cb fix(ui): always use current brush width when making straight lines 2024-09-05 22:40:10 +10:00
psychedelicious
5e292a7423 feat(ui): hold shift w/ brush to draw straight line 2024-09-05 22:40:10 +10:00
psychedelicious
73593a88bb fix(ui): update bg on canvas resize 2024-09-05 22:40:10 +10:00
psychedelicious
84e6b197a1 refactor(ui): better hud 2024-09-05 22:40:10 +10:00
psychedelicious
8aae372446 refactor(ui): scaled tool preview border 2024-09-05 22:40:10 +10:00
psychedelicious
6657d501db refactor(ui): port remaining canvasV1 rendering logic to V2, remove old code 2024-09-05 22:40:10 +10:00
psychedelicious
354830144a refactor(ui): fix more types 2024-09-05 22:40:10 +10:00
psychedelicious
2aa105379b refactor(ui): metadata recall (wip)
just enough let the app run
2024-09-05 22:40:10 +10:00
psychedelicious
ce1d6a1ede refactor(ui): undo/redo button temp fix 2024-09-05 22:40:10 +10:00
psychedelicious
a20bf91f5d refactor(ui): fix renderer stuff 2024-09-05 22:40:10 +10:00
psychedelicious
01215bbb99 refactor(ui): fix misc types 2024-09-05 22:40:10 +10:00
psychedelicious
75b40b95df refactor(ui): fix gallery stuff 2024-09-05 22:40:10 +10:00
psychedelicious
19bbbf49d9 refactor(ui): fix delete image stuff 2024-09-05 22:40:10 +10:00
psychedelicious
2688d83bd0 refactor(ui): fix useIsReadyToEnqueue for new adapterType field 2024-09-05 22:40:10 +10:00
psychedelicious
6e30f65a16 refactor(ui): update generation tab graphs 2024-09-05 22:40:10 +10:00
psychedelicious
fcd3773804 refactor(ui): add adapterType to ControlAdapterData 2024-09-05 22:40:10 +10:00
psychedelicious
3a3a1e076f refactor(ui): update components & logic to use new unified slice (again) 2024-09-05 22:40:10 +10:00
psychedelicious
566d9f99dd refactor(ui): update components & logic to use new unified slice 2024-09-05 22:40:10 +10:00
psychedelicious
21090dee48 refactor(ui): merge compositing, params into canvasV2 slice 2024-09-05 22:40:10 +10:00
psychedelicious
29c9e8f4b6 refactor(ui): add scaled bbox state 2024-09-05 22:40:10 +10:00
psychedelicious
5156b82ca1 refactor(ui): update dnd/image upload 2024-09-05 22:40:10 +10:00
psychedelicious
ff2371ce82 refactor(ui): update size/prompts state 2024-09-05 22:40:10 +10:00
psychedelicious
0cafbd7ba5 refactor(ui): rip out old control adapter implementation 2024-09-05 22:40:10 +10:00
psychedelicious
747a7d16c7 refactor(ui): canvas v2 (wip)
fix entity count select
2024-09-05 22:40:10 +10:00
psychedelicious
cf271700bf refactor(ui): canvas v2 (wip)
delete unused file
2024-09-05 22:40:10 +10:00
psychedelicious
43a40d88be refactor(ui): canvas v2 (wip)
merge all canvas state reducers into one big slice (but with the logic split across files so it's not hell)
2024-09-05 22:40:10 +10:00
psychedelicious
c761340871 refactor(ui): canvas v2 (wip)
Fix a few more components
2024-09-05 22:40:09 +10:00
psychedelicious
6271d1c34d refactor(ui): canvas v2 (wip)
missed a spot
2024-09-05 22:40:09 +10:00
psychedelicious
a7a09feaf0 refactor(ui): canvas v2 (wip)
Redo all UI components for different canvas entity types
2024-09-05 22:40:09 +10:00
psychedelicious
4f0aea2592 refactor(ui): canvas v2 (wip) 2024-09-05 22:40:09 +10:00
psychedelicious
e00ba3f6cd refactor(ui): canvas v2 (wip) 2024-09-05 22:40:09 +10:00
psychedelicious
920873e009 refactor(ui): canvas v2 (wip) 2024-09-05 22:40:09 +10:00
psychedelicious
e126ec9703 refactor(ui): canvas v2 (wip) 2024-09-05 22:40:09 +10:00
psychedelicious
ceb81d6fed feat(ui): bbox tool 2024-09-05 22:40:09 +10:00
psychedelicious
5088c9eae1 fix(ui): rect tool preview 2024-09-05 22:40:09 +10:00
psychedelicious
d41ad5115e fix(ui): multiple stages 2024-09-05 22:40:09 +10:00
psychedelicious
4caab2d2e3 feat(ui): decouple konva logic from nanostores 2024-09-05 22:40:09 +10:00
psychedelicious
528254fdd4 feat(ui): store all stage attrs in nanostores 2024-09-05 22:40:09 +10:00
psychedelicious
939ae5a7c6 feat(ui): round stage scale 2024-09-05 22:40:09 +10:00
psychedelicious
b75830086b chore(ui): bump konva 2024-09-05 22:40:09 +10:00
psychedelicious
0fea74a58a feat(ui): generation bbox transformation working
whew
2024-09-05 22:40:09 +10:00
psychedelicious
89e0fdadc5 feat(ui): wip generation bbox 2024-09-05 22:40:09 +10:00
psychedelicious
a5a5e45a59 feat(ui): wip generation bbox 2024-09-05 22:40:09 +10:00
psychedelicious
61bd9aac0f feat(ui): CL zoom and pan, some rendering optimizations 2024-09-05 22:40:09 +10:00
psychedelicious
aba28f04f8 Revert "feat(ui): add x,y,scaleX,scaleY,rotation to objects"
This reverts commit 53318b396c967c72326a7e4dea09667b2ab20bdd.
2024-09-05 22:40:09 +10:00
psychedelicious
bc7b4c5d8e feat(ui): layers manage their own bbox 2024-09-05 22:40:09 +10:00
psychedelicious
98359237c6 docs(ui): konva image object docstrings 2024-09-05 22:40:09 +10:00
psychedelicious
6982a9f41d feat(ui): add x,y,scaleX,scaleY,rotation to objects 2024-09-05 22:40:09 +10:00
psychedelicious
5665f1db7b fix(ui): show color picker when using rect tool 2024-09-05 22:40:09 +10:00
psychedelicious
7a6c9a60b3 feat(ui): image loading fallback for raster layers 2024-09-05 22:40:09 +10:00
psychedelicious
2e7ef452d5 feat(ui): bbox calc for raster layers 2024-09-05 22:40:09 +10:00
psychedelicious
5468b25c65 feat(ui): do not fill brush preview when drawing 2024-09-05 22:40:09 +10:00
psychedelicious
e4a1ef0c19 fix(ui): brush spacing handling 2024-09-05 22:40:09 +10:00
psychedelicious
fcb31d3cd2 fix(ui): jank when starting a shape when not already focused on stage 2024-09-05 22:40:09 +10:00
psychedelicious
cb32ce6a41 feat(ui): wip raster layers
I meant to split this up into smaller commits and undo some of it, but I committed afterwards and it's tedious to undo.
2024-09-05 22:40:09 +10:00
psychedelicious
a6c7f0d282 feat(ui): support image objects on raster layers
Just the UI and internal state, not rendering yet.
2024-09-05 22:40:09 +10:00
psychedelicious
45f296a35e tidy(ui): clean up event handlers
Separate logic for each tool in preparation for ellipse and polygon tools.
2024-09-05 22:40:09 +10:00
psychedelicious
8e6469d9d7 feat(ui): raster layer reset, object group util 2024-09-05 22:40:09 +10:00
psychedelicious
3298875cda feat(ui): rect shape preview now has fill 2024-09-05 22:40:09 +10:00
psychedelicious
d1c6a37b76 feat(ui): cancel shape drawing on esc 2024-09-05 22:40:09 +10:00
psychedelicious
31db9a178d feat(ui): temp disable history on CL 2024-09-05 22:40:09 +10:00
psychedelicious
1c766a43ee feat(ui): raster layer logic
- Deduplicate shared logic
- Split up giant renderers file into separate cohesive files
- Tons of cleanup
- Progress on raster layer functionality
2024-09-05 22:40:09 +10:00
psychedelicious
7b7a3fbd57 feat(ui): add raster layer rendering and interaction (WIP) 2024-09-05 22:40:09 +10:00
psychedelicious
d12474d93d feat(ui): scaffold out raster layers
Raster layers may have images, lines and shapes. These will replace initial image layers and provide sketching functionality like we have on canvas.
2024-09-05 22:40:09 +10:00
psychedelicious
12ac78a490 refactor(ui): revise types for line and rect objects
- Create separate object types for brush and eraser lines, instead of a single type that has a `tool` field.
- Create new object type for rect shapes.
- Add logic to schemas to migrate old object types to new.
- Update renderers & reducers.
2024-09-05 22:40:09 +10:00
245 changed files with 3716 additions and 7647 deletions

View File

@@ -11,7 +11,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
Batch,
BatchStatus,
CancelByBatchIDsResult,
CancelByDestinationResult,
CancelByOriginResult,
ClearResult,
EnqueueBatchResult,
PruneResult,
@@ -107,18 +107,16 @@ async def cancel_by_batch_ids(
@session_queue_router.put(
"/{queue_id}/cancel_by_destination",
operation_id="cancel_by_destination",
"/{queue_id}/cancel_by_origin",
operation_id="cancel_by_origin",
responses={200: {"model": CancelByBatchIDsResult}},
)
async def cancel_by_destination(
async def cancel_by_origin(
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:
origin: str = Query(description="The origin to cancel all queue items for"),
) -> CancelByOriginResult:
"""Immediately cancels all queue items with the given origin"""
return ApiDependencies.invoker.services.session_queue.cancel_by_destination(
queue_id=queue_id, destination=destination
)
return ApiDependencies.invoker.services.session_queue.cancel_by_origin(queue_id=queue_id, origin=origin)
@session_queue_router.put(

View File

@@ -19,8 +19,7 @@ from invokeai.app.invocations.model import CLIPField
from invokeai.app.invocations.primitives import ConditioningOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.app.util.ti_utils import generate_ti_list
from invokeai.backend.lora.lora_model_raw import LoRAModelRaw
from invokeai.backend.lora.lora_patcher import LoraPatcher
from invokeai.backend.lora import LoRAModelRaw
from invokeai.backend.model_patcher import ModelPatcher
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import (
BasicConditioningInfo,
@@ -83,10 +82,9 @@ class CompelInvocation(BaseInvocation):
# apply all patches while the model is on the target device
text_encoder_info.model_on_device() as (cached_weights, text_encoder),
tokenizer_info as tokenizer,
LoraPatcher.apply_lora_patches(
model=text_encoder,
patches=_lora_loader(),
prefix="lora_te_",
ModelPatcher.apply_lora_text_encoder(
text_encoder,
loras=_lora_loader(),
cached_weights=cached_weights,
),
# Apply CLIP Skip after LoRA to prevent LoRA application from failing on skipped layers.
@@ -179,9 +177,9 @@ class SDXLPromptInvocationBase:
# apply all patches while the model is on the target device
text_encoder_info.model_on_device() as (cached_weights, text_encoder),
tokenizer_info as tokenizer,
LoraPatcher.apply_lora_patches(
ModelPatcher.apply_lora(
text_encoder,
patches=_lora_loader(),
loras=_lora_loader(),
prefix=lora_prefix,
cached_weights=cached_weights,
),

View File

@@ -36,8 +36,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.lora.lora_model_raw import LoRAModelRaw
from invokeai.backend.lora.lora_patcher import LoraPatcher
from invokeai.backend.lora import LoRAModelRaw
from invokeai.backend.model_manager import BaseModelType, ModelVariantType
from invokeai.backend.model_patcher import ModelPatcher
from invokeai.backend.stable_diffusion import PipelineIntermediateState
@@ -980,10 +979,9 @@ class DenoiseLatentsInvocation(BaseInvocation):
ModelPatcher.apply_freeu(unet, self.unet.freeu_config),
SeamlessExt.static_patch_model(unet, self.unet.seamless_axes), # FIXME
# Apply the LoRA after unet has been moved to its target device for faster patching.
LoraPatcher.apply_lora_patches(
model=unet,
patches=_lora_loader(),
prefix="lora_unet_",
ModelPatcher.apply_lora_unet(
unet,
loras=_lora_loader(),
cached_weights=cached_weights,
),
):

View File

@@ -1,4 +1,4 @@
from typing import Callable, Iterator, Optional, Tuple
from typing import Callable, Optional
import torch
import torchvision.transforms as tv_transforms
@@ -29,8 +29,6 @@ from invokeai.backend.flux.sampling_utils import (
pack,
unpack,
)
from invokeai.backend.lora.lora_model_raw import LoRAModelRaw
from invokeai.backend.lora.lora_patcher import LoraPatcher
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
@@ -189,16 +187,7 @@ class FluxDenoiseInvocation(BaseInvocation, WithMetadata, WithBoard):
noise=noise,
)
with (
transformer_info.model_on_device() as (cached_weights, transformer),
# Apply the LoRA after transformer has been moved to its target device for faster patching.
LoraPatcher.apply_lora_patches(
model=transformer,
patches=self._lora_iterator(context),
prefix="",
cached_weights=cached_weights,
),
):
with transformer_info as transformer:
assert isinstance(transformer, Flux)
x = denoise(
@@ -252,13 +241,6 @@ class FluxDenoiseInvocation(BaseInvocation, WithMetadata, WithBoard):
# `latents`.
return mask.expand_as(latents)
def _lora_iterator(self, context: InvocationContext) -> Iterator[Tuple[LoRAModelRaw, float]]:
for lora in self.transformer.loras:
lora_info = context.models.load(lora.lora)
assert isinstance(lora_info.model, LoRAModelRaw)
yield (lora_info.model, lora.weight)
del lora_info
def _build_step_callback(self, context: InvocationContext) -> Callable[[PipelineIntermediateState], None]:
def step_callback(state: PipelineIntermediateState) -> None:
state.latents = unpack(state.latents.float(), self.height, self.width).squeeze()

View File

@@ -1,53 +0,0 @@
from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output
from invokeai.app.invocations.fields import FieldDescriptions, Input, InputField, OutputField, UIType
from invokeai.app.invocations.model import LoRAField, ModelIdentifierField, TransformerField
from invokeai.app.services.shared.invocation_context import InvocationContext
@invocation_output("flux_lora_loader_output")
class FluxLoRALoaderOutput(BaseInvocationOutput):
"""FLUX LoRA Loader Output"""
transformer: TransformerField = OutputField(
default=None, description=FieldDescriptions.transformer, title="FLUX Transformer"
)
@invocation(
"flux_lora_loader",
title="FLUX LoRA",
tags=["lora", "model", "flux"],
category="model",
version="1.0.0",
)
class FluxLoRALoaderInvocation(BaseInvocation):
"""Apply a LoRA model to a FLUX transformer."""
lora: ModelIdentifierField = InputField(
description=FieldDescriptions.lora_model, title="LoRA", ui_type=UIType.LoRAModel
)
weight: float = InputField(default=0.75, description=FieldDescriptions.lora_weight)
transformer: TransformerField = InputField(
description=FieldDescriptions.transformer,
input=Input.Connection,
title="FLUX Transformer",
)
def invoke(self, context: InvocationContext) -> FluxLoRALoaderOutput:
lora_key = self.lora.key
if not context.models.exists(lora_key):
raise ValueError(f"Unknown lora: {lora_key}!")
if any(lora.lora.key == lora_key for lora in self.transformer.loras):
raise Exception(f'LoRA "{lora_key}" already applied to transformer.')
transformer = self.transformer.model_copy(deep=True)
transformer.loras.append(
LoRAField(
lora=self.lora,
weight=self.weight,
)
)
return FluxLoRALoaderOutput(transformer=transformer)

View File

@@ -69,7 +69,6 @@ class CLIPField(BaseModel):
class TransformerField(BaseModel):
transformer: ModelIdentifierField = Field(description="Info to load Transformer submodel")
loras: List[LoRAField] = Field(description="LoRAs to apply on model loading")
class T5EncoderField(BaseModel):
@@ -203,7 +202,7 @@ class FluxModelLoaderInvocation(BaseInvocation):
assert isinstance(transformer_config, CheckpointConfigBase)
return FluxModelLoaderOutput(
transformer=TransformerField(transformer=transformer, loras=[]),
transformer=TransformerField(transformer=transformer),
clip=CLIPField(tokenizer=tokenizer, text_encoder=clip_encoder, loras=[], skipped_layers=0),
t5_encoder=T5EncoderField(tokenizer=tokenizer2, text_encoder=t5_encoder),
vae=VAEField(vae=vae),

View File

@@ -22,8 +22,8 @@ from invokeai.app.invocations.fields import (
from invokeai.app.invocations.model import UNetField
from invokeai.app.invocations.primitives import LatentsOutput
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.lora.lora_model_raw import LoRAModelRaw
from invokeai.backend.lora.lora_patcher import LoraPatcher
from invokeai.backend.lora import LoRAModelRaw
from invokeai.backend.model_patcher import ModelPatcher
from invokeai.backend.stable_diffusion.diffusers_pipeline import ControlNetData, PipelineIntermediateState
from invokeai.backend.stable_diffusion.multi_diffusion_pipeline import (
MultiDiffusionPipeline,
@@ -204,11 +204,7 @@ class TiledMultiDiffusionDenoiseLatents(BaseInvocation):
# Load the UNet model.
unet_info = context.models.load(self.unet.unet)
with (
ExitStack() as exit_stack,
unet_info as unet,
LoraPatcher.apply_lora_patches(model=unet, patches=_lora_loader(), prefix="lora_unet_"),
):
with ExitStack() as exit_stack, unet_info as unet, ModelPatcher.apply_lora_unet(unet, _lora_loader()):
assert isinstance(unet, UNet2DConditionModel)
latents = latents.to(device=unet.device, dtype=unet.dtype)
if noise is not None:

View File

@@ -6,7 +6,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
Batch,
BatchStatus,
CancelByBatchIDsResult,
CancelByDestinationResult,
CancelByOriginResult,
CancelByQueueIDResult,
ClearResult,
EnqueueBatchResult,
@@ -97,8 +97,8 @@ class SessionQueueBase(ABC):
pass
@abstractmethod
def cancel_by_destination(self, queue_id: str, destination: str) -> CancelByDestinationResult:
"""Cancels all queue items with the given batch destination"""
def cancel_by_origin(self, queue_id: str, origin: str) -> CancelByOriginResult:
"""Cancels all queue items with the given batch origin"""
pass
@abstractmethod

View File

@@ -346,10 +346,10 @@ class CancelByBatchIDsResult(BaseModel):
canceled: int = Field(..., description="Number of queue items canceled")
class CancelByDestinationResult(CancelByBatchIDsResult):
"""Result of canceling by a destination"""
class CancelByOriginResult(BaseModel):
"""Result of canceling by list of batch ids"""
pass
canceled: int = Field(..., description="Number of queue items canceled")
class CancelByQueueIDResult(CancelByBatchIDsResult):

View File

@@ -10,7 +10,7 @@ from invokeai.app.services.session_queue.session_queue_common import (
Batch,
BatchStatus,
CancelByBatchIDsResult,
CancelByDestinationResult,
CancelByOriginResult,
CancelByQueueIDResult,
ClearResult,
EnqueueBatchResult,
@@ -426,19 +426,19 @@ class SqliteSessionQueue(SessionQueueBase):
self.__lock.release()
return CancelByBatchIDsResult(canceled=count)
def cancel_by_destination(self, queue_id: str, destination: str) -> CancelByDestinationResult:
def cancel_by_origin(self, queue_id: str, origin: str) -> CancelByOriginResult:
try:
current_queue_item = self.get_current(queue_id)
self.__lock.acquire()
where = """--sql
WHERE
queue_id == ?
AND destination == ?
AND origin == ?
AND status != 'canceled'
AND status != 'completed'
AND status != 'failed'
"""
params = (queue_id, destination)
params = (queue_id, origin)
self.__cursor.execute(
f"""--sql
SELECT COUNT(*)
@@ -457,14 +457,14 @@ class SqliteSessionQueue(SessionQueueBase):
params,
)
self.__conn.commit()
if current_queue_item is not None and current_queue_item.destination == destination:
if current_queue_item is not None and current_queue_item.origin == origin:
self._set_queue_item_status(current_queue_item.item_id, "canceled")
except Exception:
self.__conn.rollback()
raise
finally:
self.__lock.release()
return CancelByDestinationResult(canceled=count)
return CancelByOriginResult(canceled=count)
def cancel_by_queue_id(self, queue_id: str) -> CancelByQueueIDResult:
try:

672
invokeai/backend/lora.py Normal file
View File

@@ -0,0 +1,672 @@
# Copyright (c) 2024 The InvokeAI Development team
"""LoRA model support."""
import bisect
from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple, Union
import torch
from safetensors.torch import load_file
from typing_extensions import Self
import invokeai.backend.util.logging as logger
from invokeai.backend.model_manager import BaseModelType
from invokeai.backend.raw_model import RawModel
class LoRALayerBase:
# rank: Optional[int]
# alpha: Optional[float]
# bias: Optional[torch.Tensor]
# layer_key: str
# @property
# def scale(self):
# return self.alpha / self.rank if (self.alpha and self.rank) else 1.0
def __init__(
self,
layer_key: str,
values: Dict[str, torch.Tensor],
):
if "alpha" in values:
self.alpha = values["alpha"].item()
else:
self.alpha = None
if "bias_indices" in values and "bias_values" in values and "bias_size" in values:
self.bias: Optional[torch.Tensor] = torch.sparse_coo_tensor(
values["bias_indices"],
values["bias_values"],
tuple(values["bias_size"]),
)
else:
self.bias = None
self.rank = None # set in layer implementation
self.layer_key = layer_key
def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor:
raise NotImplementedError()
def get_bias(self, orig_bias: torch.Tensor) -> Optional[torch.Tensor]:
return self.bias
def get_parameters(self, orig_module: torch.nn.Module) -> Dict[str, torch.Tensor]:
params = {"weight": self.get_weight(orig_module.weight)}
bias = self.get_bias(orig_module.bias)
if bias is not None:
params["bias"] = bias
return params
def calc_size(self) -> int:
model_size = 0
for val in [self.bias]:
if val is not None:
model_size += val.nelement() * val.element_size()
return model_size
def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> None:
if self.bias is not None:
self.bias = self.bias.to(device=device, dtype=dtype)
def check_keys(self, values: Dict[str, torch.Tensor], known_keys: Set[str]):
"""Log a warning if values contains unhandled keys."""
# {"alpha", "bias_indices", "bias_values", "bias_size"} are hard-coded, because they are handled by
# `LoRALayerBase`. Sub-classes should provide the known_keys that they handled.
all_known_keys = known_keys | {"alpha", "bias_indices", "bias_values", "bias_size"}
unknown_keys = set(values.keys()) - all_known_keys
if unknown_keys:
logger.warning(
f"Unexpected keys found in LoRA/LyCORIS layer, model might work incorrectly! Keys: {unknown_keys}"
)
# TODO: find and debug lora/locon with bias
class LoRALayer(LoRALayerBase):
# up: torch.Tensor
# mid: Optional[torch.Tensor]
# down: torch.Tensor
def __init__(
self,
layer_key: str,
values: Dict[str, torch.Tensor],
):
super().__init__(layer_key, values)
self.up = values["lora_up.weight"]
self.down = values["lora_down.weight"]
self.mid = values.get("lora_mid.weight", None)
self.rank = self.down.shape[0]
self.check_keys(
values,
{
"lora_up.weight",
"lora_down.weight",
"lora_mid.weight",
},
)
def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor:
if self.mid is not None:
up = self.up.reshape(self.up.shape[0], self.up.shape[1])
down = self.down.reshape(self.down.shape[0], self.down.shape[1])
weight = torch.einsum("m n w h, i m, n j -> i j w h", self.mid, up, down)
else:
weight = self.up.reshape(self.up.shape[0], -1) @ self.down.reshape(self.down.shape[0], -1)
return weight
def calc_size(self) -> int:
model_size = super().calc_size()
for val in [self.up, self.mid, self.down]:
if val is not None:
model_size += val.nelement() * val.element_size()
return model_size
def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> None:
super().to(device=device, dtype=dtype)
self.up = self.up.to(device=device, dtype=dtype)
self.down = self.down.to(device=device, dtype=dtype)
if self.mid is not None:
self.mid = self.mid.to(device=device, dtype=dtype)
class LoHALayer(LoRALayerBase):
# w1_a: torch.Tensor
# w1_b: torch.Tensor
# w2_a: torch.Tensor
# w2_b: torch.Tensor
# t1: Optional[torch.Tensor] = None
# t2: Optional[torch.Tensor] = None
def __init__(self, layer_key: str, values: Dict[str, torch.Tensor]):
super().__init__(layer_key, values)
self.w1_a = values["hada_w1_a"]
self.w1_b = values["hada_w1_b"]
self.w2_a = values["hada_w2_a"]
self.w2_b = values["hada_w2_b"]
self.t1 = values.get("hada_t1", None)
self.t2 = values.get("hada_t2", None)
self.rank = self.w1_b.shape[0]
self.check_keys(
values,
{
"hada_w1_a",
"hada_w1_b",
"hada_w2_a",
"hada_w2_b",
"hada_t1",
"hada_t2",
},
)
def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor:
if self.t1 is None:
weight: torch.Tensor = (self.w1_a @ self.w1_b) * (self.w2_a @ self.w2_b)
else:
rebuild1 = torch.einsum("i j k l, j r, i p -> p r k l", self.t1, self.w1_b, self.w1_a)
rebuild2 = torch.einsum("i j k l, j r, i p -> p r k l", self.t2, self.w2_b, self.w2_a)
weight = rebuild1 * rebuild2
return weight
def calc_size(self) -> int:
model_size = super().calc_size()
for val in [self.w1_a, self.w1_b, self.w2_a, self.w2_b, self.t1, self.t2]:
if val is not None:
model_size += val.nelement() * val.element_size()
return model_size
def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> None:
super().to(device=device, dtype=dtype)
self.w1_a = self.w1_a.to(device=device, dtype=dtype)
self.w1_b = self.w1_b.to(device=device, dtype=dtype)
if self.t1 is not None:
self.t1 = self.t1.to(device=device, dtype=dtype)
self.w2_a = self.w2_a.to(device=device, dtype=dtype)
self.w2_b = self.w2_b.to(device=device, dtype=dtype)
if self.t2 is not None:
self.t2 = self.t2.to(device=device, dtype=dtype)
class LoKRLayer(LoRALayerBase):
# w1: Optional[torch.Tensor] = None
# w1_a: Optional[torch.Tensor] = None
# w1_b: Optional[torch.Tensor] = None
# w2: Optional[torch.Tensor] = None
# w2_a: Optional[torch.Tensor] = None
# w2_b: Optional[torch.Tensor] = None
# t2: Optional[torch.Tensor] = None
def __init__(
self,
layer_key: str,
values: Dict[str, torch.Tensor],
):
super().__init__(layer_key, values)
self.w1 = values.get("lokr_w1", None)
if self.w1 is None:
self.w1_a = values["lokr_w1_a"]
self.w1_b = values["lokr_w1_b"]
else:
self.w1_b = None
self.w1_a = None
self.w2 = values.get("lokr_w2", None)
if self.w2 is None:
self.w2_a = values["lokr_w2_a"]
self.w2_b = values["lokr_w2_b"]
else:
self.w2_a = None
self.w2_b = None
self.t2 = values.get("lokr_t2", None)
if self.w1_b is not None:
self.rank = self.w1_b.shape[0]
elif self.w2_b is not None:
self.rank = self.w2_b.shape[0]
else:
self.rank = None # unscaled
self.check_keys(
values,
{
"lokr_w1",
"lokr_w1_a",
"lokr_w1_b",
"lokr_w2",
"lokr_w2_a",
"lokr_w2_b",
"lokr_t2",
},
)
def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor:
w1: Optional[torch.Tensor] = self.w1
if w1 is None:
assert self.w1_a is not None
assert self.w1_b is not None
w1 = self.w1_a @ self.w1_b
w2 = self.w2
if w2 is None:
if self.t2 is None:
assert self.w2_a is not None
assert self.w2_b is not None
w2 = self.w2_a @ self.w2_b
else:
w2 = torch.einsum("i j k l, i p, j r -> p r k l", self.t2, self.w2_a, self.w2_b)
if len(w2.shape) == 4:
w1 = w1.unsqueeze(2).unsqueeze(2)
w2 = w2.contiguous()
assert w1 is not None
assert w2 is not None
weight = torch.kron(w1, w2)
return weight
def calc_size(self) -> int:
model_size = super().calc_size()
for val in [self.w1, self.w1_a, self.w1_b, self.w2, self.w2_a, self.w2_b, self.t2]:
if val is not None:
model_size += val.nelement() * val.element_size()
return model_size
def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> None:
super().to(device=device, dtype=dtype)
if self.w1 is not None:
self.w1 = self.w1.to(device=device, dtype=dtype)
else:
assert self.w1_a is not None
assert self.w1_b is not None
self.w1_a = self.w1_a.to(device=device, dtype=dtype)
self.w1_b = self.w1_b.to(device=device, dtype=dtype)
if self.w2 is not None:
self.w2 = self.w2.to(device=device, dtype=dtype)
else:
assert self.w2_a is not None
assert self.w2_b is not None
self.w2_a = self.w2_a.to(device=device, dtype=dtype)
self.w2_b = self.w2_b.to(device=device, dtype=dtype)
if self.t2 is not None:
self.t2 = self.t2.to(device=device, dtype=dtype)
class FullLayer(LoRALayerBase):
# bias handled in LoRALayerBase(calc_size, to)
# weight: torch.Tensor
# bias: Optional[torch.Tensor]
def __init__(
self,
layer_key: str,
values: Dict[str, torch.Tensor],
):
super().__init__(layer_key, values)
self.weight = values["diff"]
self.bias = values.get("diff_b", None)
self.rank = None # unscaled
self.check_keys(values, {"diff", "diff_b"})
def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor:
return self.weight
def calc_size(self) -> int:
model_size = super().calc_size()
model_size += self.weight.nelement() * self.weight.element_size()
return model_size
def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> None:
super().to(device=device, dtype=dtype)
self.weight = self.weight.to(device=device, dtype=dtype)
class IA3Layer(LoRALayerBase):
# weight: torch.Tensor
# on_input: torch.Tensor
def __init__(
self,
layer_key: str,
values: Dict[str, torch.Tensor],
):
super().__init__(layer_key, values)
self.weight = values["weight"]
self.on_input = values["on_input"]
self.rank = None # unscaled
self.check_keys(values, {"weight", "on_input"})
def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor:
weight = self.weight
if not self.on_input:
weight = weight.reshape(-1, 1)
assert orig_weight is not None
return orig_weight * weight
def calc_size(self) -> int:
model_size = super().calc_size()
model_size += self.weight.nelement() * self.weight.element_size()
model_size += self.on_input.nelement() * self.on_input.element_size()
return model_size
def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None):
super().to(device=device, dtype=dtype)
self.weight = self.weight.to(device=device, dtype=dtype)
self.on_input = self.on_input.to(device=device, dtype=dtype)
class NormLayer(LoRALayerBase):
# bias handled in LoRALayerBase(calc_size, to)
# weight: torch.Tensor
# bias: Optional[torch.Tensor]
def __init__(
self,
layer_key: str,
values: Dict[str, torch.Tensor],
):
super().__init__(layer_key, values)
self.weight = values["w_norm"]
self.bias = values.get("b_norm", None)
self.rank = None # unscaled
self.check_keys(values, {"w_norm", "b_norm"})
def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor:
return self.weight
def calc_size(self) -> int:
model_size = super().calc_size()
model_size += self.weight.nelement() * self.weight.element_size()
return model_size
def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> None:
super().to(device=device, dtype=dtype)
self.weight = self.weight.to(device=device, dtype=dtype)
AnyLoRALayer = Union[LoRALayer, LoHALayer, LoKRLayer, FullLayer, IA3Layer, NormLayer]
class LoRAModelRaw(RawModel): # (torch.nn.Module):
_name: str
layers: Dict[str, AnyLoRALayer]
def __init__(
self,
name: str,
layers: Dict[str, AnyLoRALayer],
):
self._name = name
self.layers = layers
@property
def name(self) -> str:
return self._name
def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> None:
# TODO: try revert if exception?
for _key, layer in self.layers.items():
layer.to(device=device, dtype=dtype)
def calc_size(self) -> int:
model_size = 0
for _, layer in self.layers.items():
model_size += layer.calc_size()
return model_size
@classmethod
def _convert_sdxl_keys_to_diffusers_format(cls, state_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]:
"""Convert the keys of an SDXL LoRA state_dict to diffusers format.
The input state_dict can be in either Stability AI format or diffusers format. If the state_dict is already in
diffusers format, then this function will have no effect.
This function is adapted from:
https://github.com/bmaltais/kohya_ss/blob/2accb1305979ba62f5077a23aabac23b4c37e935/networks/lora_diffusers.py#L385-L409
Args:
state_dict (Dict[str, Tensor]): The SDXL LoRA state_dict.
Raises:
ValueError: If state_dict contains an unrecognized key, or not all keys could be converted.
Returns:
Dict[str, Tensor]: The diffusers-format state_dict.
"""
converted_count = 0 # The number of Stability AI keys converted to diffusers format.
not_converted_count = 0 # The number of keys that were not converted.
# Get a sorted list of Stability AI UNet keys so that we can efficiently search for keys with matching prefixes.
# For example, we want to efficiently find `input_blocks_4_1` in the list when searching for
# `input_blocks_4_1_proj_in`.
stability_unet_keys = list(SDXL_UNET_STABILITY_TO_DIFFUSERS_MAP)
stability_unet_keys.sort()
new_state_dict = {}
for full_key, value in state_dict.items():
if full_key.startswith("lora_unet_"):
search_key = full_key.replace("lora_unet_", "")
# Use bisect to find the key in stability_unet_keys that *may* match the search_key's prefix.
position = bisect.bisect_right(stability_unet_keys, search_key)
map_key = stability_unet_keys[position - 1]
# Now, check if the map_key *actually* matches the search_key.
if search_key.startswith(map_key):
new_key = full_key.replace(map_key, SDXL_UNET_STABILITY_TO_DIFFUSERS_MAP[map_key])
new_state_dict[new_key] = value
converted_count += 1
else:
new_state_dict[full_key] = value
not_converted_count += 1
elif full_key.startswith("lora_te1_") or full_key.startswith("lora_te2_"):
# The CLIP text encoders have the same keys in both Stability AI and diffusers formats.
new_state_dict[full_key] = value
continue
else:
raise ValueError(f"Unrecognized SDXL LoRA key prefix: '{full_key}'.")
if converted_count > 0 and not_converted_count > 0:
raise ValueError(
f"The SDXL LoRA could only be partially converted to diffusers format. converted={converted_count},"
f" not_converted={not_converted_count}"
)
return new_state_dict
@classmethod
def from_checkpoint(
cls,
file_path: Union[str, Path],
device: Optional[torch.device] = None,
dtype: Optional[torch.dtype] = None,
base_model: Optional[BaseModelType] = None,
) -> Self:
device = device or torch.device("cpu")
dtype = dtype or torch.float32
if isinstance(file_path, str):
file_path = Path(file_path)
model = cls(
name=file_path.stem,
layers={},
)
if file_path.suffix == ".safetensors":
sd = load_file(file_path.absolute().as_posix(), device="cpu")
else:
sd = torch.load(file_path, map_location="cpu")
state_dict = cls._group_state(sd)
if base_model == BaseModelType.StableDiffusionXL:
state_dict = cls._convert_sdxl_keys_to_diffusers_format(state_dict)
for layer_key, values in state_dict.items():
# Detect layers according to LyCORIS detection logic(`weight_list_det`)
# https://github.com/KohakuBlueleaf/LyCORIS/tree/8ad8000efb79e2b879054da8c9356e6143591bad/lycoris/modules
# lora and locon
if "lora_up.weight" in values:
layer: AnyLoRALayer = LoRALayer(layer_key, values)
# loha
elif "hada_w1_a" in values:
layer = LoHALayer(layer_key, values)
# lokr
elif "lokr_w1" in values or "lokr_w1_a" in values:
layer = LoKRLayer(layer_key, values)
# diff
elif "diff" in values:
layer = FullLayer(layer_key, values)
# ia3
elif "on_input" in values:
layer = IA3Layer(layer_key, values)
# norms
elif "w_norm" in values:
layer = NormLayer(layer_key, values)
else:
print(f">> Encountered unknown lora layer module in {model.name}: {layer_key} - {list(values.keys())}")
raise Exception("Unknown lora format!")
# lower memory consumption by removing already parsed layer values
state_dict[layer_key].clear()
layer.to(device=device, dtype=dtype)
model.layers[layer_key] = layer
return model
@staticmethod
def _group_state(state_dict: Dict[str, torch.Tensor]) -> Dict[str, Dict[str, torch.Tensor]]:
state_dict_groupped: Dict[str, Dict[str, torch.Tensor]] = {}
for key, value in state_dict.items():
stem, leaf = key.split(".", 1)
if stem not in state_dict_groupped:
state_dict_groupped[stem] = {}
state_dict_groupped[stem][leaf] = value
return state_dict_groupped
# code from
# https://github.com/bmaltais/kohya_ss/blob/2accb1305979ba62f5077a23aabac23b4c37e935/networks/lora_diffusers.py#L15C1-L97C32
def make_sdxl_unet_conversion_map() -> List[Tuple[str, str]]:
"""Create a dict mapping state_dict keys from Stability AI SDXL format to diffusers SDXL format."""
unet_conversion_map_layer = []
for i in range(3): # num_blocks is 3 in sdxl
# loop over downblocks/upblocks
for j in range(2):
# loop over resnets/attentions for downblocks
hf_down_res_prefix = f"down_blocks.{i}.resnets.{j}."
sd_down_res_prefix = f"input_blocks.{3*i + j + 1}.0."
unet_conversion_map_layer.append((sd_down_res_prefix, hf_down_res_prefix))
if i < 3:
# no attention layers in down_blocks.3
hf_down_atn_prefix = f"down_blocks.{i}.attentions.{j}."
sd_down_atn_prefix = f"input_blocks.{3*i + j + 1}.1."
unet_conversion_map_layer.append((sd_down_atn_prefix, hf_down_atn_prefix))
for j in range(3):
# loop over resnets/attentions for upblocks
hf_up_res_prefix = f"up_blocks.{i}.resnets.{j}."
sd_up_res_prefix = f"output_blocks.{3*i + j}.0."
unet_conversion_map_layer.append((sd_up_res_prefix, hf_up_res_prefix))
# if i > 0: commentout for sdxl
# no attention layers in up_blocks.0
hf_up_atn_prefix = f"up_blocks.{i}.attentions.{j}."
sd_up_atn_prefix = f"output_blocks.{3*i + j}.1."
unet_conversion_map_layer.append((sd_up_atn_prefix, hf_up_atn_prefix))
if i < 3:
# no downsample in down_blocks.3
hf_downsample_prefix = f"down_blocks.{i}.downsamplers.0.conv."
sd_downsample_prefix = f"input_blocks.{3*(i+1)}.0.op."
unet_conversion_map_layer.append((sd_downsample_prefix, hf_downsample_prefix))
# no upsample in up_blocks.3
hf_upsample_prefix = f"up_blocks.{i}.upsamplers.0."
sd_upsample_prefix = f"output_blocks.{3*i + 2}.{2}." # change for sdxl
unet_conversion_map_layer.append((sd_upsample_prefix, hf_upsample_prefix))
hf_mid_atn_prefix = "mid_block.attentions.0."
sd_mid_atn_prefix = "middle_block.1."
unet_conversion_map_layer.append((sd_mid_atn_prefix, hf_mid_atn_prefix))
for j in range(2):
hf_mid_res_prefix = f"mid_block.resnets.{j}."
sd_mid_res_prefix = f"middle_block.{2*j}."
unet_conversion_map_layer.append((sd_mid_res_prefix, hf_mid_res_prefix))
unet_conversion_map_resnet = [
# (stable-diffusion, HF Diffusers)
("in_layers.0.", "norm1."),
("in_layers.2.", "conv1."),
("out_layers.0.", "norm2."),
("out_layers.3.", "conv2."),
("emb_layers.1.", "time_emb_proj."),
("skip_connection.", "conv_shortcut."),
]
unet_conversion_map = []
for sd, hf in unet_conversion_map_layer:
if "resnets" in hf:
for sd_res, hf_res in unet_conversion_map_resnet:
unet_conversion_map.append((sd + sd_res, hf + hf_res))
else:
unet_conversion_map.append((sd, hf))
for j in range(2):
hf_time_embed_prefix = f"time_embedding.linear_{j+1}."
sd_time_embed_prefix = f"time_embed.{j*2}."
unet_conversion_map.append((sd_time_embed_prefix, hf_time_embed_prefix))
for j in range(2):
hf_label_embed_prefix = f"add_embedding.linear_{j+1}."
sd_label_embed_prefix = f"label_emb.0.{j*2}."
unet_conversion_map.append((sd_label_embed_prefix, hf_label_embed_prefix))
unet_conversion_map.append(("input_blocks.0.0.", "conv_in."))
unet_conversion_map.append(("out.0.", "conv_norm_out."))
unet_conversion_map.append(("out.2.", "conv_out."))
return unet_conversion_map
SDXL_UNET_STABILITY_TO_DIFFUSERS_MAP = {
sd.rstrip(".").replace(".", "_"): hf.rstrip(".").replace(".", "_") for sd, hf in make_sdxl_unet_conversion_map()
}

View File

@@ -1,210 +0,0 @@
from typing import Dict
import torch
from invokeai.backend.lora.layers.any_lora_layer import AnyLoRALayer
from invokeai.backend.lora.layers.concatenated_lora_layer import ConcatenatedLoRALayer
from invokeai.backend.lora.layers.lora_layer import LoRALayer
from invokeai.backend.lora.layers.lora_layer_base import LoRALayerBase
from invokeai.backend.lora.lora_model_raw import LoRAModelRaw
def is_state_dict_likely_in_flux_diffusers_format(state_dict: Dict[str, torch.Tensor]) -> bool:
"""Checks if the provided state dict is likely in the Diffusers FLUX LoRA format.
This is intended to be a reasonably high-precision detector, but it is not guaranteed to have perfect precision. (A
perfect-precision detector would require checking all keys against a whitelist and verifying tensor shapes.)
"""
# First, check that all keys end in "lora_A.weight" or "lora_B.weight" (i.e. are in PEFT format).
all_keys_in_peft_format = all(k.endswith(("lora_A.weight", "lora_B.weight")) for k in state_dict.keys())
# Next, check that this is likely a FLUX model by spot-checking a few keys.
expected_keys = [
"transformer.single_transformer_blocks.0.attn.to_q.lora_A.weight",
"transformer.single_transformer_blocks.0.attn.to_q.lora_B.weight",
"transformer.transformer_blocks.0.attn.add_q_proj.lora_A.weight",
"transformer.transformer_blocks.0.attn.add_q_proj.lora_B.weight",
]
all_expected_keys_present = all(k in state_dict for k in expected_keys)
return all_keys_in_peft_format and all_expected_keys_present
# TODO(ryand): What alpha should we use? 1.0? Rank of the LoRA?
def lora_model_from_flux_diffusers_state_dict(state_dict: Dict[str, torch.Tensor], alpha: float = 1.0) -> LoRAModelRaw: # pyright: ignore[reportRedeclaration] (state_dict is intentionally re-declared)
"""Loads a state dict in the Diffusers FLUX LoRA format into a LoRAModelRaw object.
This function is based on:
https://github.com/huggingface/diffusers/blob/55ac421f7bb12fd00ccbef727be4dc2f3f920abb/scripts/convert_flux_to_diffusers.py
"""
# Group keys by layer.
grouped_state_dict: dict[str, dict[str, torch.Tensor]] = _group_by_layer(state_dict)
# Remove the "transformer." prefix from all keys.
grouped_state_dict = {k.replace("transformer.", ""): v for k, v in grouped_state_dict.items()}
# Constants for FLUX.1
num_double_layers = 19
num_single_layers = 38
# inner_dim = 3072
# mlp_ratio = 4.0
layers: dict[str, AnyLoRALayer] = {}
def add_lora_layer_if_present(src_key: str, dst_key: str) -> None:
if src_key in grouped_state_dict:
src_layer_dict = grouped_state_dict.pop(src_key)
layers[dst_key] = LoRALayer(
values={
"lora_down.weight": src_layer_dict.pop("lora_A.weight"),
"lora_up.weight": src_layer_dict.pop("lora_B.weight"),
"alpha": torch.tensor(alpha),
},
)
assert len(src_layer_dict) == 0
def add_qkv_lora_layer_if_present(src_keys: list[str], dst_qkv_key: str) -> None:
"""Handle the Q, K, V matrices for a transformer block. We need special handling because the diffusers format
stores them in separate matrices, whereas the BFL format used internally by InvokeAI concatenates them.
"""
# We expect that either all src keys are present or none of them are. Verify this.
keys_present = [key in grouped_state_dict for key in src_keys]
assert all(keys_present) or not any(keys_present)
# If none of the keys are present, return early.
if not any(keys_present):
return
src_layer_dicts = [grouped_state_dict.pop(key) for key in src_keys]
sub_layers: list[LoRALayerBase] = []
for src_layer_dict in src_layer_dicts:
sub_layers.append(
LoRALayer(
values={
"lora_down.weight": src_layer_dict.pop("lora_A.weight"),
"lora_up.weight": src_layer_dict.pop("lora_B.weight"),
"alpha": torch.tensor(alpha),
},
)
)
assert len(src_layer_dict) == 0
layers[dst_qkv_key] = ConcatenatedLoRALayer(lora_layers=sub_layers, concat_axis=0)
# time_text_embed.timestep_embedder -> time_in.
add_lora_layer_if_present("time_text_embed.timestep_embedder.linear_1", "time_in.in_layer")
add_lora_layer_if_present("time_text_embed.timestep_embedder.linear_2", "time_in.out_layer")
# time_text_embed.text_embedder -> vector_in.
add_lora_layer_if_present("time_text_embed.text_embedder.linear_1", "vector_in.in_layer")
add_lora_layer_if_present("time_text_embed.text_embedder.linear_2", "vector_in.out_layer")
# time_text_embed.guidance_embedder -> guidance_in.
add_lora_layer_if_present("time_text_embed.guidance_embedder.linear_1", "guidance_in")
add_lora_layer_if_present("time_text_embed.guidance_embedder.linear_2", "guidance_in")
# context_embedder -> txt_in.
add_lora_layer_if_present("context_embedder", "txt_in")
# x_embedder -> img_in.
add_lora_layer_if_present("x_embedder", "img_in")
# Double transformer blocks.
for i in range(num_double_layers):
# norms.
add_lora_layer_if_present(f"transformer_blocks.{i}.norm1.linear", f"double_blocks.{i}.img_mod.lin")
add_lora_layer_if_present(f"transformer_blocks.{i}.norm1_context.linear", f"double_blocks.{i}.txt_mod.lin")
# Q, K, V
add_qkv_lora_layer_if_present(
[
f"transformer_blocks.{i}.attn.to_q",
f"transformer_blocks.{i}.attn.to_k",
f"transformer_blocks.{i}.attn.to_v",
],
f"double_blocks.{i}.img_attn.qkv",
)
add_qkv_lora_layer_if_present(
[
f"transformer_blocks.{i}.attn.add_q_proj",
f"transformer_blocks.{i}.attn.add_k_proj",
f"transformer_blocks.{i}.attn.add_v_proj",
],
f"double_blocks.{i}.txt_attn.qkv",
)
# ff img_mlp
add_lora_layer_if_present(
f"transformer_blocks.{i}.ff.net.0.proj",
f"double_blocks.{i}.img_mlp.0",
)
add_lora_layer_if_present(
f"transformer_blocks.{i}.ff.net.2",
f"double_blocks.{i}.img_mlp.2",
)
# ff txt_mlp
add_lora_layer_if_present(
f"transformer_blocks.{i}.ff_context.net.0.proj",
f"double_blocks.{i}.txt_mlp.0",
)
add_lora_layer_if_present(
f"transformer_blocks.{i}.ff_context.net.2",
f"double_blocks.{i}.txt_mlp.2",
)
# output projections.
add_lora_layer_if_present(
f"transformer_blocks.{i}.attn.to_out.0",
f"double_blocks.{i}.img_attn.proj",
)
add_lora_layer_if_present(
f"transformer_blocks.{i}.attn.to_add_out",
f"double_blocks.{i}.txt_attn.proj",
)
# Single transformer blocks.
for i in range(num_single_layers):
# norms
add_lora_layer_if_present(
f"single_transformer_blocks.{i}.norm.linear",
f"single_blocks.{i}.modulation.lin",
)
# Q, K, V, mlp
add_qkv_lora_layer_if_present(
[
f"single_transformer_blocks.{i}.attn.to_q",
f"single_transformer_blocks.{i}.attn.to_k",
f"single_transformer_blocks.{i}.attn.to_v",
f"single_transformer_blocks.{i}.proj_mlp",
],
f"single_blocks.{i}.linear1",
)
# Output projections.
add_lora_layer_if_present(
f"single_transformer_blocks.{i}.proj_out",
f"single_blocks.{i}.linear2",
)
# Final layer.
add_lora_layer_if_present("proj_out", "final_layer.linear")
# Assert that all keys were processed.
assert len(grouped_state_dict) == 0
return LoRAModelRaw(layers=layers)
def _group_by_layer(state_dict: Dict[str, torch.Tensor]) -> dict[str, dict[str, torch.Tensor]]:
"""Groups the keys in the state dict by layer."""
layer_dict: dict[str, dict[str, torch.Tensor]] = {}
for key in state_dict:
# Split the 'lora_A.weight' or 'lora_B.weight' suffix from the layer name.
parts = key.rsplit(".", maxsplit=2)
layer_name = parts[0]
key_name = ".".join(parts[1:])
if layer_name not in layer_dict:
layer_dict[layer_name] = {}
layer_dict[layer_name][key_name] = state_dict[key]
return layer_dict

View File

@@ -1,80 +0,0 @@
import re
from typing import Any, Dict, TypeVar
import torch
from invokeai.backend.lora.layers.any_lora_layer import AnyLoRALayer
from invokeai.backend.lora.layers.utils import any_lora_layer_from_state_dict
from invokeai.backend.lora.lora_model_raw import LoRAModelRaw
# A regex pattern that matches all of the keys in the Kohya FLUX LoRA format.
# Example keys:
# lora_unet_double_blocks_0_img_attn_proj.alpha
# lora_unet_double_blocks_0_img_attn_proj.lora_down.weight
# lora_unet_double_blocks_0_img_attn_proj.lora_up.weight
FLUX_KOHYA_KEY_REGEX = (
r"lora_unet_(\w+_blocks)_(\d+)_(img_attn|img_mlp|img_mod|txt_attn|txt_mlp|txt_mod|linear1|linear2|modulation)_?(.*)"
)
def is_state_dict_likely_in_flux_kohya_format(state_dict: Dict[str, Any]) -> bool:
"""Checks if the provided state dict is likely in the Kohya FLUX LoRA format.
This is intended to be a high-precision detector, but it is not guaranteed to have perfect precision. (A
perfect-precision detector would require checking all keys against a whitelist and verifying tensor shapes.)
"""
return all(re.match(FLUX_KOHYA_KEY_REGEX, k) for k in state_dict.keys())
def lora_model_from_flux_kohya_state_dict(state_dict: Dict[str, torch.Tensor]) -> LoRAModelRaw:
# Group keys by layer.
grouped_state_dict: dict[str, dict[str, torch.Tensor]] = {}
for key, value in state_dict.items():
layer_name, param_name = key.split(".", 1)
if layer_name not in grouped_state_dict:
grouped_state_dict[layer_name] = {}
grouped_state_dict[layer_name][param_name] = value
# Convert the state dict to the InvokeAI format.
grouped_state_dict = convert_flux_kohya_state_dict_to_invoke_format(grouped_state_dict)
# Create LoRA layers.
layers: dict[str, AnyLoRALayer] = {}
for layer_key, layer_state_dict in grouped_state_dict.items():
layers[layer_key] = any_lora_layer_from_state_dict(layer_state_dict)
# Create and return the LoRAModelRaw.
return LoRAModelRaw(layers=layers)
T = TypeVar("T")
def convert_flux_kohya_state_dict_to_invoke_format(state_dict: Dict[str, T]) -> Dict[str, T]:
"""Converts a state dict from the Kohya FLUX LoRA format to LoRA weight format used internally by InvokeAI.
Example key conversions:
"lora_unet_double_blocks_0_img_attn_proj" -> "double_blocks.0.img_attn.proj"
"lora_unet_double_blocks_0_img_attn_proj" -> "double_blocks.0.img_attn.proj"
"lora_unet_double_blocks_0_img_attn_proj" -> "double_blocks.0.img_attn.proj"
"lora_unet_double_blocks_0_img_attn_qkv" -> "double_blocks.0.img_attn.qkv"
"lora_unet_double_blocks_0_img_attn_qkv" -> "double_blocks.0.img.attn.qkv"
"lora_unet_double_blocks_0_img_attn_qkv" -> "double_blocks.0.img.attn.qkv"
"""
def replace_func(match: re.Match[str]) -> str:
s = f"{match.group(1)}.{match.group(2)}.{match.group(3)}"
if match.group(4):
s += f".{match.group(4)}"
return s
converted_dict: dict[str, T] = {}
for k, v in state_dict.items():
match = re.match(FLUX_KOHYA_KEY_REGEX, k)
if match:
new_key = re.sub(FLUX_KOHYA_KEY_REGEX, replace_func, k)
converted_dict[new_key] = v
else:
raise ValueError(f"Key '{k}' does not match the expected pattern for FLUX LoRA weights.")
return converted_dict

View File

@@ -1,29 +0,0 @@
from typing import Dict
import torch
from invokeai.backend.lora.layers.any_lora_layer import AnyLoRALayer
from invokeai.backend.lora.layers.utils import any_lora_layer_from_state_dict
from invokeai.backend.lora.lora_model_raw import LoRAModelRaw
def lora_model_from_sd_state_dict(state_dict: Dict[str, torch.Tensor]) -> LoRAModelRaw:
grouped_state_dict: dict[str, dict[str, torch.Tensor]] = _group_state(state_dict)
layers: dict[str, AnyLoRALayer] = {}
for layer_key, values in grouped_state_dict.items():
layers[layer_key] = any_lora_layer_from_state_dict(values)
return LoRAModelRaw(layers=layers)
def _group_state(state_dict: Dict[str, torch.Tensor]) -> Dict[str, Dict[str, torch.Tensor]]:
state_dict_groupped: Dict[str, Dict[str, torch.Tensor]] = {}
for key, value in state_dict.items():
stem, leaf = key.split(".", 1)
if stem not in state_dict_groupped:
state_dict_groupped[stem] = {}
state_dict_groupped[stem][leaf] = value
return state_dict_groupped

View File

@@ -1,154 +0,0 @@
import bisect
from typing import Dict, List, Tuple, TypeVar
T = TypeVar("T")
def convert_sdxl_keys_to_diffusers_format(state_dict: Dict[str, T]) -> dict[str, T]:
"""Convert the keys of an SDXL LoRA state_dict to diffusers format.
The input state_dict can be in either Stability AI format or diffusers format. If the state_dict is already in
diffusers format, then this function will have no effect.
This function is adapted from:
https://github.com/bmaltais/kohya_ss/blob/2accb1305979ba62f5077a23aabac23b4c37e935/networks/lora_diffusers.py#L385-L409
Args:
state_dict (Dict[str, Tensor]): The SDXL LoRA state_dict.
Raises:
ValueError: If state_dict contains an unrecognized key, or not all keys could be converted.
Returns:
Dict[str, Tensor]: The diffusers-format state_dict.
"""
converted_count = 0 # The number of Stability AI keys converted to diffusers format.
not_converted_count = 0 # The number of keys that were not converted.
# Get a sorted list of Stability AI UNet keys so that we can efficiently search for keys with matching prefixes.
# For example, we want to efficiently find `input_blocks_4_1` in the list when searching for
# `input_blocks_4_1_proj_in`.
stability_unet_keys = list(SDXL_UNET_STABILITY_TO_DIFFUSERS_MAP)
stability_unet_keys.sort()
new_state_dict: dict[str, T] = {}
for full_key, value in state_dict.items():
if full_key.startswith("lora_unet_"):
search_key = full_key.replace("lora_unet_", "")
# Use bisect to find the key in stability_unet_keys that *may* match the search_key's prefix.
position = bisect.bisect_right(stability_unet_keys, search_key)
map_key = stability_unet_keys[position - 1]
# Now, check if the map_key *actually* matches the search_key.
if search_key.startswith(map_key):
new_key = full_key.replace(map_key, SDXL_UNET_STABILITY_TO_DIFFUSERS_MAP[map_key])
new_state_dict[new_key] = value
converted_count += 1
else:
new_state_dict[full_key] = value
not_converted_count += 1
elif full_key.startswith("lora_te1_") or full_key.startswith("lora_te2_"):
# The CLIP text encoders have the same keys in both Stability AI and diffusers formats.
new_state_dict[full_key] = value
continue
else:
raise ValueError(f"Unrecognized SDXL LoRA key prefix: '{full_key}'.")
if converted_count > 0 and not_converted_count > 0:
raise ValueError(
f"The SDXL LoRA could only be partially converted to diffusers format. converted={converted_count},"
f" not_converted={not_converted_count}"
)
return new_state_dict
# code from
# https://github.com/bmaltais/kohya_ss/blob/2accb1305979ba62f5077a23aabac23b4c37e935/networks/lora_diffusers.py#L15C1-L97C32
def _make_sdxl_unet_conversion_map() -> List[Tuple[str, str]]:
"""Create a dict mapping state_dict keys from Stability AI SDXL format to diffusers SDXL format."""
unet_conversion_map_layer: list[tuple[str, str]] = []
for i in range(3): # num_blocks is 3 in sdxl
# loop over downblocks/upblocks
for j in range(2):
# loop over resnets/attentions for downblocks
hf_down_res_prefix = f"down_blocks.{i}.resnets.{j}."
sd_down_res_prefix = f"input_blocks.{3*i + j + 1}.0."
unet_conversion_map_layer.append((sd_down_res_prefix, hf_down_res_prefix))
if i < 3:
# no attention layers in down_blocks.3
hf_down_atn_prefix = f"down_blocks.{i}.attentions.{j}."
sd_down_atn_prefix = f"input_blocks.{3*i + j + 1}.1."
unet_conversion_map_layer.append((sd_down_atn_prefix, hf_down_atn_prefix))
for j in range(3):
# loop over resnets/attentions for upblocks
hf_up_res_prefix = f"up_blocks.{i}.resnets.{j}."
sd_up_res_prefix = f"output_blocks.{3*i + j}.0."
unet_conversion_map_layer.append((sd_up_res_prefix, hf_up_res_prefix))
# if i > 0: commentout for sdxl
# no attention layers in up_blocks.0
hf_up_atn_prefix = f"up_blocks.{i}.attentions.{j}."
sd_up_atn_prefix = f"output_blocks.{3*i + j}.1."
unet_conversion_map_layer.append((sd_up_atn_prefix, hf_up_atn_prefix))
if i < 3:
# no downsample in down_blocks.3
hf_downsample_prefix = f"down_blocks.{i}.downsamplers.0.conv."
sd_downsample_prefix = f"input_blocks.{3*(i+1)}.0.op."
unet_conversion_map_layer.append((sd_downsample_prefix, hf_downsample_prefix))
# no upsample in up_blocks.3
hf_upsample_prefix = f"up_blocks.{i}.upsamplers.0."
sd_upsample_prefix = f"output_blocks.{3*i + 2}.{2}." # change for sdxl
unet_conversion_map_layer.append((sd_upsample_prefix, hf_upsample_prefix))
hf_mid_atn_prefix = "mid_block.attentions.0."
sd_mid_atn_prefix = "middle_block.1."
unet_conversion_map_layer.append((sd_mid_atn_prefix, hf_mid_atn_prefix))
for j in range(2):
hf_mid_res_prefix = f"mid_block.resnets.{j}."
sd_mid_res_prefix = f"middle_block.{2*j}."
unet_conversion_map_layer.append((sd_mid_res_prefix, hf_mid_res_prefix))
unet_conversion_map_resnet = [
# (stable-diffusion, HF Diffusers)
("in_layers.0.", "norm1."),
("in_layers.2.", "conv1."),
("out_layers.0.", "norm2."),
("out_layers.3.", "conv2."),
("emb_layers.1.", "time_emb_proj."),
("skip_connection.", "conv_shortcut."),
]
unet_conversion_map: list[tuple[str, str]] = []
for sd, hf in unet_conversion_map_layer:
if "resnets" in hf:
for sd_res, hf_res in unet_conversion_map_resnet:
unet_conversion_map.append((sd + sd_res, hf + hf_res))
else:
unet_conversion_map.append((sd, hf))
for j in range(2):
hf_time_embed_prefix = f"time_embedding.linear_{j+1}."
sd_time_embed_prefix = f"time_embed.{j*2}."
unet_conversion_map.append((sd_time_embed_prefix, hf_time_embed_prefix))
for j in range(2):
hf_label_embed_prefix = f"add_embedding.linear_{j+1}."
sd_label_embed_prefix = f"label_emb.0.{j*2}."
unet_conversion_map.append((sd_label_embed_prefix, hf_label_embed_prefix))
unet_conversion_map.append(("input_blocks.0.0.", "conv_in."))
unet_conversion_map.append(("out.0.", "conv_norm_out."))
unet_conversion_map.append(("out.2.", "conv_out."))
return unet_conversion_map
SDXL_UNET_STABILITY_TO_DIFFUSERS_MAP = {
sd.rstrip(".").replace(".", "_"): hf.rstrip(".").replace(".", "_") for sd, hf in _make_sdxl_unet_conversion_map()
}

View File

@@ -1,11 +0,0 @@
from typing import Union
from invokeai.backend.lora.layers.concatenated_lora_layer import ConcatenatedLoRALayer
from invokeai.backend.lora.layers.full_layer import FullLayer
from invokeai.backend.lora.layers.ia3_layer import IA3Layer
from invokeai.backend.lora.layers.loha_layer import LoHALayer
from invokeai.backend.lora.layers.lokr_layer import LoKRLayer
from invokeai.backend.lora.layers.lora_layer import LoRALayer
from invokeai.backend.lora.layers.norm_layer import NormLayer
AnyLoRALayer = Union[LoRALayer, LoHALayer, LoKRLayer, FullLayer, IA3Layer, NormLayer, ConcatenatedLoRALayer]

View File

@@ -1,46 +0,0 @@
from typing import List, Optional
import torch
from invokeai.backend.lora.layers.lora_layer_base import LoRALayerBase
class ConcatenatedLoRALayer(LoRALayerBase):
"""A LoRA layer that is composed of multiple LoRA layers concatenated along a specified axis.
This class was created to handle a special case with FLUX LoRA models. In the BFL FLUX model format, the attention
Q, K, V matrices are concatenated along the first dimension. In the diffusers LoRA format, the Q, K, V matrices are
stored as separate tensors. This class enables diffusers LoRA layers to be used in BFL FLUX models.
"""
def __init__(self, lora_layers: List[LoRALayerBase], concat_axis: int = 0):
# Note: We pass values={} to the base class, because the values are handled by the individual LoRA layers.
super().__init__(values={})
self._lora_layers = lora_layers
self._concat_axis = concat_axis
def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor:
# TODO(ryand): Currently, we pass orig_weight=None to the sub-layers. If we want to support sub-layers that
# require this value, we will need to implement chunking of the original weight tensor here.
layer_weights = [lora_layer.get_weight(None) for lora_layer in self._lora_layers] # pyright: ignore[reportArgumentType]
return torch.cat(layer_weights, dim=self._concat_axis)
def get_bias(self, orig_bias: torch.Tensor) -> Optional[torch.Tensor]:
# TODO(ryand): Currently, we pass orig_bias=None to the sub-layers. If we want to support sub-layers that
# require this value, we will need to implement chunking of the original bias tensor here.
layer_biases = [lora_layer.get_bias(None) for lora_layer in self._lora_layers] # pyright: ignore[reportArgumentType]
layer_bias_is_none = [layer_bias is None for layer_bias in layer_biases]
if any(layer_bias_is_none):
assert all(layer_bias_is_none)
return None
# Ignore the type error, because we have just verified that all layer biases are non-None.
return torch.cat(layer_biases, dim=self._concat_axis)
def calc_size(self) -> int:
return sum(lora_layer.calc_size() for lora_layer in self._lora_layers)
def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> None:
for lora_layer in self._lora_layers:
lora_layer.to(device=device, dtype=dtype)

View File

@@ -1,36 +0,0 @@
from typing import Dict, Optional
import torch
from invokeai.backend.lora.layers.lora_layer_base import LoRALayerBase
class FullLayer(LoRALayerBase):
# bias handled in LoRALayerBase(calc_size, to)
# weight: torch.Tensor
# bias: Optional[torch.Tensor]
def __init__(
self,
values: Dict[str, torch.Tensor],
):
super().__init__(values)
self.weight = values["diff"]
self.bias = values.get("diff_b", None)
self.rank = None # unscaled
self.check_keys(values, {"diff", "diff_b"})
def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor:
return self.weight
def calc_size(self) -> int:
model_size = super().calc_size()
model_size += self.weight.nelement() * self.weight.element_size()
return model_size
def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> None:
super().to(device=device, dtype=dtype)
self.weight = self.weight.to(device=device, dtype=dtype)

View File

@@ -1,41 +0,0 @@
from typing import Dict, Optional
import torch
from invokeai.backend.lora.layers.lora_layer_base import LoRALayerBase
class IA3Layer(LoRALayerBase):
# weight: torch.Tensor
# on_input: torch.Tensor
def __init__(
self,
values: Dict[str, torch.Tensor],
):
super().__init__(values)
self.weight = values["weight"]
self.on_input = values["on_input"]
self.rank = None # unscaled
self.check_keys(values, {"weight", "on_input"})
def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor:
weight = self.weight
if not self.on_input:
weight = weight.reshape(-1, 1)
assert orig_weight is not None
return orig_weight * weight
def calc_size(self) -> int:
model_size = super().calc_size()
model_size += self.weight.nelement() * self.weight.element_size()
model_size += self.on_input.nelement() * self.on_input.element_size()
return model_size
def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None):
super().to(device=device, dtype=dtype)
self.weight = self.weight.to(device=device, dtype=dtype)
self.on_input = self.on_input.to(device=device, dtype=dtype)

View File

@@ -1,68 +0,0 @@
from typing import Dict, Optional
import torch
from invokeai.backend.lora.layers.lora_layer_base import LoRALayerBase
class LoHALayer(LoRALayerBase):
# w1_a: torch.Tensor
# w1_b: torch.Tensor
# w2_a: torch.Tensor
# w2_b: torch.Tensor
# t1: Optional[torch.Tensor] = None
# t2: Optional[torch.Tensor] = None
def __init__(self, values: Dict[str, torch.Tensor]):
super().__init__(values)
self.w1_a = values["hada_w1_a"]
self.w1_b = values["hada_w1_b"]
self.w2_a = values["hada_w2_a"]
self.w2_b = values["hada_w2_b"]
self.t1 = values.get("hada_t1", None)
self.t2 = values.get("hada_t2", None)
self.rank = self.w1_b.shape[0]
self.check_keys(
values,
{
"hada_w1_a",
"hada_w1_b",
"hada_w2_a",
"hada_w2_b",
"hada_t1",
"hada_t2",
},
)
def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor:
if self.t1 is None:
weight: torch.Tensor = (self.w1_a @ self.w1_b) * (self.w2_a @ self.w2_b)
else:
rebuild1 = torch.einsum("i j k l, j r, i p -> p r k l", self.t1, self.w1_b, self.w1_a)
rebuild2 = torch.einsum("i j k l, j r, i p -> p r k l", self.t2, self.w2_b, self.w2_a)
weight = rebuild1 * rebuild2
return weight
def calc_size(self) -> int:
model_size = super().calc_size()
for val in [self.w1_a, self.w1_b, self.w2_a, self.w2_b, self.t1, self.t2]:
if val is not None:
model_size += val.nelement() * val.element_size()
return model_size
def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> None:
super().to(device=device, dtype=dtype)
self.w1_a = self.w1_a.to(device=device, dtype=dtype)
self.w1_b = self.w1_b.to(device=device, dtype=dtype)
if self.t1 is not None:
self.t1 = self.t1.to(device=device, dtype=dtype)
self.w2_a = self.w2_a.to(device=device, dtype=dtype)
self.w2_b = self.w2_b.to(device=device, dtype=dtype)
if self.t2 is not None:
self.t2 = self.t2.to(device=device, dtype=dtype)

View File

@@ -1,113 +0,0 @@
from typing import Dict, Optional
import torch
from invokeai.backend.lora.layers.lora_layer_base import LoRALayerBase
class LoKRLayer(LoRALayerBase):
# w1: Optional[torch.Tensor] = None
# w1_a: Optional[torch.Tensor] = None
# w1_b: Optional[torch.Tensor] = None
# w2: Optional[torch.Tensor] = None
# w2_a: Optional[torch.Tensor] = None
# w2_b: Optional[torch.Tensor] = None
# t2: Optional[torch.Tensor] = None
def __init__(
self,
values: Dict[str, torch.Tensor],
):
super().__init__(values)
self.w1 = values.get("lokr_w1", None)
if self.w1 is None:
self.w1_a = values["lokr_w1_a"]
self.w1_b = values["lokr_w1_b"]
else:
self.w1_b = None
self.w1_a = None
self.w2 = values.get("lokr_w2", None)
if self.w2 is None:
self.w2_a = values["lokr_w2_a"]
self.w2_b = values["lokr_w2_b"]
else:
self.w2_a = None
self.w2_b = None
self.t2 = values.get("lokr_t2", None)
if self.w1_b is not None:
self.rank = self.w1_b.shape[0]
elif self.w2_b is not None:
self.rank = self.w2_b.shape[0]
else:
self.rank = None # unscaled
self.check_keys(
values,
{
"lokr_w1",
"lokr_w1_a",
"lokr_w1_b",
"lokr_w2",
"lokr_w2_a",
"lokr_w2_b",
"lokr_t2",
},
)
def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor:
w1: Optional[torch.Tensor] = self.w1
if w1 is None:
assert self.w1_a is not None
assert self.w1_b is not None
w1 = self.w1_a @ self.w1_b
w2 = self.w2
if w2 is None:
if self.t2 is None:
assert self.w2_a is not None
assert self.w2_b is not None
w2 = self.w2_a @ self.w2_b
else:
w2 = torch.einsum("i j k l, i p, j r -> p r k l", self.t2, self.w2_a, self.w2_b)
if len(w2.shape) == 4:
w1 = w1.unsqueeze(2).unsqueeze(2)
w2 = w2.contiguous()
assert w1 is not None
assert w2 is not None
weight = torch.kron(w1, w2)
return weight
def calc_size(self) -> int:
model_size = super().calc_size()
for val in [self.w1, self.w1_a, self.w1_b, self.w2, self.w2_a, self.w2_b, self.t2]:
if val is not None:
model_size += val.nelement() * val.element_size()
return model_size
def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> None:
super().to(device=device, dtype=dtype)
if self.w1 is not None:
self.w1 = self.w1.to(device=device, dtype=dtype)
else:
assert self.w1_a is not None
assert self.w1_b is not None
self.w1_a = self.w1_a.to(device=device, dtype=dtype)
self.w1_b = self.w1_b.to(device=device, dtype=dtype)
if self.w2 is not None:
self.w2 = self.w2.to(device=device, dtype=dtype)
else:
assert self.w2_a is not None
assert self.w2_b is not None
self.w2_a = self.w2_a.to(device=device, dtype=dtype)
self.w2_b = self.w2_b.to(device=device, dtype=dtype)
if self.t2 is not None:
self.t2 = self.t2.to(device=device, dtype=dtype)

View File

@@ -1,58 +0,0 @@
from typing import Dict, Optional
import torch
from invokeai.backend.lora.layers.lora_layer_base import LoRALayerBase
# TODO: find and debug lora/locon with bias
class LoRALayer(LoRALayerBase):
# up: torch.Tensor
# mid: Optional[torch.Tensor]
# down: torch.Tensor
def __init__(
self,
values: Dict[str, torch.Tensor],
):
super().__init__(values)
self.up = values["lora_up.weight"]
self.down = values["lora_down.weight"]
self.mid = values.get("lora_mid.weight", None)
self.rank = self.down.shape[0]
self.check_keys(
values,
{
"lora_up.weight",
"lora_down.weight",
"lora_mid.weight",
},
)
def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor:
if self.mid is not None:
up = self.up.reshape(self.up.shape[0], self.up.shape[1])
down = self.down.reshape(self.down.shape[0], self.down.shape[1])
weight = torch.einsum("m n w h, i m, n j -> i j w h", self.mid, up, down)
else:
weight = self.up.reshape(self.up.shape[0], -1) @ self.down.reshape(self.down.shape[0], -1)
return weight
def calc_size(self) -> int:
model_size = super().calc_size()
for val in [self.up, self.mid, self.down]:
if val is not None:
model_size += val.nelement() * val.element_size()
return model_size
def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> None:
super().to(device=device, dtype=dtype)
self.up = self.up.to(device=device, dtype=dtype)
self.down = self.down.to(device=device, dtype=dtype)
if self.mid is not None:
self.mid = self.mid.to(device=device, dtype=dtype)

View File

@@ -1,71 +0,0 @@
from typing import Dict, Optional, Set
import torch
import invokeai.backend.util.logging as logger
class LoRALayerBase:
# rank: Optional[int]
# alpha: Optional[float]
# bias: Optional[torch.Tensor]
# @property
# def scale(self):
# return self.alpha / self.rank if (self.alpha and self.rank) else 1.0
def __init__(
self,
values: Dict[str, torch.Tensor],
):
if "alpha" in values:
self.alpha = values["alpha"].item()
else:
self.alpha = None
if "bias_indices" in values and "bias_values" in values and "bias_size" in values:
self.bias: Optional[torch.Tensor] = torch.sparse_coo_tensor(
values["bias_indices"],
values["bias_values"],
tuple(values["bias_size"]),
)
else:
self.bias = None
self.rank = None # set in layer implementation
def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor:
raise NotImplementedError()
def get_bias(self, orig_bias: torch.Tensor) -> Optional[torch.Tensor]:
return self.bias
def get_parameters(self, orig_module: torch.nn.Module) -> Dict[str, torch.Tensor]:
params = {"weight": self.get_weight(orig_module.weight)}
bias = self.get_bias(orig_module.bias)
if bias is not None:
params["bias"] = bias
return params
def calc_size(self) -> int:
model_size = 0
for val in [self.bias]:
if val is not None:
model_size += val.nelement() * val.element_size()
return model_size
def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> None:
if self.bias is not None:
self.bias = self.bias.to(device=device, dtype=dtype)
def check_keys(self, values: Dict[str, torch.Tensor], known_keys: Set[str]):
"""Log a warning if values contains unhandled keys."""
# {"alpha", "bias_indices", "bias_values", "bias_size"} are hard-coded, because they are handled by
# `LoRALayerBase`. Sub-classes should provide the known_keys that they handled.
all_known_keys = known_keys | {"alpha", "bias_indices", "bias_values", "bias_size"}
unknown_keys = set(values.keys()) - all_known_keys
if unknown_keys:
logger.warning(
f"Unexpected keys found in LoRA/LyCORIS layer, model might work incorrectly! Keys: {unknown_keys}"
)

View File

@@ -1,36 +0,0 @@
from typing import Dict, Optional
import torch
from invokeai.backend.lora.layers.lora_layer_base import LoRALayerBase
class NormLayer(LoRALayerBase):
# bias handled in LoRALayerBase(calc_size, to)
# weight: torch.Tensor
# bias: Optional[torch.Tensor]
def __init__(
self,
values: Dict[str, torch.Tensor],
):
super().__init__(values)
self.weight = values["w_norm"]
self.bias = values.get("b_norm", None)
self.rank = None # unscaled
self.check_keys(values, {"w_norm", "b_norm"})
def get_weight(self, orig_weight: torch.Tensor) -> torch.Tensor:
return self.weight
def calc_size(self) -> int:
model_size = super().calc_size()
model_size += self.weight.nelement() * self.weight.element_size()
return model_size
def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> None:
super().to(device=device, dtype=dtype)
self.weight = self.weight.to(device=device, dtype=dtype)

View File

@@ -1,33 +0,0 @@
from typing import Dict
import torch
from invokeai.backend.lora.layers.any_lora_layer import AnyLoRALayer
from invokeai.backend.lora.layers.full_layer import FullLayer
from invokeai.backend.lora.layers.ia3_layer import IA3Layer
from invokeai.backend.lora.layers.loha_layer import LoHALayer
from invokeai.backend.lora.layers.lokr_layer import LoKRLayer
from invokeai.backend.lora.layers.lora_layer import LoRALayer
from invokeai.backend.lora.layers.norm_layer import NormLayer
def any_lora_layer_from_state_dict(state_dict: Dict[str, torch.Tensor]) -> AnyLoRALayer:
# Detect layers according to LyCORIS detection logic(`weight_list_det`)
# https://github.com/KohakuBlueleaf/LyCORIS/tree/8ad8000efb79e2b879054da8c9356e6143591bad/lycoris/modules
if "lora_up.weight" in state_dict:
# LoRA a.k.a LoCon
return LoRALayer(state_dict)
elif "hada_w1_a" in state_dict:
return LoHALayer(state_dict)
elif "lokr_w1" in state_dict or "lokr_w1_a" in state_dict:
return LoKRLayer(state_dict)
elif "diff" in state_dict:
# Full a.k.a Diff
return FullLayer(state_dict)
elif "on_input" in state_dict:
return IA3Layer(state_dict)
elif "w_norm" in state_dict:
return NormLayer(state_dict)
else:
raise ValueError(f"Unsupported lora format: {state_dict.keys()}")

View File

@@ -1,22 +0,0 @@
# Copyright (c) 2024 The InvokeAI Development team
from typing import Dict, Optional
import torch
from invokeai.backend.lora.layers.any_lora_layer import AnyLoRALayer
from invokeai.backend.raw_model import RawModel
class LoRAModelRaw(RawModel): # (torch.nn.Module):
def __init__(self, layers: Dict[str, AnyLoRALayer]):
self.layers = layers
def to(self, device: Optional[torch.device] = None, dtype: Optional[torch.dtype] = None) -> None:
for _key, layer in self.layers.items():
layer.to(device=device, dtype=dtype)
def calc_size(self) -> int:
model_size = 0
for _, layer in self.layers.items():
model_size += layer.calc_size()
return model_size

View File

@@ -1,148 +0,0 @@
from contextlib import contextmanager
from typing import Dict, Iterable, Optional, Tuple
import torch
from invokeai.backend.lora.lora_model_raw import LoRAModelRaw
from invokeai.backend.util.devices import TorchDevice
from invokeai.backend.util.original_weights_storage import OriginalWeightsStorage
class LoraPatcher:
@staticmethod
@torch.no_grad()
@contextmanager
def apply_lora_patches(
model: torch.nn.Module,
patches: Iterable[Tuple[LoRAModelRaw, float]],
prefix: str,
cached_weights: Optional[Dict[str, torch.Tensor]] = None,
):
"""Apply one or more LoRA patches to a model within a context manager.
:param model: The model to patch.
:param loras: An iterator that returns tuples of LoRA patches and associated weights. An iterator is used so
that the LoRA patches do not need to be loaded into memory all at once.
:param prefix: The keys in the patches will be filtered to only include weights with this prefix.
:cached_weights: Read-only copy of the model's state dict in CPU, for efficient unpatching purposes.
"""
original_weights = OriginalWeightsStorage(cached_weights)
try:
for patch, patch_weight in patches:
LoraPatcher.apply_lora_patch(
model=model,
prefix=prefix,
patch=patch,
patch_weight=patch_weight,
original_weights=original_weights,
)
del patch
yield
finally:
for param_key, weight in original_weights.get_changed_weights():
model.get_parameter(param_key).copy_(weight)
@staticmethod
@torch.no_grad()
def apply_lora_patch(
model: torch.nn.Module,
prefix: str,
patch: LoRAModelRaw,
patch_weight: float,
original_weights: OriginalWeightsStorage,
):
"""
Apply a single LoRA patch to a model.
:param model: The model to patch.
:param patch: LoRA model to patch in.
:param patch_weight: LoRA patch weight.
:param prefix: A string prefix that precedes keys used in the LoRAs weight layers.
:param original_weights: Storage with original weights, filled by weights which lora patches, used for unpatching.
"""
if patch_weight == 0:
return
# If the layer keys contain a dot, then they are not flattened, and can be directly used to access model
# submodules. If the layer keys do not contain a dot, then they are flattened, meaning that all '.' have been
# replaced with '_'. Non-flattened keys are preferred, because they allow submodules to be accessed directly
# without searching, but some legacy code still uses flattened keys.
layer_keys_are_flattened = "." not in next(iter(patch.layers.keys()))
prefix_len = len(prefix)
for layer_key, layer in patch.layers.items():
if not layer_key.startswith(prefix):
continue
module_key, module = LoraPatcher._get_submodule(
model, layer_key[prefix_len:], layer_key_is_flattened=layer_keys_are_flattened
)
# All of the LoRA weight calculations will be done on the same device as the module weight.
# (Performance will be best if this is a CUDA device.)
device = module.weight.device
dtype = module.weight.dtype
layer_scale = layer.alpha / layer.rank if (layer.alpha and layer.rank) else 1.0
# We intentionally move to the target device first, then cast. Experimentally, this was found to
# be significantly faster for 16-bit CPU tensors being moved to a CUDA device than doing the
# same thing in a single call to '.to(...)'.
layer.to(device=device)
layer.to(dtype=torch.float32)
# TODO(ryand): Using torch.autocast(...) over explicit casting may offer a speed benefit on CUDA
# devices here. Experimentally, it was found to be very slow on CPU. More investigation needed.
for param_name, lora_param_weight in layer.get_parameters(module).items():
param_key = module_key + "." + param_name
module_param = module.get_parameter(param_name)
# Save original weight
original_weights.save(param_key, module_param)
if module_param.shape != lora_param_weight.shape:
lora_param_weight = lora_param_weight.reshape(module_param.shape)
lora_param_weight *= patch_weight * layer_scale
module_param += lora_param_weight.to(dtype=dtype)
layer.to(device=TorchDevice.CPU_DEVICE)
@staticmethod
def _get_submodule(
model: torch.nn.Module, layer_key: str, layer_key_is_flattened: bool
) -> tuple[str, torch.nn.Module]:
"""Get the submodule corresponding to the given layer key.
:param model: The model to search.
:param layer_key: The layer key to search for.
:param layer_key_is_flattened: Whether the layer key is flattened. If flattened, then all '.' have been replaced
with '_'. Non-flattened keys are preferred, because they allow submodules to be accessed directly without
searching, but some legacy code still uses flattened keys.
:return: A tuple containing the module key and the submodule.
"""
if not layer_key_is_flattened:
return layer_key, model.get_submodule(layer_key)
# Handle flattened keys.
assert "." not in layer_key
module = model
module_key = ""
key_parts = layer_key.split("_")
submodule_name = key_parts.pop(0)
while len(key_parts) > 0:
try:
module = module.get_submodule(submodule_name)
module_key += "." + submodule_name
submodule_name = key_parts.pop(0)
except Exception:
submodule_name += "_" + key_parts.pop(0)
module = module.get_submodule(submodule_name)
module_key = (module_key + "." + submodule_name).lstrip(".")
return module_key, module

View File

@@ -32,9 +32,7 @@ from invokeai.backend.model_manager.config import (
)
from invokeai.backend.model_manager.load.load_default import ModelLoader
from invokeai.backend.model_manager.load.model_loader_registry import ModelLoaderRegistry
from invokeai.backend.model_manager.util.model_util import (
convert_bundle_to_flux_transformer_checkpoint,
)
from invokeai.backend.model_manager.util.model_util import convert_bundle_to_flux_transformer_checkpoint
from invokeai.backend.util.silence_warnings import SilenceWarnings
try:
@@ -195,11 +193,6 @@ class FluxCheckpointModel(ModelLoader):
sd = load_file(model_path)
if "model.diffusion_model.double_blocks.0.img_attn.norm.key_norm.scale" in sd:
sd = convert_bundle_to_flux_transformer_checkpoint(sd)
new_sd_size = sum([ten.nelement() * torch.bfloat16.itemsize for ten in sd.values()])
self._ram_cache.make_room(new_sd_size)
for k in sd.keys():
# We need to cast to bfloat16 due to it being the only currently supported dtype for inference
sd[k] = sd[k].to(torch.bfloat16)
model.load_state_dict(sd, assign=True)
return model

View File

@@ -5,18 +5,8 @@ from logging import Logger
from pathlib import Path
from typing import Optional
import torch
from safetensors.torch import load_file
from invokeai.app.services.config import InvokeAIAppConfig
from invokeai.backend.lora.conversions.flux_diffusers_lora_conversion_utils import (
lora_model_from_flux_diffusers_state_dict,
)
from invokeai.backend.lora.conversions.flux_kohya_lora_conversion_utils import (
lora_model_from_flux_kohya_state_dict,
)
from invokeai.backend.lora.conversions.sd_lora_conversion_utils import lora_model_from_sd_state_dict
from invokeai.backend.lora.conversions.sdxl_lora_conversion_utils import convert_sdxl_keys_to_diffusers_format
from invokeai.backend.lora import LoRAModelRaw
from invokeai.backend.model_manager import (
AnyModel,
AnyModelConfig,
@@ -55,33 +45,14 @@ class LoRALoader(ModelLoader):
raise ValueError("There are no submodels in a LoRA model.")
model_path = Path(config.path)
assert self._model_base is not None
# Load the state dict from the model file.
if model_path.suffix == ".safetensors":
state_dict = load_file(model_path.absolute().as_posix(), device="cpu")
else:
state_dict = torch.load(model_path, map_location="cpu")
# Apply state_dict key conversions, if necessary.
if self._model_base == BaseModelType.StableDiffusionXL:
state_dict = convert_sdxl_keys_to_diffusers_format(state_dict)
model = lora_model_from_sd_state_dict(state_dict=state_dict)
elif self._model_base == BaseModelType.Flux:
if config.format == ModelFormat.Diffusers:
model = lora_model_from_flux_diffusers_state_dict(state_dict=state_dict)
elif config.format == ModelFormat.LyCORIS:
model = lora_model_from_flux_kohya_state_dict(state_dict=state_dict)
else:
raise ValueError(f"LoRA model is in unsupported FLUX format: {config.format}")
elif self._model_base in [BaseModelType.StableDiffusion1, BaseModelType.StableDiffusion2]:
# Currently, we don't apply any conversions for SD1 and SD2 LoRA models.
model = lora_model_from_sd_state_dict(state_dict=state_dict)
else:
raise ValueError(f"Unsupported LoRA base model: {self._model_base}")
model.to(dtype=self._torch_dtype)
model = LoRAModelRaw.from_checkpoint(
file_path=model_path,
dtype=self._torch_dtype,
base_model=self._model_base,
)
return model
# override
def _get_model_path(self, config: AnyModelConfig) -> Path:
# cheating a little - we remember this variable for using in the subsequent call to _load_model()
self._model_base = config.base

View File

@@ -15,7 +15,7 @@ from invokeai.backend.image_util.depth_anything.depth_anything_pipeline import D
from invokeai.backend.image_util.grounding_dino.grounding_dino_pipeline import GroundingDinoPipeline
from invokeai.backend.image_util.segment_anything.segment_anything_pipeline import SegmentAnythingPipeline
from invokeai.backend.ip_adapter.ip_adapter import IPAdapter
from invokeai.backend.lora.lora_model_raw import LoRAModelRaw
from invokeai.backend.lora import LoRAModelRaw
from invokeai.backend.model_manager.config import AnyModel
from invokeai.backend.onnx.onnx_runtime import IAIOnnxRuntimeModel
from invokeai.backend.spandrel_image_to_image_model import SpandrelImageToImageModel

View File

@@ -10,10 +10,6 @@ from picklescan.scanner import scan_file_path
import invokeai.backend.util.logging as logger
from invokeai.app.util.misc import uuid_string
from invokeai.backend.lora.conversions.flux_diffusers_lora_conversion_utils import (
is_state_dict_likely_in_flux_diffusers_format,
)
from invokeai.backend.lora.conversions.flux_kohya_lora_conversion_utils import is_state_dict_likely_in_flux_kohya_format
from invokeai.backend.model_hash.model_hash import HASHING_ALGORITHMS, ModelHash
from invokeai.backend.model_manager.config import (
AnyModelConfig,
@@ -248,9 +244,7 @@ class ModelProbe(object):
return ModelType.VAE
elif key.startswith(("lora_te_", "lora_unet_")):
return ModelType.LoRA
# "lora_A.weight" and "lora_B.weight" are associated with models in PEFT format. We don't support all PEFT
# LoRA models, but as of the time of writing, we support Diffusers FLUX PEFT LoRA models.
elif key.endswith(("to_k_lora.up.weight", "to_q_lora.down.weight", "lora_A.weight", "lora_B.weight")):
elif key.endswith(("to_k_lora.up.weight", "to_q_lora.down.weight")):
return ModelType.LoRA
elif key.startswith(("controlnet", "control_model", "input_blocks")):
return ModelType.ControlNet
@@ -560,21 +554,12 @@ class LoRACheckpointProbe(CheckpointProbeBase):
"""Class for LoRA checkpoints."""
def get_format(self) -> ModelFormat:
if is_state_dict_likely_in_flux_diffusers_format(self.checkpoint):
# TODO(ryand): This is an unusual case. In other places throughout the codebase, we treat
# ModelFormat.Diffusers as meaning that the model is in a directory. In this case, the model is a single
# file, but the weight keys are in the diffusers format.
return ModelFormat.Diffusers
return ModelFormat.LyCORIS
return ModelFormat("lycoris")
def get_base_type(self) -> BaseModelType:
if is_state_dict_likely_in_flux_kohya_format(self.checkpoint) or is_state_dict_likely_in_flux_diffusers_format(
self.checkpoint
):
return BaseModelType.Flux
checkpoint = self.checkpoint
token_vector_length = lora_token_vector_length(checkpoint)
# If we've gotten here, we assume that the model is a Stable Diffusion model.
token_vector_length = lora_token_vector_length(self.checkpoint)
if token_vector_length == 768:
return BaseModelType.StableDiffusion1
elif token_vector_length == 1024:

View File

@@ -5,18 +5,32 @@ from __future__ import annotations
import pickle
from contextlib import contextmanager
from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union
from typing import Any, Dict, Generator, Iterator, List, Optional, Tuple, Type, Union
import numpy as np
import torch
from diffusers import UNet2DConditionModel
from diffusers import OnnxRuntimeModel, UNet2DConditionModel
from transformers import CLIPTextModel, CLIPTextModelWithProjection, CLIPTokenizer
from invokeai.app.shared.models import FreeUConfig
from invokeai.backend.lora.lora_model_raw import LoRAModelRaw
from invokeai.backend.lora import LoRAModelRaw
from invokeai.backend.model_manager import AnyModel
from invokeai.backend.model_manager.load.optimizations import skip_torch_weight_init
from invokeai.backend.onnx.onnx_runtime import IAIOnnxRuntimeModel
from invokeai.backend.stable_diffusion.extensions.lora import LoRAExt
from invokeai.backend.textual_inversion import TextualInversionManager, TextualInversionModelRaw
from invokeai.backend.util.original_weights_storage import OriginalWeightsStorage
"""
loras = [
(lora_model1, 0.7),
(lora_model2, 0.4),
]
with LoRAHelper.apply_lora_unet(unet, loras):
# unet with applied loras
# unmodified unet
"""
class ModelPatcher:
@@ -40,6 +54,95 @@ class ModelPatcher:
finally:
unet.set_attn_processor(unet_orig_processors)
@staticmethod
def _resolve_lora_key(model: torch.nn.Module, lora_key: str, prefix: str) -> Tuple[str, torch.nn.Module]:
assert "." not in lora_key
if not lora_key.startswith(prefix):
raise Exception(f"lora_key with invalid prefix: {lora_key}, {prefix}")
module = model
module_key = ""
key_parts = lora_key[len(prefix) :].split("_")
submodule_name = key_parts.pop(0)
while len(key_parts) > 0:
try:
module = module.get_submodule(submodule_name)
module_key += "." + submodule_name
submodule_name = key_parts.pop(0)
except Exception:
submodule_name += "_" + key_parts.pop(0)
module = module.get_submodule(submodule_name)
module_key = (module_key + "." + submodule_name).lstrip(".")
return (module_key, module)
@classmethod
@contextmanager
def apply_lora_unet(
cls,
unet: UNet2DConditionModel,
loras: Iterator[Tuple[LoRAModelRaw, float]],
cached_weights: Optional[Dict[str, torch.Tensor]] = None,
) -> Generator[None, None, None]:
with cls.apply_lora(
unet,
loras=loras,
prefix="lora_unet_",
cached_weights=cached_weights,
):
yield
@classmethod
@contextmanager
def apply_lora_text_encoder(
cls,
text_encoder: CLIPTextModel,
loras: Iterator[Tuple[LoRAModelRaw, float]],
cached_weights: Optional[Dict[str, torch.Tensor]] = None,
) -> Generator[None, None, None]:
with cls.apply_lora(text_encoder, loras=loras, prefix="lora_te_", cached_weights=cached_weights):
yield
@classmethod
@contextmanager
def apply_lora(
cls,
model: AnyModel,
loras: Iterator[Tuple[LoRAModelRaw, float]],
prefix: str,
cached_weights: Optional[Dict[str, torch.Tensor]] = None,
) -> Generator[None, None, None]:
"""
Apply one or more LoRAs to a model.
:param model: The model to patch.
:param loras: An iterator that returns the LoRA to patch in and its patch weight.
:param prefix: A string prefix that precedes keys used in the LoRAs weight layers.
:cached_weights: Read-only copy of the model's state dict in CPU, for unpatching purposes.
"""
original_weights = OriginalWeightsStorage(cached_weights)
try:
for lora_model, lora_weight in loras:
LoRAExt.patch_model(
model=model,
prefix=prefix,
lora=lora_model,
lora_weight=lora_weight,
original_weights=original_weights,
)
del lora_model
yield
finally:
with torch.no_grad():
for param_key, weight in original_weights.get_changed_weights():
model.get_parameter(param_key).copy_(weight)
@classmethod
@contextmanager
def apply_ti(
@@ -179,6 +282,26 @@ class ModelPatcher:
class ONNXModelPatcher:
@classmethod
@contextmanager
def apply_lora_unet(
cls,
unet: OnnxRuntimeModel,
loras: Iterator[Tuple[LoRAModelRaw, float]],
) -> None:
with cls.apply_lora(unet, loras, "lora_unet_"):
yield
@classmethod
@contextmanager
def apply_lora_text_encoder(
cls,
text_encoder: OnnxRuntimeModel,
loras: List[Tuple[LoRAModelRaw, float]],
) -> None:
with cls.apply_lora(text_encoder, loras, "lora_te_"):
yield
# based on
# https://github.com/ssube/onnx-web/blob/ca2e436f0623e18b4cfe8a0363fcfcf10508acf7/api/onnx_web/convert/diffusion/lora.py#L323
@classmethod

View File

@@ -1,17 +1,18 @@
from __future__ import annotations
from contextlib import contextmanager
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Tuple
import torch
from diffusers import UNet2DConditionModel
from invokeai.backend.lora.lora_model_raw import LoRAModelRaw
from invokeai.backend.lora.lora_patcher import LoraPatcher
from invokeai.backend.stable_diffusion.extensions.base import ExtensionBase
from invokeai.backend.util.devices import TorchDevice
if TYPE_CHECKING:
from invokeai.app.invocations.model import ModelIdentifierField
from invokeai.app.services.shared.invocation_context import InvocationContext
from invokeai.backend.lora import LoRAModelRaw
from invokeai.backend.util.original_weights_storage import OriginalWeightsStorage
@@ -30,14 +31,107 @@ class LoRAExt(ExtensionBase):
@contextmanager
def patch_unet(self, unet: UNet2DConditionModel, original_weights: OriginalWeightsStorage):
lora_model = self._node_context.models.load(self._model_id).model
assert isinstance(lora_model, LoRAModelRaw)
LoraPatcher.apply_lora_patch(
self.patch_model(
model=unet,
prefix="lora_unet_",
patch=lora_model,
patch_weight=self._weight,
lora=lora_model,
lora_weight=self._weight,
original_weights=original_weights,
)
del lora_model
yield
@classmethod
@torch.no_grad()
def patch_model(
cls,
model: torch.nn.Module,
prefix: str,
lora: LoRAModelRaw,
lora_weight: float,
original_weights: OriginalWeightsStorage,
):
"""
Apply one or more LoRAs to a model.
:param model: The model to patch.
:param lora: LoRA model to patch in.
:param lora_weight: LoRA patch weight.
:param prefix: A string prefix that precedes keys used in the LoRAs weight layers.
:param original_weights: Storage with original weights, filled by weights which lora patches, used for unpatching.
"""
if lora_weight == 0:
return
# assert lora.device.type == "cpu"
for layer_key, layer in lora.layers.items():
if not layer_key.startswith(prefix):
continue
# TODO(ryand): A non-negligible amount of time is currently spent resolving LoRA keys. This
# should be improved in the following ways:
# 1. The key mapping could be more-efficiently pre-computed. This would save time every time a
# LoRA model is applied.
# 2. From an API perspective, there's no reason that the `ModelPatcher` should be aware of the
# intricacies of Stable Diffusion key resolution. It should just expect the input LoRA
# weights to have valid keys.
assert isinstance(model, torch.nn.Module)
module_key, module = cls._resolve_lora_key(model, layer_key, prefix)
# All of the LoRA weight calculations will be done on the same device as the module weight.
# (Performance will be best if this is a CUDA device.)
device = module.weight.device
dtype = module.weight.dtype
layer_scale = layer.alpha / layer.rank if (layer.alpha and layer.rank) else 1.0
# We intentionally move to the target device first, then cast. Experimentally, this was found to
# be significantly faster for 16-bit CPU tensors being moved to a CUDA device than doing the
# same thing in a single call to '.to(...)'.
layer.to(device=device)
layer.to(dtype=torch.float32)
# TODO(ryand): Using torch.autocast(...) over explicit casting may offer a speed benefit on CUDA
# devices here. Experimentally, it was found to be very slow on CPU. More investigation needed.
for param_name, lora_param_weight in layer.get_parameters(module).items():
param_key = module_key + "." + param_name
module_param = module.get_parameter(param_name)
# save original weight
original_weights.save(param_key, module_param)
if module_param.shape != lora_param_weight.shape:
# TODO: debug on lycoris
lora_param_weight = lora_param_weight.reshape(module_param.shape)
lora_param_weight *= lora_weight * layer_scale
module_param += lora_param_weight.to(dtype=dtype)
layer.to(device=TorchDevice.CPU_DEVICE)
@staticmethod
def _resolve_lora_key(model: torch.nn.Module, lora_key: str, prefix: str) -> Tuple[str, torch.nn.Module]:
assert "." not in lora_key
if not lora_key.startswith(prefix):
raise Exception(f"lora_key with invalid prefix: {lora_key}, {prefix}")
module = model
module_key = ""
key_parts = lora_key[len(prefix) :].split("_")
submodule_name = key_parts.pop(0)
while len(key_parts) > 0:
try:
module = module.get_submodule(submodule_name)
module_key += "." + submodule_name
submodule_name = key_parts.pop(0)
except Exception:
submodule_name += "_" + key_parts.pop(0)
module = module.get_submodule(submodule_name)
module_key = (module_key + "." + submodule_name).lstrip(".")
return (module_key, module)

View File

@@ -16,14 +16,6 @@ module.exports = {
'no-promise-executor-return': 'error',
// https://eslint.org/docs/latest/rules/require-await
'require-await': 'error',
'no-restricted-properties': [
'error',
{
object: 'crypto',
property: 'randomUUID',
message: 'Use of crypto.randomUUID is not allowed as it is not available in all browsers.',
},
],
},
overrides: [
/**

View File

@@ -11,8 +11,6 @@ const config: KnipConfig = {
'src/features/nodes/types/v2/**',
// TODO(psyche): maybe we can clean up these utils after canvas v2 release
'src/features/controlLayers/konva/util.ts',
// TODO(psyche): restore HRF functionality?
'src/features/hrf/**',
],
ignoreBinaries: ['only-allow'],
paths: {

View File

@@ -58,7 +58,7 @@
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fontsource-variable/inter": "^5.0.20",
"@invoke-ai/ui-library": "^0.0.33",
"@invoke-ai/ui-library": "^0.0.32",
"@nanostores/react": "^0.7.3",
"@reduxjs/toolkit": "2.2.3",
"@roarr/browser-log-writer": "^1.3.0",
@@ -92,7 +92,7 @@
"react-i18next": "^14.1.3",
"react-icons": "^5.2.1",
"react-redux": "9.1.2",
"react-resizable-panels": "^2.1.2",
"react-resizable-panels": "^2.0.23",
"react-use": "^17.5.1",
"react-virtuoso": "^4.9.0",
"reactflow": "^11.11.4",

View File

@@ -24,8 +24,8 @@ dependencies:
specifier: ^5.0.20
version: 5.0.20
'@invoke-ai/ui-library':
specifier: ^0.0.33
version: 0.0.33(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1)
specifier: ^0.0.32
version: 0.0.32(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1)
'@nanostores/react':
specifier: ^0.7.3
version: 0.7.3(nanostores@0.11.2)(react@18.3.1)
@@ -126,8 +126,8 @@ dependencies:
specifier: 9.1.2
version: 9.1.2(@types/react@18.3.3)(react@18.3.1)(redux@5.0.1)
react-resizable-panels:
specifier: ^2.1.2
version: 2.1.2(react-dom@18.3.1)(react@18.3.1)
specifier: ^2.0.23
version: 2.0.23(react-dom@18.3.1)(react@18.3.1)
react-use:
specifier: ^17.5.1
version: 17.5.1(react-dom@18.3.1)(react@18.3.1)
@@ -2056,7 +2056,7 @@ packages:
dependencies:
'@chakra-ui/dom-utils': 2.1.0
react: 18.3.1
react-focus-lock: 2.13.2(@types/react@18.3.3)(react@18.3.1)
react-focus-lock: 2.13.0(@types/react@18.3.3)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
dev: false
@@ -2253,7 +2253,7 @@ packages:
framer-motion: 10.18.0(react-dom@18.3.1)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-remove-scroll: 2.6.0(@types/react@18.3.3)(react@18.3.1)
react-remove-scroll: 2.5.10(@types/react@18.3.3)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
dev: false
@@ -3574,8 +3574,8 @@ packages:
prettier: 3.3.3
dev: true
/@invoke-ai/ui-library@0.0.33(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-YLydTCOTUEgju4Ex6yXt/bvNBcO97y6zc1cGYjt7vtJMS8e6deA89cC5JejjbmVgntdnn49cDyeUxB8Z24gZew==}
/@invoke-ai/ui-library@0.0.32(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-JxAoblrDu/cZ4ha9KO4ry5OWvyLUE1Dj28i+ciMaDNUpC/cN+IyiTbUBoFoPaoN5JP8Zpd/MYCcmF2qsziHDzg==}
peerDependencies:
'@fontsource-variable/inter': ^5.0.16
react: ^18.2.0
@@ -10011,8 +10011,8 @@ packages:
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
dev: false
/react-focus-lock@2.13.2(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-T/7bsofxYqnod2xadvuwjGKHOoL5GH7/EIPI5UyEvaU/c2CcphvGI371opFtuY/SYdbMsNiuF4HsHQ50nA/TKQ==}
/react-focus-lock@2.13.0(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-w7aIcTwZwNzUp2fYQDMICy+khFwVmKmOrLF8kNsPS+dz4Oi/oxoVJ2wCMVvX6rWGriM/+mYaTyp1MRmkcs2amw==}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -10147,6 +10147,25 @@ packages:
tslib: 2.7.0
dev: false
/react-remove-scroll@2.5.10(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-m3zvBRANPBw3qxVVjEIPEQinkcwlFZ4qyomuWVpNJdv4c6MvHfXV0C3L9Jx5rr3HeBHKNRX+1jreB5QloDIJjA==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.3.3
react: 18.3.1
react-remove-scroll-bar: 2.3.6(@types/react@18.3.3)(react@18.3.1)
react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1)
tslib: 2.7.0
use-callback-ref: 1.3.2(@types/react@18.3.3)(react@18.3.1)
use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1)
dev: false
/react-remove-scroll@2.5.5(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==}
engines: {node: '>=10'}
@@ -10166,27 +10185,8 @@ packages:
use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1)
dev: false
/react-remove-scroll@2.6.0(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==}
engines: {node: '>=10'}
peerDependencies:
'@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.3.3
react: 18.3.1
react-remove-scroll-bar: 2.3.6(@types/react@18.3.3)(react@18.3.1)
react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1)
tslib: 2.7.0
use-callback-ref: 1.3.2(@types/react@18.3.3)(react@18.3.1)
use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1)
dev: false
/react-resizable-panels@2.1.2(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-Ku2Bo7JvE8RpHhl4X1uhkdeT9auPBoxAOlGTqomDUUrBAX2mVGuHYZTcWvlnJSgx0QyHIxHECgGB5XVPUbUOkQ==}
/react-resizable-panels@2.0.23(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-8ZKTwTU11t/FYwiwhMdtZYYyFxic5U5ysRu2YwfkAgDbUJXFvnWSJqhnzkSlW+mnDoNAzDCrJhdOSXBPA76wug==}
peerDependencies:
react: ^16.14.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0

View File

@@ -127,14 +127,7 @@
"bulkDownloadRequestedDesc": "Dein Download wird vorbereitet. Dies kann ein paar Momente dauern.",
"bulkDownloadRequestFailed": "Problem beim Download vorbereiten",
"bulkDownloadFailed": "Download fehlgeschlagen",
"alwaysShowImageSizeBadge": "Zeige immer Bilder Größe Abzeichen",
"selectForCompare": "Zum Vergleichen auswählen",
"compareImage": "Bilder vergleichen",
"exitSearch": "Suche beenden",
"newestFirst": "Neueste zuerst",
"oldestFirst": "Älteste zuerst",
"openInViewer": "Im Viewer öffnen",
"swapImages": "Bilder tauschen"
"alwaysShowImageSizeBadge": "Zeige immer Bilder Größe Abzeichen"
},
"hotkeys": {
"keyboardShortcuts": "Tastenkürzel",
@@ -638,8 +631,7 @@
"archived": "Archiviert",
"noBoards": "Kein {boardType}} Ordner",
"hideBoards": "Ordner verstecken",
"viewBoards": "Ordner ansehen",
"deletedPrivateBoardsCannotbeRestored": "Gelöschte Boards können nicht wiederhergestellt werden. Wenn Sie „Nur Board löschen“ wählen, werden die Bilder in einen privaten, nicht kategorisierten Status für den Ersteller des Bildes versetzt."
"viewBoards": "Ordner ansehen"
},
"controlnet": {
"showAdvanced": "Zeige Erweitert",
@@ -789,9 +781,7 @@
"batchFieldValues": "Stapelverarbeitungswerte",
"batchQueued": "Stapelverarbeitung eingereiht",
"graphQueued": "Graph eingereiht",
"graphFailedToQueue": "Fehler beim Einreihen des Graphen",
"generations_one": "Generation",
"generations_other": "Generationen"
"graphFailedToQueue": "Fehler beim Einreihen des Graphen"
},
"metadata": {
"negativePrompt": "Negativ Beschreibung",
@@ -1156,10 +1146,5 @@
"noMatchingTriggers": "Keine passenden Trigger",
"addPromptTrigger": "Prompt-Trigger hinzufügen",
"compatibleEmbeddings": "Kompatible Einbettungen"
},
"ui": {
"tabs": {
"queue": "Warteschlange"
}
}
}

View File

@@ -93,7 +93,6 @@
"copy": "Copy",
"copyError": "$t(gallery.copy) Error",
"on": "On",
"off": "Off",
"or": "or",
"checkpoint": "Checkpoint",
"communityLabel": "Community",
@@ -135,7 +134,6 @@
"nodes": "Workflows",
"notInstalled": "Not $t(common.installed)",
"openInNewTab": "Open in New Tab",
"openInViewer": "Open in Viewer",
"orderBy": "Order By",
"outpaint": "outpaint",
"outputs": "Outputs",
@@ -375,7 +373,6 @@
"useCache": "Use Cache"
},
"gallery": {
"gallery": "Gallery",
"alwaysShowImageSizeBadge": "Always Show Image Size Badge",
"assets": "Assets",
"autoAssignBoardOnClick": "Auto-Assign Board on Click",
@@ -388,11 +385,11 @@
"deleteImage_one": "Delete Image",
"deleteImage_other": "Delete {{count}} Images",
"deleteImagePermanent": "Deleted images cannot be restored.",
"displayBoardSearch": "Board Search",
"displaySearch": "Image Search",
"displayBoardSearch": "Display Board Search",
"displaySearch": "Display Search",
"download": "Download",
"exitBoardSearch": "Exit Board Search",
"exitSearch": "Exit Image Search",
"exitSearch": "Exit Search",
"featuresWillReset": "If you delete this image, those features will immediately be reset.",
"galleryImageSize": "Image Size",
"gallerySettings": "Gallery Settings",
@@ -438,8 +435,7 @@
"compareHelp1": "Hold <Kbd>Alt</Kbd> while clicking a gallery image or using the arrow keys to change the compare image.",
"compareHelp2": "Press <Kbd>M</Kbd> to cycle through comparison modes.",
"compareHelp3": "Press <Kbd>C</Kbd> to swap the compared images.",
"compareHelp4": "Press <Kbd>Z</Kbd> or <Kbd>Esc</Kbd> to exit.",
"toggleMiniViewer": "Toggle Mini Viewer"
"compareHelp4": "Press <Kbd>Z</Kbd> or <Kbd>Esc</Kbd> to exit."
},
"hotkeys": {
"searchHotkeys": "Search Hotkeys",
@@ -1018,8 +1014,6 @@
"noModelForControlAdapter": "Control Adapter #{{number}} has no model selected.",
"incompatibleBaseModelForControlAdapter": "Control Adapter #{{number}} model is incompatible with main model.",
"noModelSelected": "No model selected",
"canvasManagerNotLoaded": "Canvas Manager not loaded",
"canvasBusy": "Canvas is busy",
"noPrompts": "No prompts generated",
"noNodesInGraph": "No nodes in graph",
"systemDisconnected": "System disconnected",
@@ -1051,11 +1045,12 @@
"scaledHeight": "Scaled H",
"scaledWidth": "Scaled W",
"scheduler": "Scheduler",
"seamlessXAxis": "Seamless X Axis",
"seamlessYAxis": "Seamless Y Axis",
"seamlessXAxis": "Seamless Tiling X Axis",
"seamlessYAxis": "Seamless Tiling Y Axis",
"seed": "Seed",
"imageActions": "Image Actions",
"sendToCanvas": "Send To Canvas",
"sendToImg2Img": "Send to Image to Image",
"sendToUnifiedCanvas": "Send To Unified Canvas",
"sendToUpscale": "Send To Upscale",
"showOptionsPanel": "Show Side Panel (O or T)",
"shuffle": "Shuffle Seed",
@@ -1196,8 +1191,8 @@
"problemSavingMaskDesc": "Unable to export mask",
"prunedQueue": "Pruned Queue",
"resetInitialImage": "Reset Initial Image",
"sentToCanvas": "Sent to Canvas",
"sentToUpscale": "Sent to Upscale",
"sentToImageToImage": "Sent To Image To Image",
"sentToUnifiedCanvas": "Sent to Unified Canvas",
"serverError": "Server Error",
"sessionRef": "Session: {{sessionId}}",
"setAsCanvasInitialImage": "Set as canvas initial image",
@@ -1660,7 +1655,6 @@
},
"controlLayers": {
"bookmark": "Bookmark for Quick Switch",
"fitBboxToLayers": "Fit Bbox To Layers",
"removeBookmark": "Remove Bookmark",
"saveCanvasToGallery": "Save Canvas To Gallery",
"saveBboxToGallery": "Save Bbox To Gallery",
@@ -1716,8 +1710,6 @@
"inpaintMask": "Inpaint Mask",
"regionalGuidance": "Regional Guidance",
"ipAdapter": "IP Adapter",
"sendingToCanvas": "Sending to Canvas",
"sendingToGallery": "Sending to Gallery",
"sendToGallery": "Send To Gallery",
"sendToGalleryDesc": "Generations will be sent to the gallery.",
"sendToCanvas": "Send To Canvas",
@@ -1797,19 +1789,9 @@
"filter": "Filter",
"filters": "Filters",
"filterType": "Filter Type",
"autoProcess": "Auto Process",
"reset": "Reset",
"process": "Process",
"preview": "Preview",
"apply": "Apply",
"cancel": "Cancel",
"spandrel": {
"label": "Image-to-Image Model",
"description": "Run an image-to-image model on the selected layer.",
"paramModel": "Model",
"paramAutoScale": "Auto Scale",
"paramAutoScaleDesc": "The selected model will be run until the target scale is reached.",
"paramScale": "Target Scale"
}
"cancel": "Cancel"
},
"transform": {
"transform": "Transform",
@@ -1817,28 +1799,6 @@
"reset": "Reset",
"apply": "Apply",
"cancel": "Cancel"
},
"settings": {
"snapToGrid": {
"label": "Snap to Grid",
"on": "On",
"off": "Off"
}
},
"HUD": {
"bbox": "Bbox",
"scaledBbox": "Scaled Bbox",
"autoSave": "Auto Save",
"entityStatus": {
"selectedEntity": "Selected Entity",
"selectedEntityIs": "Selected Entity is",
"isFiltering": "is filtering",
"isTransforming": "is transforming",
"isLocked": "is locked",
"isHidden": "is hidden",
"isDisabled": "is disabled",
"enabled": "Enabled"
}
}
},
"upscaling": {

View File

@@ -86,15 +86,15 @@
"loadMore": "Cargar más",
"noImagesInGallery": "No hay imágenes para mostrar",
"deleteImage_one": "Eliminar Imagen",
"deleteImage_many": "Eliminar {{count}} Imágenes",
"deleteImage_other": "Eliminar {{count}} Imágenes",
"deleteImage_many": "",
"deleteImage_other": "",
"deleteImagePermanent": "Las imágenes eliminadas no se pueden restaurar.",
"assets": "Activos",
"autoAssignBoardOnClick": "Asignación automática de tableros al hacer clic"
},
"hotkeys": {
"keyboardShortcuts": "Atajos de teclado",
"appHotkeys": "Atajos de aplicación",
"appHotkeys": "Atajos de applicación",
"generalHotkeys": "Atajos generales",
"galleryHotkeys": "Atajos de galería",
"unifiedCanvasHotkeys": "Atajos de lienzo unificado",
@@ -535,7 +535,7 @@
"bottomMessage": "Al eliminar este panel y las imágenes que contiene, se restablecerán las funciones que los estén utilizando actualmente.",
"deleteBoardAndImages": "Borrar el panel y las imágenes",
"loading": "Cargando...",
"deletedBoardsCannotbeRestored": "Los paneles eliminados no se pueden restaurar. Al Seleccionar 'Borrar Solo el Panel' transferirá las imágenes a un estado sin categorizar.",
"deletedBoardsCannotbeRestored": "Los paneles eliminados no se pueden restaurar",
"move": "Mover",
"menuItemAutoAdd": "Agregar automáticamente a este panel",
"searchBoard": "Buscando paneles…",
@@ -549,13 +549,7 @@
"imagesWithCount_other": "{{count}} imágenes",
"assetsWithCount_one": "{{count}} activo",
"assetsWithCount_many": "{{count}} activos",
"assetsWithCount_other": "{{count}} activos",
"hideBoards": "Ocultar Paneles",
"addPrivateBoard": "Agregar un tablero privado",
"addSharedBoard": "Agregar Panel Compartido",
"boards": "Paneles",
"archiveBoard": "Archivar Panel",
"archived": "Archivado"
"assetsWithCount_other": "{{count}} activos"
},
"accordions": {
"compositing": {

View File

@@ -496,9 +496,7 @@
"main": "Principali",
"noModelsInstalledDesc1": "Installa i modelli con",
"ipAdapters": "Adattatori IP",
"noMatchingModels": "Nessun modello corrispondente",
"starterModelsInModelManager": "I modelli iniziali possono essere trovati in Gestione Modelli",
"spandrelImageToImage": "Immagine a immagine (Spandrel)"
"noMatchingModels": "Nessun modello corrispondente"
},
"parameters": {
"images": "Immagini",
@@ -512,7 +510,7 @@
"perlinNoise": "Rumore Perlin",
"type": "Tipo",
"strength": "Forza",
"upscaling": "Amplia",
"upscaling": "Ampliamento",
"scale": "Scala",
"imageFit": "Adatta l'immagine iniziale alle dimensioni di output",
"scaleBeforeProcessing": "Scala prima dell'elaborazione",
@@ -595,7 +593,7 @@
"globalPositivePromptPlaceholder": "Prompt positivo globale",
"globalNegativePromptPlaceholder": "Prompt negativo globale",
"processImage": "Elabora Immagine",
"sendToUpscale": "Invia a Amplia",
"sendToUpscale": "Invia a Ampliare",
"postProcessing": "Post-elaborazione (Shift + U)"
},
"settings": {
@@ -1422,7 +1420,7 @@
"paramUpscaleMethod": {
"heading": "Metodo di ampliamento",
"paragraphs": [
"Metodo utilizzato per ampliare l'immagine per la correzione ad alta risoluzione."
"Metodo utilizzato per eseguire l'ampliamento dell'immagine per la correzione ad alta risoluzione."
]
},
"patchmatchDownScaleSize": {
@@ -1530,7 +1528,7 @@
},
"upscaleModel": {
"paragraphs": [
"Il modello di ampliamento, scala l'immagine alle dimensioni di uscita prima di aggiungere i dettagli. È possibile utilizzare qualsiasi modello di ampliamento supportato, ma alcuni sono specializzati per diversi tipi di immagini, come foto o disegni al tratto."
"Il modello di ampliamento (Upscale), scala l'immagine alle dimensioni di uscita prima di aggiungere i dettagli. È possibile utilizzare qualsiasi modello di ampliamento supportato, ma alcuni sono specializzati per diversi tipi di immagini, come foto o disegni al tratto."
],
"heading": "Modello di ampliamento"
},
@@ -1722,27 +1720,26 @@
"modelsTab": "$t(ui.tabs.models) $t(common.tab)",
"queue": "Coda",
"queueTab": "$t(ui.tabs.queue) $t(common.tab)",
"upscaling": "Amplia",
"upscaling": "Ampliamento",
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)"
}
},
"upscaling": {
"creativity": "Creatività",
"structure": "Struttura",
"upscaleModel": "Modello di ampliamento",
"upscaleModel": "Modello di Ampliamento",
"scale": "Scala",
"missingModelsWarning": "Visita <LinkComponent>Gestione modelli</LinkComponent> per installare i modelli richiesti:",
"mainModelDesc": "Modello principale (architettura SD1.5 o SDXL)",
"tileControlNetModelDesc": "Modello Tile ControlNet per l'architettura del modello principale scelto",
"upscaleModelDesc": "Modello per l'ampliamento (immagine a immagine)",
"upscaleModelDesc": "Modello per l'ampliamento (da immagine a immagine)",
"missingUpscaleInitialImage": "Immagine iniziale mancante per l'ampliamento",
"missingUpscaleModel": "Modello per lampliamento mancante",
"missingTileControlNetModel": "Nessun modello ControlNet Tile valido installato",
"postProcessingModel": "Modello di post-elaborazione",
"postProcessingMissingModelWarning": "Visita <LinkComponent>Gestione modelli</LinkComponent> per installare un modello di post-elaborazione (da immagine a immagine).",
"exceedsMaxSize": "Le impostazioni di ampliamento superano il limite massimo delle dimensioni",
"exceedsMaxSizeDetails": "Il limite massimo di ampliamento è {{maxUpscaleDimension}}x{{maxUpscaleDimension}} pixel. Prova un'immagine più piccola o diminuisci la scala selezionata.",
"upscale": "Amplia"
"exceedsMaxSizeDetails": "Il limite massimo di ampliamento è {{maxUpscaleDimension}}x{{maxUpscaleDimension}} pixel. Prova un'immagine più piccola o diminuisci la scala selezionata."
},
"upsell": {
"inviteTeammates": "Invita collaboratori",
@@ -1792,7 +1789,6 @@
"positivePromptColumn": "'prompt' o 'positive_prompt'",
"noTemplates": "Nessun modello",
"acceptedColumnsKeys": "Colonne/chiavi accettate:",
"templateActions": "Azioni modello",
"promptTemplateCleared": "Modello di prompt cancellato"
"templateActions": "Azioni modello"
}
}

View File

@@ -501,8 +501,7 @@
"noModelsInstalled": "Нет установленных моделей",
"noModelsInstalledDesc1": "Установите модели с помощью",
"noMatchingModels": "Нет подходящих моделей",
"ipAdapters": "IP адаптеры",
"starterModelsInModelManager": "Стартовые модели можно найти в Менеджере моделей"
"ipAdapters": "IP адаптеры"
},
"parameters": {
"images": "Изображения",
@@ -1759,8 +1758,7 @@
"postProcessingModel": "Модель постобработки",
"tileControlNetModelDesc": "Модель ControlNet для выбранной архитектуры основной модели",
"missingModelsWarning": "Зайдите в <LinkComponent>Менеджер моделей</LinkComponent> чтоб установить необходимые модели:",
"postProcessingMissingModelWarning": "Посетите <LinkComponent>Менеджер моделей</LinkComponent>, чтобы установить модель постобработки (img2img).",
"upscale": "Увеличить"
"postProcessingMissingModelWarning": "Посетите <LinkComponent>Менеджер моделей</LinkComponent>, чтобы установить модель постобработки (img2img)."
},
"stylePresets": {
"noMatchingTemplates": "Нет подходящих шаблонов",
@@ -1806,8 +1804,7 @@
"noTemplates": "Нет шаблонов",
"promptTemplatesDesc2": "Используйте строку-заполнитель <Pre>{{placeholder}}</Pre>, чтобы указать место, куда должен быть включен ваш запрос в шаблоне.",
"searchByName": "Поиск по имени",
"shared": "Общий",
"promptTemplateCleared": "Шаблон запроса создан"
"shared": "Общий"
},
"upsell": {
"inviteTeammates": "Пригласите членов команды",

View File

@@ -154,8 +154,7 @@
"displaySearch": "显示搜索",
"stretchToFit": "拉伸以适应",
"exitCompare": "退出对比",
"compareHelp1": "在点击图库中的图片或使用箭头键切换比较图片时,请按住<Kbd>Alt</Kbd> 键。",
"go": "运行"
"compareHelp1": "在点击图库中的图片或使用箭头键切换比较图片时,请按住<Kbd>Alt</Kbd> 键。"
},
"hotkeys": {
"keyboardShortcuts": "快捷键",
@@ -495,9 +494,7 @@
"huggingFacePlaceholder": "所有者或模型名称",
"huggingFaceRepoID": "HuggingFace仓库ID",
"loraTriggerPhrases": "LoRA 触发词",
"ipAdapters": "IP适配器",
"spandrelImageToImage": "图生图(Spandrel)",
"starterModelsInModelManager": "您可以在模型管理器中找到初始模型"
"ipAdapters": "IP适配器"
},
"parameters": {
"images": "图像",
@@ -698,9 +695,7 @@
"outOfMemoryErrorDesc": "您当前的生成设置已超出系统处理能力.请调整设置后再次尝试.",
"parametersSet": "参数已恢复",
"errorCopied": "错误信息已复制",
"modelImportCanceled": "模型导入已取消",
"importFailed": "导入失败",
"importSuccessful": "导入成功"
"modelImportCanceled": "模型导入已取消"
},
"unifiedCanvas": {
"layer": "图层",
@@ -1710,55 +1705,12 @@
"missingModelsWarning": "请访问<LinkComponent>模型管理器</LinkComponent> 安装所需的模型:",
"mainModelDesc": "主模型SD1.5或SDXL架构",
"exceedsMaxSize": "放大设置超出了最大尺寸限制",
"exceedsMaxSizeDetails": "最大放大限制是 {{maxUpscaleDimension}}x{{maxUpscaleDimension}} 像素.请尝试一个较小的图像或减少您的缩放选择.",
"upscale": "放大"
"exceedsMaxSizeDetails": "最大放大限制是 {{maxUpscaleDimension}}x{{maxUpscaleDimension}} 像素.请尝试一个较小的图像或减少您的缩放选择."
},
"upsell": {
"inviteTeammates": "邀请团队成员",
"professional": "专业",
"professionalUpsell": "可在 Invoke 的专业版中使用.点击此处或访问 invoke.com/pricing 了解更多详情.",
"shareAccess": "共享访问权限"
},
"stylePresets": {
"positivePrompt": "正向提示词",
"preview": "预览",
"deleteImage": "删除图像",
"deleteTemplate": "删除模版",
"deleteTemplate2": "您确定要删除这个模板吗?请注意,删除后无法恢复.",
"importTemplates": "导入提示模板支持CSV或JSON格式",
"insertPlaceholder": "插入一个占位符",
"myTemplates": "我的模版",
"name": "名称",
"type": "类型",
"unableToDeleteTemplate": "无法删除提示模板",
"updatePromptTemplate": "更新提示词模版",
"exportPromptTemplates": "导出我的提示模板为CSV格式",
"exportDownloaded": "导出已下载",
"noMatchingTemplates": "无匹配的模版",
"promptTemplatesDesc1": "提示模板可以帮助您在编写提示时添加预设的文本内容.",
"promptTemplatesDesc3": "如果您没有使用占位符,那么模板的内容将会被添加到您提示的末尾.",
"searchByName": "按名称搜索",
"shared": "已分享",
"sharedTemplates": "已分享的模版",
"templateActions": "模版操作",
"templateDeleted": "提示模版已删除",
"toggleViewMode": "切换显示模式",
"uploadImage": "上传图像",
"active": "激活",
"choosePromptTemplate": "选择提示词模板",
"clearTemplateSelection": "清除模版选择",
"copyTemplate": "拷贝模版",
"createPromptTemplate": "创建提示词模版",
"defaultTemplates": "默认模版",
"editTemplate": "编辑模版",
"exportFailed": "无法生成并下载CSV文件",
"flatten": "将选定的模板内容合并到当前提示中",
"negativePrompt": "反向提示词",
"promptTemplateCleared": "提示模板已清除",
"useForTemplate": "用于提示词模版",
"viewList": "预览模版列表",
"viewModeTooltip": "这是您的提示在当前选定的模板下的预览效果。如需编辑提示,请直接在文本框中点击进行修改.",
"noTemplates": "无模版",
"private": "私密"
}
}

View File

@@ -21,16 +21,10 @@ function ThemeLocaleProvider({ children }: ThemeLocaleProviderProps) {
direction,
shadows: {
..._theme.shadows,
selected:
'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-500), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)',
hoverSelected:
'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-400), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)',
hoverUnselected:
'inset 0px 0px 0px 2px var(--invoke-colors-invokeBlue-300), inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-800)',
selectedForCompare:
'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-300), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)',
'0px 0px 0px 1px var(--invoke-colors-base-900), 0px 0px 0px 4px var(--invoke-colors-green-400)',
hoverSelectedForCompare:
'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-200), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)',
'0px 0px 0px 1px var(--invoke-colors-base-900), 0px 0px 0px 4px var(--invoke-colors-green-300)',
},
});
}, [direction]);

View File

@@ -1,5 +1,5 @@
import type { TypedStartListening } from '@reduxjs/toolkit';
import { addListener, createListenerMiddleware } from '@reduxjs/toolkit';
import { createListenerMiddleware } from '@reduxjs/toolkit';
import { addAdHocPostProcessingRequestedListener } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
import { addStagingListeners } from 'app/store/middleware/listenerMiddleware/listeners/addCommitStagingAreaImageListener';
import { addAnyEnqueuedListener } from 'app/store/middleware/listenerMiddleware/listeners/anyEnqueued';
@@ -9,7 +9,6 @@ import { addBatchEnqueuedListener } from 'app/store/middleware/listenerMiddlewar
import { addDeleteBoardAndImagesFulfilledListener } from 'app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted';
import { addBoardIdSelectedListener } from 'app/store/middleware/listenerMiddleware/listeners/boardIdSelected';
import { addBulkDownloadListeners } from 'app/store/middleware/listenerMiddleware/listeners/bulkDownload';
import { addCancellationsListeners } from 'app/store/middleware/listenerMiddleware/listeners/cancellationsListeners';
import { addEnqueueRequestedLinear } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedLinear';
import { addEnqueueRequestedNodes } from 'app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes';
import { addGalleryImageClickedListener } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked';
@@ -41,8 +40,6 @@ export type AppStartListening = TypedStartListening<RootState, AppDispatch>;
const startAppListening = listenerMiddleware.startListening as AppStartListening;
export const addAppListener = addListener.withTypes<RootState, AppDispatch>();
/**
* The RTK listener middleware is a lightweight alternative sagas/observables.
*
@@ -122,5 +119,3 @@ addDynamicPromptsListener(startAppListening);
addSetDefaultSettingsListener(startAppListening);
// addControlAdapterPreprocessor(startAppListening);
addCancellationsListeners(startAppListening);

View File

@@ -1,34 +1,37 @@
import { isAnyOf } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { canvasReset, rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
import { stagingAreaImageAccepted, stagingAreaReset } from 'features/controlLayers/store/canvasStagingAreaSlice';
import {
sessionStagingAreaImageAccepted,
sessionStagingAreaReset,
} from 'features/controlLayers/store/canvasSessionSlice';
import { rasterLayerAdded } from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import type { CanvasRasterLayerState } from 'features/controlLayers/store/types';
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { queueApi } from 'services/api/endpoints/queue';
import { $lastCanvasProgressEvent } from 'services/events/setEventListeners';
import { assert } from 'tsafe';
const log = logger('canvas');
const matchCanvasOrStagingAreaRest = isAnyOf(stagingAreaReset, canvasReset);
export const addStagingListeners = (startAppListening: AppStartListening) => {
startAppListening({
matcher: matchCanvasOrStagingAreaRest,
actionCreator: sessionStagingAreaReset,
effect: async (_, { dispatch }) => {
try {
const req = dispatch(
queueApi.endpoints.cancelByBatchDestination.initiate(
{ destination: 'canvas' },
queueApi.endpoints.cancelByBatchOrigin.initiate(
{ origin: 'canvas' },
{ fixedCacheKey: 'cancelByBatchOrigin' }
)
);
const { canceled } = await req.unwrap();
req.reset();
$lastCanvasProgressEvent.set(null);
if (canceled > 0) {
log.debug(`Canceled ${canceled} canvas batches`);
toast({
@@ -49,11 +52,11 @@ export const addStagingListeners = (startAppListening: AppStartListening) => {
});
startAppListening({
actionCreator: stagingAreaImageAccepted,
actionCreator: sessionStagingAreaImageAccepted,
effect: (action, api) => {
const { index } = action.payload;
const state = api.getState();
const stagingAreaImage = state.canvasStagingArea.stagedImages[index];
const stagingAreaImage = state.canvasSession.stagedImages[index];
assert(stagingAreaImage, 'No staged image found to accept');
const { x, y } = selectCanvasSlice(state).bbox.rect;
@@ -66,7 +69,7 @@ export const addStagingListeners = (startAppListening: AppStartListening) => {
};
api.dispatch(rasterLayerAdded({ overrides, isSelected: false }));
api.dispatch(stagingAreaReset());
api.dispatch(sessionStagingAreaReset());
},
});
};

View File

@@ -1,137 +0,0 @@
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { $lastCanvasProgressEvent } from 'features/controlLayers/store/canvasSlice';
import { queueApi } from 'services/api/endpoints/queue';
/**
* To prevent a race condition where a progress event arrives after a successful cancellation, we need to keep track of
* cancellations:
* - In the route handlers above, we track and update the cancellations object
* - When the user queues a, we should reset the cancellations, also handled int he route handlers above
* - When we get a progress event, we should check if the event is cancelled before setting the event
*
* We have a few ways that cancellations are effected, so we need to track them all:
* - by queue item id (in this case, we will compare the session_id and not the item_id)
* - by batch id
* - by destination
* - by clearing the queue
*/
type Cancellations = {
sessionIds: Set<string>;
batchIds: Set<string>;
destinations: Set<string>;
clearQueue: boolean;
};
const resetCancellations = (): void => {
cancellations.clearQueue = false;
cancellations.sessionIds.clear();
cancellations.batchIds.clear();
cancellations.destinations.clear();
};
const cancellations: Cancellations = {
sessionIds: new Set(),
batchIds: new Set(),
destinations: new Set(),
clearQueue: false,
} as Readonly<Cancellations>;
/**
* Checks if an item is cancelled, used to prevent race conditions with event handling.
*
* To use this, provide the session_id, batch_id and destination from the event payload.
*/
export const getIsCancelled = (item: {
session_id: string;
batch_id: string;
destination?: string | null;
}): boolean => {
if (cancellations.clearQueue) {
return true;
}
if (cancellations.sessionIds.has(item.session_id)) {
return true;
}
if (cancellations.batchIds.has(item.batch_id)) {
return true;
}
if (item.destination && cancellations.destinations.has(item.destination)) {
return true;
}
return false;
};
export const addCancellationsListeners = (startAppListening: AppStartListening) => {
// When we get a cancellation, we may need to clear the last progress event - next few listeners handle those cases.
// Maybe we could use the `getIsCancelled` util here, but I think that could introduce _another_ race condition...
startAppListening({
matcher: queueApi.endpoints.enqueueBatch.matchFulfilled,
effect: () => {
resetCancellations();
},
});
startAppListening({
matcher: queueApi.endpoints.cancelByBatchDestination.matchFulfilled,
effect: (action) => {
cancellations.destinations.add(action.meta.arg.originalArgs.destination);
const event = $lastCanvasProgressEvent.get();
if (!event) {
return;
}
const { session_id, batch_id, destination } = event;
if (getIsCancelled({ session_id, batch_id, destination })) {
$lastCanvasProgressEvent.set(null);
}
},
});
startAppListening({
matcher: queueApi.endpoints.cancelQueueItem.matchFulfilled,
effect: (action) => {
cancellations.sessionIds.add(action.payload.session_id);
const event = $lastCanvasProgressEvent.get();
if (!event) {
return;
}
const { session_id, batch_id, destination } = event;
if (getIsCancelled({ session_id, batch_id, destination })) {
$lastCanvasProgressEvent.set(null);
}
},
});
startAppListening({
matcher: queueApi.endpoints.cancelByBatchIds.matchFulfilled,
effect: (action) => {
for (const batch_id of action.meta.arg.originalArgs.batch_ids) {
cancellations.batchIds.add(batch_id);
}
const event = $lastCanvasProgressEvent.get();
if (!event) {
return;
}
const { session_id, batch_id, destination } = event;
if (getIsCancelled({ session_id, batch_id, destination })) {
$lastCanvasProgressEvent.set(null);
}
},
});
startAppListening({
matcher: queueApi.endpoints.clearQueue.matchFulfilled,
effect: () => {
cancellations.clearQueue = true;
const event = $lastCanvasProgressEvent.get();
if (!event) {
return;
}
const { session_id, batch_id, destination } = event;
if (getIsCancelled({ session_id, batch_id, destination })) {
$lastCanvasProgressEvent.set(null);
}
},
});
};

View File

@@ -4,12 +4,8 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'
import type { SerializableObject } from 'common/types';
import type { Result } from 'common/util/result';
import { isErr, withResult, withResultAsync } from 'common/util/result';
import { $canvasManager } from 'features/controlLayers/store/canvasSlice';
import {
selectIsStaging,
stagingAreaReset,
stagingAreaStartedStaging,
} from 'features/controlLayers/store/canvasStagingAreaSlice';
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
import { sessionStagingAreaReset, sessionStartedStaging } from 'features/controlLayers/store/canvasSessionSlice';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildSD1Graph } from 'features/nodes/util/graph/generation/buildSD1Graph';
import { buildSDXLGraph } from 'features/nodes/util/graph/generation/buildSDXLGraph';
@@ -35,14 +31,14 @@ export const addEnqueueRequestedLinear = (startAppListening: AppStartListening)
let didStartStaging = false;
if (!selectIsStaging(state) && state.canvasSettings.sendToCanvas) {
dispatch(stagingAreaStartedStaging());
if (!state.canvasSession.isStaging && state.canvasSettings.sendToCanvas) {
dispatch(sessionStartedStaging());
didStartStaging = true;
}
const abortStaging = () => {
if (didStartStaging && selectIsStaging(getState())) {
dispatch(stagingAreaReset());
if (didStartStaging && getState().canvasSession.isStaging) {
dispatch(sessionStagingAreaReset());
}
};

View File

@@ -1,5 +1,6 @@
import { enqueueRequested } from 'app/store/actions';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
import { prepareLinearUIBatch } from 'features/nodes/util/graph/buildLinearBatchConfig';
import { buildMultidiffusionUpscaleGraph } from 'features/nodes/util/graph/buildMultidiffusionUpscaleGraph';
import { queueApi } from 'services/api/endpoints/queue';
@@ -10,6 +11,7 @@ export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening)
enqueueRequested.match(action) && action.payload.tabName === 'upscaling',
effect: async (action, { getState, dispatch }) => {
const state = getState();
const { shouldShowProgressInViewer } = state.ui;
const { prepend } = action.payload;
const { g, noise, posCond } = await buildMultidiffusionUpscaleGraph(state);
@@ -23,6 +25,9 @@ export const addEnqueueRequestedUpscale = (startAppListening: AppStartListening)
);
try {
await req.unwrap();
if (shouldShowProgressInViewer) {
dispatch(isImageViewerOpenChanged(true));
}
} finally {
req.reset();
}

View File

@@ -17,7 +17,7 @@ import type { ImageDTO } from 'services/api/types';
const log = logger('gallery');
//TODO(psyche): handle image deletion (canvas staging area?)
//TODO(psyche): handle image deletion (canvas sessions?)
// Some utils to delete images from different parts of the app
const deleteNodesImages = (state: RootState, dispatch: AppDispatch, imageDTO: ImageDTO) => {

View File

@@ -1,7 +1,6 @@
import { createAction } from '@reduxjs/toolkit';
import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { selectDefaultControlAdapter } from 'features/controlLayers/hooks/addLayerHooks';
import {
controlLayerAdded,
ipaImageChanged,
@@ -13,7 +12,7 @@ import type { CanvasControlLayerState, CanvasRasterLayerState } from 'features/c
import { imageDTOToImageObject } from 'features/controlLayers/store/types';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { isValidDrop } from 'features/dnd/util/isValidDrop';
import { imageToCompareChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
import { imageToCompareChanged, isImageViewerOpenChanged, selectionChanged } from 'features/gallery/store/gallerySlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
import { imagesApi } from 'services/api/endpoints/images';
@@ -104,14 +103,11 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
activeData.payloadType === 'IMAGE_DTO' &&
activeData.payload.imageDTO
) {
const state = getState();
const imageObject = imageDTOToImageObject(activeData.payload.imageDTO);
const { x, y } = selectCanvasSlice(state).bbox.rect;
const defaultControlAdapter = selectDefaultControlAdapter(state);
const { x, y } = selectCanvasSlice(getState()).bbox.rect;
const overrides: Partial<CanvasControlLayerState> = {
objects: [imageObject],
position: { x, y },
controlAdapter: defaultControlAdapter,
};
dispatch(controlLayerAdded({ overrides, isSelected: true }));
return;
@@ -146,6 +142,7 @@ export const addImageDroppedListener = (startAppListening: AppStartListening) =>
) {
const { imageDTO } = activeData.payload;
dispatch(imageToCompareChanged(imageDTO));
dispatch(isImageViewerOpenChanged(true));
return;
}

View File

@@ -2,7 +2,6 @@ import { logger } from 'app/logging/logger';
import type { AppStartListening } from 'app/store/middleware/listenerMiddleware';
import { ipaImageChanged, rgIPAdapterImageChanged } from 'features/controlLayers/store/canvasSlice';
import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors';
import { boardIdSelected, galleryViewChanged } from 'features/gallery/store/gallerySlice';
import { fieldImageValueChanged } from 'features/nodes/store/nodesSlice';
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
import { toast } from 'features/toast/toast';
@@ -45,8 +44,6 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
if (!autoAddBoardId || autoAddBoardId === 'none') {
const title = postUploadAction.title || DEFAULT_UPLOADED_TOAST.title;
toast({ ...DEFAULT_UPLOADED_TOAST, title });
dispatch(boardIdSelected({ boardId: 'none' }));
dispatch(galleryViewChanged('assets'));
} else {
// Add this image to the board
dispatch(
@@ -70,8 +67,6 @@ export const addImageUploadedFulfilledListener = (startAppListening: AppStartLis
...DEFAULT_UPLOADED_TOAST,
description,
});
dispatch(boardIdSelected({ boardId: autoAddBoardId }));
dispatch(galleryViewChanged('assets'));
}
return;
}

View File

@@ -13,7 +13,7 @@ import { loraDeleted } from 'features/controlLayers/store/lorasSlice';
import { modelChanged, refinerModelChanged, vaeSelected } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { getEntityIdentifier } from 'features/controlLayers/store/types';
import { calculateNewSize } from 'features/parameters/components/Bbox/calculateNewSize';
import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize';
import { postProcessingModelChanged, upscaleModelChanged } from 'features/parameters/store/upscaleSlice';
import { zParameterModel, zParameterVAEModel } from 'features/parameters/types/parameterSchemas';
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';

View File

@@ -6,12 +6,9 @@ import { errorHandler } from 'app/store/enhancers/reduxRemember/errors';
import type { SerializableObject } from 'common/types';
import { deepClone } from 'common/util/deepClone';
import { changeBoardModalSlice } from 'features/changeBoardModal/store/slice';
import { canvasSessionPersistConfig, canvasSessionSlice } from 'features/controlLayers/store/canvasSessionSlice';
import { canvasSettingsPersistConfig, canvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
import { canvasPersistConfig, canvasSlice, canvasUndoableConfig } from 'features/controlLayers/store/canvasSlice';
import {
canvasStagingAreaPersistConfig,
canvasStagingAreaSlice,
} from 'features/controlLayers/store/canvasStagingAreaSlice';
import { lorasPersistConfig, lorasSlice } from 'features/controlLayers/store/lorasSlice';
import { paramsPersistConfig, paramsSlice } from 'features/controlLayers/store/paramsSlice';
import { deleteImageModalSlice } from 'features/deleteImageModal/store/slice';
@@ -66,7 +63,7 @@ const allReducers = {
[stylePresetSlice.name]: stylePresetSlice.reducer,
[paramsSlice.name]: paramsSlice.reducer,
[canvasSettingsSlice.name]: canvasSettingsSlice.reducer,
[canvasStagingAreaSlice.name]: canvasStagingAreaSlice.reducer,
[canvasSessionSlice.name]: canvasSessionSlice.reducer,
[lorasSlice.name]: lorasSlice.reducer,
};
@@ -111,7 +108,7 @@ const persistConfigs: { [key in keyof typeof allReducers]?: PersistConfig } = {
[stylePresetPersistConfig.name]: stylePresetPersistConfig,
[paramsPersistConfig.name]: paramsPersistConfig,
[canvasSettingsPersistConfig.name]: canvasSettingsPersistConfig,
[canvasStagingAreaPersistConfig.name]: canvasStagingAreaPersistConfig,
[canvasSessionPersistConfig.name]: canvasSessionPersistConfig,
[lorasPersistConfig.name]: lorasPersistConfig,
};

View File

@@ -6,51 +6,18 @@ import { useImageUploadButton } from 'common/hooks/useImageUploadButton';
import type { TypesafeDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import ImageContextMenu from 'features/gallery/components/ImageContextMenu/ImageContextMenu';
import type { MouseEvent, ReactElement, ReactNode, SyntheticEvent } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import { PiImageBold, PiUploadSimpleBold } from 'react-icons/pi';
import type { ImageDTO, PostUploadAction } from 'services/api/types';
import IAIDraggable from './IAIDraggable';
import IAIDroppable from './IAIDroppable';
import SelectionOverlay from './SelectionOverlay';
const defaultUploadElement = <Icon as={PiUploadSimpleBold} boxSize={16} />;
const defaultNoContentFallback = <IAINoContentFallback icon={PiImageBold} />;
const sx: SystemStyleObject = {
'.gallery-image-container::before': {
content: '""',
display: 'inline-block',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
pointerEvents: 'none',
borderRadius: 'base',
},
'&[data-selected="selected"]>.gallery-image-container::before': {
boxShadow:
'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-500), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)',
},
'&[data-selected="selectedForCompare"]>.gallery-image-container::before': {
boxShadow:
'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-300), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)',
},
'&:hover>.gallery-image-container::before': {
boxShadow:
'inset 0px 0px 0px 2px var(--invoke-colors-invokeBlue-300), inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-800)',
},
'&:hover[data-selected="selected"]>.gallery-image-container::before': {
boxShadow:
'inset 0px 0px 0px 3px var(--invoke-colors-invokeBlue-400), inset 0px 0px 0px 4px var(--invoke-colors-invokeBlue-800)',
},
'&:hover[data-selected="selectedForCompare"]>.gallery-image-container::before': {
boxShadow:
'inset 0px 0px 0px 3px var(--invoke-colors-invokeGreen-200), inset 0px 0px 0px 4px var(--invoke-colors-invokeGreen-800)',
},
};
type IAIDndImageProps = FlexProps & {
imageDTO: ImageDTO | undefined;
onError?: (event: SyntheticEvent<HTMLImageElement>) => void;
@@ -108,11 +75,13 @@ const IAIDndImage = (props: IAIDndImageProps) => {
...rest
} = props;
const [isHovered, setIsHovered] = useState(false);
const handleMouseOver = useCallback(
(e: MouseEvent<HTMLDivElement>) => {
if (onMouseOver) {
onMouseOver(e);
}
setIsHovered(true);
},
[onMouseOver]
);
@@ -121,6 +90,7 @@ const IAIDndImage = (props: IAIDndImageProps) => {
if (onMouseOut) {
onMouseOut(e);
}
setIsHovered(false);
},
[onMouseOut]
);
@@ -171,13 +141,10 @@ const IAIDndImage = (props: IAIDndImageProps) => {
minH={minSize ? minSize : undefined}
userSelect="none"
cursor={isDragDisabled || !imageDTO ? 'default' : 'pointer'}
sx={withHoverOverlay ? sx : undefined}
data-selected={isSelectedForCompare ? 'selectedForCompare' : isSelected ? 'selected' : undefined}
{...rest}
>
{imageDTO && (
<Flex
className="gallery-image-container"
w="full"
h="full"
position={fitContainer ? 'absolute' : 'relative'}
@@ -200,6 +167,11 @@ const IAIDndImage = (props: IAIDndImageProps) => {
data-testid={dataTestId}
/>
{withMetadataOverlay && <ImageMetadataOverlay imageDTO={imageDTO} />}
<SelectionOverlay
isSelected={isSelected}
isSelectedForCompare={isSelectedForCompare}
isHovered={withHoverOverlay ? isHovered : false}
/>
</Flex>
)}
{!imageDTO && !isUploadDisabled && (

View File

@@ -0,0 +1,104 @@
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box, Flex, IconButton, Tooltip, useToken } from '@invoke-ai/ui-library';
import type { ReactElement, ReactNode } from 'react';
import { memo, useCallback, useMemo } from 'react';
type IconSwitchProps = {
isChecked: boolean;
onChange: (checked: boolean) => void;
iconChecked: ReactElement;
tooltipChecked?: ReactNode;
iconUnchecked: ReactElement;
tooltipUnchecked?: ReactNode;
ariaLabel: string;
};
const getSx = (padding: string | number): SystemStyleObject => ({
transition: 'left 0.1s ease-in-out, transform 0.1s ease-in-out',
'&[data-checked="true"]': {
left: `calc(100% - ${padding})`,
transform: 'translateX(-100%)',
},
'&[data-checked="false"]': {
left: padding,
transform: 'translateX(0)',
},
});
export const IconSwitch = memo(
({
isChecked,
onChange,
iconChecked,
tooltipChecked,
iconUnchecked,
tooltipUnchecked,
ariaLabel,
}: IconSwitchProps) => {
const onUncheck = useCallback(() => {
onChange(false);
}, [onChange]);
const onCheck = useCallback(() => {
onChange(true);
}, [onChange]);
const gap = useToken('space', 1.5);
const sx = useMemo(() => getSx(gap), [gap]);
return (
<Flex
position="relative"
bg="base.800"
borderRadius="base"
alignItems="center"
justifyContent="center"
h="full"
p={gap}
gap={gap}
>
<Box
position="absolute"
borderRadius="base"
bg="invokeBlue.400"
w={12}
top={gap}
bottom={gap}
data-checked={isChecked}
sx={sx}
/>
<Tooltip hasArrow label={tooltipUnchecked}>
<IconButton
size="sm"
fontSize={16}
icon={iconUnchecked}
onClick={onUncheck}
variant={!isChecked ? 'solid' : 'ghost'}
colorScheme={!isChecked ? 'invokeBlue' : 'base'}
aria-label={ariaLabel}
data-checked={!isChecked}
w={12}
alignSelf="stretch"
h="auto"
/>
</Tooltip>
<Tooltip hasArrow label={tooltipChecked}>
<IconButton
size="sm"
fontSize={16}
icon={iconChecked}
onClick={onCheck}
variant={isChecked ? 'solid' : 'ghost'}
colorScheme={isChecked ? 'invokeBlue' : 'base'}
aria-label={ariaLabel}
data-checked={isChecked}
w={12}
alignSelf="stretch"
h="auto"
/>
</Tooltip>
</Flex>
);
}
);
IconSwitch.displayName = 'IconSwitch';

View File

@@ -0,0 +1,46 @@
import { Box } from '@invoke-ai/ui-library';
import { memo, useMemo } from 'react';
type Props = {
isSelected: boolean;
isSelectedForCompare: boolean;
isHovered: boolean;
};
const SelectionOverlay = ({ isSelected, isSelectedForCompare, isHovered }: Props) => {
const shadow = useMemo(() => {
if (isSelectedForCompare && isHovered) {
return 'hoverSelectedForCompare';
}
if (isSelectedForCompare && !isHovered) {
return 'selectedForCompare';
}
if (isSelected && isHovered) {
return 'hoverSelected';
}
if (isSelected && !isHovered) {
return 'selected';
}
if (!isSelected && isHovered) {
return 'hoverUnselected';
}
return undefined;
}, [isHovered, isSelected, isSelectedForCompare]);
return (
<Box
className="selection-box"
position="absolute"
top={0}
insetInlineEnd={0}
bottom={0}
insetInlineStart={0}
borderRadius="base"
opacity={isSelected || isSelectedForCompare ? 1 : 0.7}
transitionProperty="common"
transitionDuration="0.1s"
pointerEvents="none"
shadow={shadow}
/>
);
};
export default memo(SelectionOverlay);

View File

@@ -114,13 +114,4 @@ export const useGlobalHotkeys = () => {
},
[dispatch, isModelManagerEnabled]
);
useHotkeys(
isModelManagerEnabled ? '6' : '5',
() => {
dispatch(setActiveTab('gallery'));
setScopes([]);
},
[dispatch, isModelManagerEnabled]
);
};

View File

@@ -2,7 +2,6 @@ import { useStore } from '@nanostores/react';
import { $isConnected } from 'app/hooks/useSocketIO';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppSelector } from 'app/store/storeHooks';
import { useCanvasManagerSafe } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
@@ -18,7 +17,6 @@ import { selectSystemSlice } from 'features/system/store/systemSlice';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import i18n from 'i18next';
import { forEach, upperFirst } from 'lodash-es';
import { atom } from 'nanostores';
import { useMemo } from 'react';
import { getConnectedEdges } from 'reactflow';
@@ -30,7 +28,7 @@ const LAYER_TYPE_TO_TKEY = {
control_layer: 'controlLayers.globalControlAdapter',
} as const;
const createSelector = (templates: Templates, isConnected: boolean, canvasIsBusy: boolean) =>
const createSelector = (templates: Templates, isConnected: boolean) =>
createMemoizedSelector(
[
selectSystemSlice,
@@ -119,10 +117,6 @@ const createSelector = (templates: Templates, isConnected: boolean, canvasIsBusy
reasons.push({ content: i18n.t('upscaling.missingTileControlNetModel') });
}
} else {
if (canvasIsBusy) {
reasons.push({ content: i18n.t('parameters.invoke.canvasBusy') });
}
if (dynamicPrompts.prompts.length === 0 && getShouldProcessPrompt(positivePrompt)) {
reasons.push({ content: i18n.t('parameters.invoke.noPrompts') });
}
@@ -246,17 +240,10 @@ const createSelector = (templates: Templates, isConnected: boolean, canvasIsBusy
}
);
const dummyAtom = atom(true);
export const useIsReadyToEnqueue = () => {
const templates = useStore($templates);
const isConnected = useStore($isConnected);
const canvasManager = useCanvasManagerSafe();
const canvasIsBusy = useStore(canvasManager?.$isBusy ?? dummyAtom);
const selector = useMemo(
() => createSelector(templates, isConnected, canvasIsBusy),
[templates, isConnected, canvasIsBusy]
);
const selector = useMemo(() => createSelector(templates, isConnected), [templates, isConnected]);
const value = useAppSelector(selector);
return value;
};

View File

@@ -1,9 +1,6 @@
export const roundDownToMultiple = (num: number, multiple: number): number => {
return Math.floor(num / multiple) * multiple;
};
export const roundUpToMultiple = (num: number, multiple: number): number => {
return Math.ceil(num / multiple) * multiple;
};
export const roundToMultiple = (num: number, multiple: number): number => {
return Math.round(num / multiple) * multiple;

View File

@@ -1,22 +1,34 @@
import { Button, ButtonGroup, Flex } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import {
useAddControlLayer,
useAddInpaintMask,
useAddIPAdapter,
useAddRasterLayer,
useAddRegionalGuidance,
} from 'features/controlLayers/hooks/addLayerHooks';
import { memo } from 'react';
controlLayerAdded,
inpaintMaskAdded,
ipaAdded,
rasterLayerAdded,
rgAdded,
} from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
export const CanvasAddEntityButtons = memo(() => {
const { t } = useTranslation();
const addInpaintMask = useAddInpaintMask();
const addRegionalGuidance = useAddRegionalGuidance();
const addRasterLayer = useAddRasterLayer();
const addControlLayer = useAddControlLayer();
const addIPAdapter = useAddIPAdapter();
const dispatch = useAppDispatch();
const addInpaintMask = useCallback(() => {
dispatch(inpaintMaskAdded({ isSelected: true }));
}, [dispatch]);
const addRegionalGuidance = useCallback(() => {
dispatch(rgAdded({ isSelected: true }));
}, [dispatch]);
const addRasterLayer = useCallback(() => {
dispatch(rasterLayerAdded({ isSelected: true }));
}, [dispatch]);
const addControlLayer = useCallback(() => {
dispatch(controlLayerAdded({ isSelected: true }));
}, [dispatch]);
const addIPAdapter = useCallback(() => {
dispatch(ipaAdded({ isSelected: true }));
}, [dispatch]);
return (
<Flex flexDir="column" w="full" h="full" alignItems="center">

View File

@@ -1,6 +1,7 @@
import { Flex } from '@invoke-ai/ui-library';
import IAIDroppable from 'common/components/IAIDroppable';
import type { AddControlLayerFromImageDropData, AddRasterLayerFromImageDropData } from 'features/dnd/types';
import { useIsImageViewerOpen } from 'features/gallery/components/ImageViewer/useImageViewer';
import { memo } from 'react';
const addRasterLayerFromImageDropData: AddRasterLayerFromImageDropData = {
@@ -14,6 +15,12 @@ const addControlLayerFromImageDropData: AddControlLayerFromImageDropData = {
};
export const CanvasDropArea = memo(() => {
const isImageViewerOpen = useIsImageViewerOpen();
if (isImageViewerOpen) {
return null;
}
return (
<>
<Flex position="absolute" top={0} right={0} bottom="50%" left={0} gap={2} pointerEvents="none">

View File

@@ -0,0 +1,24 @@
import { Flex } from '@invoke-ai/ui-library';
import type { Meta, StoryObj } from '@storybook/react';
import { CanvasEditor } from 'features/controlLayers/components/CanvasEditor';
const meta: Meta<typeof CanvasEditor> = {
title: 'Feature/ControlLayers',
tags: ['autodocs'],
component: CanvasEditor,
};
export default meta;
type Story = StoryObj<typeof CanvasEditor>;
const Component = () => {
return (
<Flex w={1500} h={1500}>
<CanvasEditor />
</Flex>
);
};
export const Default: Story = {
render: Component,
};

View File

@@ -11,7 +11,7 @@ import { Transform } from 'features/controlLayers/components/Transform/Transform
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { memo, useRef } from 'react';
export const CanvasTabContent = memo(() => {
export const CanvasEditor = memo(() => {
const ref = useRef<HTMLDivElement>(null);
useScopeOnFocus('canvas', ref);
@@ -48,4 +48,4 @@ export const CanvasTabContent = memo(() => {
);
});
CanvasTabContent.displayName = 'CanvasTabContent';
CanvasEditor.displayName = 'CanvasEditor';

View File

@@ -0,0 +1,20 @@
import { Flex, Spacer } from '@invoke-ai/ui-library';
import { EntityListActionBarAddLayerButton } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuButton';
import { EntityListActionBarDeleteButton } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarDeleteButton';
import { EntityListActionBarSelectedEntityFill } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityFill';
import { SelectedEntityOpacity } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityOpacity';
import { memo } from 'react';
export const EntityListActionBar = memo(() => {
return (
<Flex w="full" py={1} px={1} gap={2} alignItems="center">
<SelectedEntityOpacity />
<Spacer />
<EntityListActionBarSelectedEntityFill />
<EntityListActionBarAddLayerButton />
<EntityListActionBarDeleteButton />
</Flex>
);
});
EntityListActionBar.displayName = 'EntityListActionBar';

View File

@@ -0,0 +1,28 @@
import { IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
import { CanvasEntityListMenuItems } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
export const EntityListActionBarAddLayerButton = memo(() => {
const { t } = useTranslation();
return (
<Menu>
<MenuButton
as={IconButton}
size="sm"
tooltip={t('controlLayers.addLayer')}
aria-label={t('controlLayers.addLayer')}
icon={<PiPlusBold />}
variant="ghost"
data-testid="control-layers-add-layer-menu-button"
/>
<MenuList>
<CanvasEntityListMenuItems />
</MenuList>
</Menu>
);
});
EntityListActionBarAddLayerButton.displayName = 'EntityListActionBarAddLayerButton';

View File

@@ -0,0 +1,57 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useDefaultIPAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter';
import {
controlLayerAdded,
inpaintMaskAdded,
ipaAdded,
rasterLayerAdded,
rgAdded,
} from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
export const CanvasEntityListMenuItems = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const defaultIPAdapter = useDefaultIPAdapter();
const addInpaintMask = useCallback(() => {
dispatch(inpaintMaskAdded({ isSelected: true }));
}, [dispatch]);
const addRegionalGuidance = useCallback(() => {
dispatch(rgAdded({ isSelected: true }));
}, [dispatch]);
const addRasterLayer = useCallback(() => {
dispatch(rasterLayerAdded({ isSelected: true }));
}, [dispatch]);
const addControlLayer = useCallback(() => {
dispatch(controlLayerAdded({ isSelected: true }));
}, [dispatch]);
const addIPAdapter = useCallback(() => {
const overrides = { ipAdapter: defaultIPAdapter };
dispatch(ipaAdded({ isSelected: true, overrides }));
}, [defaultIPAdapter, dispatch]);
return (
<>
<MenuItem icon={<PiPlusBold />} onClick={addInpaintMask}>
{t('controlLayers.inpaintMask')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addRegionalGuidance}>
{t('controlLayers.regionalGuidance')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addRasterLayer}>
{t('controlLayers.rasterLayer')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addControlLayer}>
{t('controlLayers.controlLayer')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addIPAdapter}>
{t('controlLayers.globalIPAdapter')}
</MenuItem>
</>
);
});
CanvasEntityListMenuItems.displayName = 'CanvasEntityListMenu';

View File

@@ -0,0 +1,39 @@
import { IconButton, useShiftModifier } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { allEntitiesDeleted, entityDeleted } from 'features/controlLayers/store/canvasSlice';
import { selectEntityCount, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiTrashSimpleFill } from 'react-icons/pi';
export const EntityListActionBarDeleteButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const entityCount = useAppSelector(selectEntityCount);
const shift = useShiftModifier();
const onClick = useCallback(() => {
if (shift) {
dispatch(allEntitiesDeleted());
return;
}
if (!selectedEntityIdentifier) {
return;
}
dispatch(entityDeleted({ entityIdentifier: selectedEntityIdentifier }));
}, [dispatch, selectedEntityIdentifier, shift]);
return (
<IconButton
onClick={onClick}
isDisabled={shift ? entityCount === 0 : !selectedEntityIdentifier}
size="sm"
variant="ghost"
aria-label={shift ? t('controlLayers.deleteAll') : t('controlLayers.deleteSelected')}
tooltip={shift ? t('controlLayers.deleteAll') : t('controlLayers.deleteSelected')}
icon={<PiTrashSimpleFill />}
/>
);
});
EntityListActionBarDeleteButton.displayName = 'EntityListActionBarDeleteButton';

View File

@@ -9,7 +9,7 @@ import { type FillStyle, isMaskEntityIdentifier, type RgbColor } from 'features/
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const EntityListSelectedEntityActionBarFill = memo(() => {
export const EntityListActionBarSelectedEntityFill = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
@@ -67,4 +67,4 @@ export const EntityListSelectedEntityActionBarFill = memo(() => {
);
});
EntityListSelectedEntityActionBarFill.displayName = 'EntityListSelectedEntityActionBarFill';
EntityListActionBarSelectedEntityFill.displayName = 'EntityListActionBarSelectedEntityFill';

View File

@@ -77,7 +77,7 @@ const selectOpacity = createSelector(selectCanvasSlice, (canvas) => {
return selectedEntity.opacity;
});
export const EntityListSelectedEntityActionBarOpacity = memo(() => {
export const SelectedEntityOpacity = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
@@ -193,4 +193,4 @@ export const EntityListSelectedEntityActionBarOpacity = memo(() => {
);
});
EntityListSelectedEntityActionBarOpacity.displayName = 'EntityListSelectedEntityActionBarOpacity';
SelectedEntityOpacity.displayName = 'SelectedEntityOpacity';

View File

@@ -1,54 +0,0 @@
import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import {
useAddControlLayer,
useAddInpaintMask,
useAddIPAdapter,
useAddRasterLayer,
useAddRegionalGuidance,
} from 'features/controlLayers/hooks/addLayerHooks';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
export const EntityListGlobalActionBarAddLayerMenu = memo(() => {
const { t } = useTranslation();
const addInpaintMask = useAddInpaintMask();
const addRegionalGuidance = useAddRegionalGuidance();
const addRasterLayer = useAddRasterLayer();
const addControlLayer = useAddControlLayer();
const addIPAdapter = useAddIPAdapter();
return (
<Menu>
<MenuButton
as={IconButton}
size="sm"
variant="link"
alignSelf="stretch"
tooltip={t('controlLayers.addLayer')}
aria-label={t('controlLayers.addLayer')}
icon={<PiPlusBold />}
data-testid="control-layers-add-layer-menu-button"
/>
<MenuList>
<MenuItem icon={<PiPlusBold />} onClick={addInpaintMask}>
{t('controlLayers.inpaintMask')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addRegionalGuidance}>
{t('controlLayers.regionalGuidance')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addRasterLayer}>
{t('controlLayers.rasterLayer')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addControlLayer}>
{t('controlLayers.controlLayer')}
</MenuItem>
<MenuItem icon={<PiPlusBold />} onClick={addIPAdapter}>
{t('controlLayers.globalIPAdapter')}
</MenuItem>
</MenuList>
</Menu>
);
});
EntityListGlobalActionBarAddLayerMenu.displayName = 'EntityListGlobalActionBarAddLayerMenu';

View File

@@ -1,26 +0,0 @@
import { Flex, Spacer } from '@invoke-ai/ui-library';
import { EntityListGlobalActionBarAddLayerMenu } from 'features/controlLayers/components/CanvasEntityList/EntityListGlobalActionBarAddLayerMenu';
import { EntityListSelectedEntityActionBarDuplicateButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarDuplicateButton';
import { EntityListSelectedEntityActionBarFill } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFill';
import { EntityListSelectedEntityActionBarFilterButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarFilterButton';
import { EntityListSelectedEntityActionBarOpacity } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarOpacity';
import { EntityListSelectedEntityActionBarTransformButton } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBarTransformButton';
import { memo } from 'react';
export const EntityListSelectedEntityActionBar = memo(() => {
return (
<Flex w="full" gap={2} alignItems="center" ps={1}>
<EntityListSelectedEntityActionBarOpacity />
<Spacer />
<EntityListSelectedEntityActionBarFill />
<Flex h="full">
<EntityListSelectedEntityActionBarFilterButton />
<EntityListSelectedEntityActionBarTransformButton />
<EntityListSelectedEntityActionBarDuplicateButton />
<EntityListGlobalActionBarAddLayerMenu />
</Flex>
</Flex>
);
});
EntityListSelectedEntityActionBar.displayName = 'EntityListSelectedEntityActionBar';

View File

@@ -1,34 +0,0 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { entityDuplicated } from 'features/controlLayers/store/canvasSlice';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCopyFill } from 'react-icons/pi';
export const EntityListSelectedEntityActionBarDuplicateButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const onClick = useCallback(() => {
if (!selectedEntityIdentifier) {
return;
}
dispatch(entityDuplicated({ entityIdentifier: selectedEntityIdentifier }));
}, [dispatch, selectedEntityIdentifier]);
return (
<IconButton
onClick={onClick}
isDisabled={!selectedEntityIdentifier}
size="sm"
variant="link"
alignSelf="stretch"
aria-label={t('controlLayers.duplicate')}
tooltip={t('controlLayers.duplicate')}
icon={<PiCopyFill />}
/>
);
});
EntityListSelectedEntityActionBarDuplicateButton.displayName = 'EntityListSelectedEntityActionBarDuplicateButton';

View File

@@ -1,37 +0,0 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useEntityFilter } from 'features/controlLayers/hooks/useEntityFilter';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { isFilterableEntityIdentifier } from 'features/controlLayers/store/types';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiShootingStarBold } from 'react-icons/pi';
export const EntityListSelectedEntityActionBarFilterButton = memo(() => {
const { t } = useTranslation();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const filter = useEntityFilter(selectedEntityIdentifier);
if (!selectedEntityIdentifier) {
return null;
}
if (!isFilterableEntityIdentifier(selectedEntityIdentifier)) {
return null;
}
return (
<IconButton
onClick={filter.start}
isDisabled={filter.isDisabled}
size="sm"
variant="link"
alignSelf="stretch"
aria-label={t('controlLayers.filter.filter')}
tooltip={t('controlLayers.filter.filter')}
icon={<PiShootingStarBold />}
/>
);
});
EntityListSelectedEntityActionBarFilterButton.displayName = 'EntityListSelectedEntityActionBarFilterButton';

View File

@@ -1,37 +0,0 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useEntityTransform } from 'features/controlLayers/hooks/useEntityTransform';
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
import { isTransformableEntityIdentifier } from 'features/controlLayers/store/types';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiFrameCornersBold } from 'react-icons/pi';
export const EntityListSelectedEntityActionBarTransformButton = memo(() => {
const { t } = useTranslation();
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const transform = useEntityTransform(selectedEntityIdentifier);
if (!selectedEntityIdentifier) {
return null;
}
if (!isTransformableEntityIdentifier(selectedEntityIdentifier)) {
return null;
}
return (
<IconButton
onClick={transform.start}
isDisabled={transform.isDisabled}
size="sm"
variant="link"
alignSelf="stretch"
aria-label={t('controlLayers.transform.transform')}
tooltip={t('controlLayers.transform.transform')}
icon={<PiFrameCornersBold />}
/>
);
});
EntityListSelectedEntityActionBarTransformButton.displayName = 'EntityListSelectedEntityActionBarTransformButton';

View File

@@ -2,7 +2,7 @@ import { Divider, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons';
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList';
import { EntityListSelectedEntityActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListSelectedEntityActionBar';
import { EntityListActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBar';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { selectHasEntities } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
@@ -13,7 +13,7 @@ export const CanvasPanelContent = memo(() => {
return (
<CanvasManagerProviderGate>
<Flex flexDir="column" gap={2} w="full" h="full">
<EntityListSelectedEntityActionBar />
<EntityListActionBar />
<Divider py={0} />
{!hasEntities && <CanvasAddEntityButtons />}
{hasEntities && <CanvasEntityList />}

View File

@@ -0,0 +1,145 @@
import { Flex, Grid, GridItem, IconButton } from '@invoke-ai/ui-library';
import { memo, useCallback, useState } from 'react';
import {
PiArrowDownBold,
PiArrowDownLeftBold,
PiArrowDownRightBold,
PiArrowLeftBold,
PiArrowRightBold,
PiArrowUpBold,
PiArrowUpLeftBold,
PiArrowUpRightBold,
PiSquareBold,
} from 'react-icons/pi';
type ResizeDirection =
| 'up-left'
| 'up'
| 'up-right'
| 'left'
| 'center-out'
| 'right'
| 'down-left'
| 'down'
| 'down-right';
export const CanvasResizer = memo(() => {
const [resizeDirection, setResizeDirection] = useState<ResizeDirection>('center-out');
const setDirUpLeft = useCallback(() => {
setResizeDirection('up-left');
}, []);
const setDirUp = useCallback(() => {
setResizeDirection('up');
}, []);
const setDirUpRight = useCallback(() => {
setResizeDirection('up-right');
}, []);
const setDirLeft = useCallback(() => {
setResizeDirection('left');
}, []);
const setDirCenterOut = useCallback(() => {
setResizeDirection('center-out');
}, []);
const setDirRight = useCallback(() => {
setResizeDirection('right');
}, []);
const setDirDownLeft = useCallback(() => {
setResizeDirection('down-left');
}, []);
const setDirDown = useCallback(() => {
setResizeDirection('down');
}, []);
const setDirDownRight = useCallback(() => {
setResizeDirection('down-right');
}, []);
return (
<Flex p={2}>
<Grid gridTemplateRows="1fr 1fr 1fr" gridTemplateColumns="1fr 1fr 1fr" gap={2}>
<GridItem>
<IconButton
onClick={setDirUpLeft}
aria-label="up-left"
icon={<PiArrowUpLeftBold />}
variant={resizeDirection === 'up-left' ? 'solid' : 'ghost'}
/>
</GridItem>
<GridItem>
<IconButton
onClick={setDirUp}
aria-label="up"
icon={<PiArrowUpBold />}
variant={resizeDirection === 'up' ? 'solid' : 'ghost'}
/>
</GridItem>
<GridItem>
<IconButton
onClick={setDirUpRight}
aria-label="up-right"
icon={<PiArrowUpRightBold />}
variant={resizeDirection === 'up-right' ? 'solid' : 'ghost'}
/>
</GridItem>
<GridItem>
<IconButton
onClick={setDirLeft}
aria-label="left"
icon={<PiArrowLeftBold />}
variant={resizeDirection === 'left' ? 'solid' : 'ghost'}
/>
</GridItem>
<GridItem>
<IconButton
onClick={setDirCenterOut}
aria-label="center-out"
icon={<PiSquareBold />}
variant={resizeDirection === 'center-out' ? 'solid' : 'ghost'}
/>
</GridItem>
<GridItem>
<IconButton
onClick={setDirRight}
aria-label="right"
icon={<PiArrowRightBold />}
variant={resizeDirection === 'right' ? 'solid' : 'ghost'}
/>
</GridItem>
<GridItem>
<IconButton
onClick={setDirDownLeft}
aria-label="down-left"
icon={<PiArrowDownLeftBold />}
variant={resizeDirection === 'down-left' ? 'solid' : 'ghost'}
/>
</GridItem>
<GridItem>
<IconButton
onClick={setDirDown}
aria-label="down"
icon={<PiArrowDownBold />}
variant={resizeDirection === 'down' ? 'solid' : 'ghost'}
/>
</GridItem>
<GridItem>
<IconButton
onClick={setDirDownRight}
aria-label="down-right"
icon={<PiArrowDownRightBold />}
variant={resizeDirection === 'down-right' ? 'solid' : 'ghost'}
/>
</GridItem>
</Grid>
</Flex>
);
});
CanvasResizer.displayName = 'CanvasResizer';

View File

@@ -1,83 +0,0 @@
import { useDndContext } from '@dnd-kit/core';
import { Box, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
import { CanvasPanelContent } from 'features/controlLayers/components/CanvasPanelContent';
import { CanvasSendToToggle } from 'features/controlLayers/components/CanvasSendToToggle';
import { selectSendToCanvas } from 'features/controlLayers/store/canvasSettingsSlice';
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
import { memo, useCallback, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
export const CanvasRightPanelContent = memo(() => {
const ref = useRef<HTMLDivElement>(null);
const [tab, setTab] = useState(0);
useScopeOnFocus('gallery', ref);
return (
<Tabs index={tab} onChange={setTab} w="full" h="full" display="flex" flexDir="column">
<TabList alignItems="center">
<PanelTabs setTab={setTab} />
<Spacer />
<CanvasSendToToggle />
</TabList>
<TabPanels w="full" h="full">
<TabPanel w="full" h="full" p={0} pt={3}>
<CanvasPanelContent />
</TabPanel>
<TabPanel w="full" h="full" p={0} pt={3}>
<GalleryPanelContent />
</TabPanel>
</TabPanels>
</Tabs>
);
});
CanvasRightPanelContent.displayName = 'CanvasRightPanelContent';
const PanelTabs = memo(({ setTab }: { setTab: (val: number) => void }) => {
const { t } = useTranslation();
const sendToCanvas = useAppSelector(selectSendToCanvas);
const tabTimeout = useRef<number | null>(null);
const dndCtx = useDndContext();
const onOnMouseOverLayersTab = useCallback(() => {
tabTimeout.current = window.setTimeout(() => {
if (dndCtx.active) {
setTab(1);
}
}, 300);
}, [dndCtx.active, setTab]);
const onOnMouseOverGalleryTab = useCallback(() => {
tabTimeout.current = window.setTimeout(() => {
if (dndCtx.active) {
setTab(0);
}
}, 300);
}, [dndCtx.active, setTab]);
const onMouseOut = useCallback(() => {
if (tabTimeout.current) {
clearTimeout(tabTimeout.current);
}
}, []);
return (
<>
<Tab position="relative" onMouseOver={onOnMouseOverLayersTab} onMouseOut={onMouseOut}>
{t('controlLayers.layer_other')}
{sendToCanvas && (
<Box position="absolute" top={2} right={2} h={2} w={2} bg="invokeYellow.300" borderRadius="full" />
)}
</Tab>
<Tab position="relative" onMouseOver={onOnMouseOverGalleryTab} onMouseOut={onMouseOut}>
{t('gallery.gallery')}
{!sendToCanvas && (
<Box position="absolute" top={2} right={2} h={2} w={2} bg="invokeYellow.300" borderRadius="full" />
)}
</Tab>
</>
);
});
PanelTabs.displayName = 'PanelTabs';

View File

@@ -1,76 +1,64 @@
import {
Button,
Flex,
Icon,
Popover,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
Text,
} from '@invoke-ai/ui-library';
import { Flex, Text } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectSendToCanvas, settingsSendToCanvasChanged } from 'features/controlLayers/store/canvasSettingsSlice';
import { IconSwitch } from 'common/components/IconSwitch';
import {
selectCanvasSettingsSlice,
settingsSendToCanvasChanged,
} from 'features/controlLayers/store/canvasSettingsSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiCaretDownBold, PiCheckBold } from 'react-icons/pi';
import { PiImageBold, PiPaintBrushBold } from 'react-icons/pi';
export const CanvasSendToToggle = memo(() => {
const TooltipSendToGallery = memo(() => {
const { t } = useTranslation();
const sendToCanvas = useAppSelector(selectSendToCanvas);
const dispatch = useAppDispatch();
const enableSendToCanvas = useCallback(() => {
dispatch(settingsSendToCanvasChanged(true));
}, [dispatch]);
const disableSendToCanvas = useCallback(() => {
dispatch(settingsSendToCanvasChanged(false));
}, [dispatch]);
return (
<Popover isLazy>
<PopoverTrigger>
<Button
size="sm"
variant="link"
data-testid="toggle-viewer-menu-button"
pointerEvents="auto"
rightIcon={<PiCaretDownBold />}
>
{sendToCanvas ? t('controlLayers.sendingToCanvas') : t('controlLayers.sendingToGallery')}
</Button>
</PopoverTrigger>
<PopoverContent p={2} pointerEvents="auto">
<PopoverArrow />
<PopoverBody>
<Flex flexDir="column">
<Button onClick={disableSendToCanvas} variant="ghost" h="auto" w="auto" p={2}>
<Flex gap={2} w="full">
<Icon as={PiCheckBold} visibility={!sendToCanvas ? 'visible' : 'hidden'} />
<Flex flexDir="column" gap={2} alignItems="flex-start">
<Text fontWeight="semibold">{t('controlLayers.sendToGallery')}</Text>
<Text fontWeight="normal" variant="subtext">
{t('controlLayers.sendToGalleryDesc')}
</Text>
</Flex>
</Flex>
</Button>
<Button onClick={enableSendToCanvas} variant="ghost" h="auto" w="auto" p={2}>
<Flex gap={2} w="full">
<Icon as={PiCheckBold} visibility={sendToCanvas ? 'visible' : 'hidden'} />
<Flex flexDir="column" gap={2} alignItems="flex-start">
<Text fontWeight="semibold">{t('controlLayers.sendToCanvas')}</Text>
<Text fontWeight="normal" variant="subtext">
{t('controlLayers.sendToCanvasDesc')}
</Text>
</Flex>
</Flex>
</Button>
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
<Flex flexDir="column">
<Text fontWeight="semibold">{t('controlLayers.sendToGallery')}</Text>
<Text fontWeight="normal">{t('controlLayers.sendToGalleryDesc')}</Text>
</Flex>
);
});
TooltipSendToGallery.displayName = 'TooltipSendToGallery';
const TooltipSendToCanvas = memo(() => {
const { t } = useTranslation();
return (
<Flex flexDir="column">
<Text fontWeight="semibold">{t('controlLayers.sendToCanvas')}</Text>
<Text fontWeight="normal">{t('controlLayers.sendToCanvasDesc')}</Text>
</Flex>
);
});
TooltipSendToCanvas.displayName = 'TooltipSendToCanvas';
const selectSendToCanvas = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.sendToCanvas);
export const CanvasSendToToggle = memo(() => {
const dispatch = useAppDispatch();
const sendToCanvas = useAppSelector(selectSendToCanvas);
const onChange = useCallback(
(isChecked: boolean) => {
dispatch(settingsSendToCanvasChanged(isChecked));
},
[dispatch]
);
return (
<IconSwitch
isChecked={sendToCanvas}
onChange={onChange}
iconUnchecked={<PiImageBold />}
tooltipUnchecked={<TooltipSendToGallery />}
iconChecked={<PiPaintBrushBold />}
tooltipChecked={<TooltipSendToCanvas />}
ariaLabel="Toggle canvas mode"
/>
);
});

View File

@@ -1,35 +1,21 @@
import { Flex } from '@invoke-ai/ui-library';
import { createMemoizedAppSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useAppDispatch } from 'app/store/storeHooks';
import { BeginEndStepPct } from 'features/controlLayers/components/common/BeginEndStepPct';
import { Weight } from 'features/controlLayers/components/common/Weight';
import { ControlLayerControlAdapterControlMode } from 'features/controlLayers/components/ControlLayer/ControlLayerControlAdapterControlMode';
import { ControlLayerControlAdapterModel } from 'features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useControlLayerControlAdapter } from 'features/controlLayers/hooks/useLayerControlAdapter';
import {
controlLayerBeginEndStepPctChanged,
controlLayerControlModeChanged,
controlLayerModelChanged,
controlLayerWeightChanged,
} from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier, ControlModeV2 } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import type { ControlModeV2 } from 'features/controlLayers/store/types';
import { memo, useCallback } from 'react';
import type { ControlNetModelConfig, T2IAdapterModelConfig } from 'services/api/types';
const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIdentifier<'control_layer'>) => {
const selectControlAdapter = useMemo(
() =>
createMemoizedAppSelector(selectCanvasSlice, (canvas) => {
const layer = selectEntityOrThrow(canvas, entityIdentifier);
return layer.controlAdapter;
}),
[entityIdentifier]
);
const controlAdapter = useAppSelector(selectControlAdapter);
return controlAdapter;
};
export const ControlLayerControlAdapter = memo(() => {
const dispatch = useAppDispatch();
const entityIdentifier = useEntityIdentifierContext('control_layer');

View File

@@ -1,7 +1,10 @@
import { Combobox, FormControl, Tooltip } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { selectBase } from 'features/controlLayers/store/paramsSlice';
import { IMAGE_FILTERS, isFilterType } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useControlNetAndT2IAdapterModels } from 'services/api/hooks/modelsByType';
@@ -14,6 +17,8 @@ type Props = {
export const ControlLayerControlAdapterModel = memo(({ modelKey, onChange: onChangeModel }: Props) => {
const { t } = useTranslation();
const entityIdentifier = useEntityIdentifierContext();
const canvasManager = useCanvasManager();
const currentBaseModel = useAppSelector(selectBase);
const [modelConfigs, { isLoading }] = useControlNetAndT2IAdapterModels();
const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]);
@@ -24,8 +29,29 @@ export const ControlLayerControlAdapterModel = memo(({ modelKey, onChange: onCha
return;
}
onChangeModel(modelConfig);
// When we set the model for the first time, we'll set the default filter settings and open the filter popup
if (modelKey) {
// If there is already a model key, this is not the first time we're setting the model
return;
}
// Open the filter popup by setting this entity as the filtering entity
if (!canvasManager.filter.$adapter.get()) {
// Update the filter, preferring the model's default
if (isFilterType(modelConfig.default_settings?.preprocessor)) {
canvasManager.filter.$config.set(
IMAGE_FILTERS[modelConfig.default_settings.preprocessor].buildDefaults(modelConfig.base)
);
} else {
canvasManager.filter.$config.set(IMAGE_FILTERS.canny_image_processor.buildDefaults(modelConfig.base));
}
canvasManager.filter.startFilter(entityIdentifier);
canvasManager.filter.previewFilter();
}
},
[onChangeModel]
[canvasManager.filter, entityIdentifier, modelKey, onChangeModel]
);
const getIsDisabled = useCallback(

View File

@@ -1,49 +1,37 @@
import { Button, ButtonGroup, Flex, FormControl, FormLabel, Heading, Spacer, Switch } from '@invoke-ai/ui-library';
import { Button, ButtonGroup, Flex, Heading } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { FilterSettings } from 'features/controlLayers/components/Filters/FilterSettings';
import { FilterTypeSelect } from 'features/controlLayers/components/Filters/FilterTypeSelect';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import type { CanvasEntityAdapterControlLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterControlLayer';
import type { CanvasEntityAdapterRasterLayer } from 'features/controlLayers/konva/CanvasEntity/CanvasEntityAdapterRasterLayer';
import {
selectAutoProcessFilter,
settingsAutoProcessFilterToggled,
} from 'features/controlLayers/store/canvasSettingsSlice';
import { type FilterConfig, IMAGE_FILTERS } from 'features/controlLayers/store/types';
import { memo, useCallback, useMemo } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowsCounterClockwiseBold, PiCheckBold, PiShootingStarBold, PiXBold } from 'react-icons/pi';
import { PiCheckBold, PiShootingStarBold, PiXBold } from 'react-icons/pi';
const FilterBox = memo(({ adapter }: { adapter: CanvasEntityAdapterRasterLayer | CanvasEntityAdapterControlLayer }) => {
export const Filter = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const config = useStore(adapter.filterer.$filterConfig);
const isProcessing = useStore(adapter.filterer.$isProcessing);
const hasProcessed = useStore(adapter.filterer.$hasProcessed);
const autoProcessFilter = useAppSelector(selectAutoProcessFilter);
const canvasManager = useCanvasManager();
const config = useStore(canvasManager.filter.$config);
const isFiltering = useStore(canvasManager.filter.$isFiltering);
const isProcessing = useStore(canvasManager.filter.$isProcessing);
const onChangeFilterConfig = useCallback(
(filterConfig: FilterConfig) => {
adapter.filterer.$filterConfig.set(filterConfig);
canvasManager.filter.$config.set(filterConfig);
},
[adapter.filterer.$filterConfig]
[canvasManager.filter.$config]
);
const onChangeFilterType = useCallback(
(filterType: FilterConfig['type']) => {
adapter.filterer.$filterConfig.set(IMAGE_FILTERS[filterType].buildDefaults());
canvasManager.filter.$config.set(IMAGE_FILTERS[filterType].buildDefaults());
},
[adapter.filterer.$filterConfig]
[canvasManager.filter.$config]
);
const onChangeAutoProcessFilter = useCallback(() => {
dispatch(settingsAutoProcessFilterToggled());
}, [dispatch]);
const isValid = useMemo(() => {
return IMAGE_FILTERS[config.type].validateConfig?.(config as never) ?? true;
}, [config]);
if (!isFiltering) {
return null;
}
return (
<Flex
@@ -58,56 +46,36 @@ const FilterBox = memo(({ adapter }: { adapter: CanvasEntityAdapterRasterLayer |
transitionProperty="height"
transitionDuration="normal"
>
<Flex w="full">
<Heading size="md" color="base.300" userSelect="none">
{t('controlLayers.filter.filter')}
</Heading>
<Spacer />
<FormControl w="min-content">
<FormLabel m={0}>{t('controlLayers.filter.autoProcess')}</FormLabel>
<Switch size="sm" isChecked={autoProcessFilter} onChange={onChangeAutoProcessFilter} />
</FormControl>
</Flex>
<Heading size="md" color="base.300" userSelect="none">
{t('controlLayers.filter.filter')}
</Heading>
<FilterTypeSelect filterType={config.type} onChange={onChangeFilterType} />
<FilterSettings filterConfig={config} onChange={onChangeFilterConfig} />
<ButtonGroup isAttached={false} size="sm" w="full">
<ButtonGroup isAttached={false} size="sm" alignSelf="self-end">
<Button
variant="ghost"
leftIcon={<PiShootingStarBold />}
onClick={adapter.filterer.process}
onClick={canvasManager.filter.previewFilter}
isLoading={isProcessing}
loadingText={t('controlLayers.filter.process')}
isDisabled={!isValid}
loadingText={t('controlLayers.filter.preview')}
>
{t('controlLayers.filter.process')}
</Button>
<Spacer />
<Button
leftIcon={<PiArrowsCounterClockwiseBold />}
onClick={adapter.filterer.reset}
isLoading={isProcessing}
loadingText={t('controlLayers.filter.reset')}
variant="ghost"
>
{t('controlLayers.filter.reset')}
{t('controlLayers.filter.preview')}
</Button>
<Button
variant="ghost"
leftIcon={<PiCheckBold />}
onClick={adapter.filterer.apply}
onClick={canvasManager.filter.applyFilter}
isLoading={isProcessing}
loadingText={t('controlLayers.filter.apply')}
isDisabled={!isValid || !hasProcessed}
>
{t('controlLayers.filter.apply')}
</Button>
<Button
variant="ghost"
leftIcon={<PiXBold />}
onClick={adapter.filterer.cancel}
onClick={canvasManager.filter.cancelFilter}
isLoading={isProcessing}
loadingText={t('controlLayers.filter.cancel')}
isDisabled={!isValid}
>
{t('controlLayers.filter.cancel')}
</Button>
@@ -116,16 +84,4 @@ const FilterBox = memo(({ adapter }: { adapter: CanvasEntityAdapterRasterLayer |
);
});
FilterBox.displayName = 'FilterBox';
export const Filter = () => {
const canvasManager = useCanvasManager();
const adapter = useStore(canvasManager.stateApi.$filteringAdapter);
if (!adapter) {
return null;
}
return <FilterBox adapter={adapter} />;
};
Filter.displayName = 'Filter';

View File

@@ -10,7 +10,6 @@ import { FilterMediapipeFace } from 'features/controlLayers/components/Filters/F
import { FilterMidasDepth } from 'features/controlLayers/components/Filters/FilterMidasDepth';
import { FilterMlsdImage } from 'features/controlLayers/components/Filters/FilterMlsdImage';
import { FilterPidi } from 'features/controlLayers/components/Filters/FilterPidi';
import { FilterSpandrel } from 'features/controlLayers/components/Filters/FilterSpandrel';
import type { FilterConfig } from 'features/controlLayers/store/types';
import { IMAGE_FILTERS } from 'features/controlLayers/store/types';
import { memo } from 'react';
@@ -65,10 +64,6 @@ export const FilterSettings = memo(({ filterConfig, onChange }: Props) => {
return <FilterPidi config={filterConfig} onChange={onChange} />;
}
if (filterConfig.type === 'spandrel_filter') {
return <FilterSpandrel config={filterConfig} onChange={onChange} />;
}
return (
<IAINoContentFallback
py={4}

View File

@@ -1,125 +0,0 @@
import {
Box,
Combobox,
CompositeNumberInput,
CompositeSlider,
Flex,
FormControl,
FormHelperText,
FormLabel,
Switch,
Tooltip,
} from '@invoke-ai/ui-library';
import { useModelCombobox } from 'common/hooks/useModelCombobox';
import type { SpandrelFilterConfig } from 'features/controlLayers/store/types';
import { IMAGE_FILTERS } from 'features/controlLayers/store/types';
import type { ChangeEvent } from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useSpandrelImageToImageModels } from 'services/api/hooks/modelsByType';
import type { SpandrelImageToImageModelConfig } from 'services/api/types';
import type { FilterComponentProps } from './types';
type Props = FilterComponentProps<SpandrelFilterConfig>;
const DEFAULTS = IMAGE_FILTERS['spandrel_filter'].buildDefaults();
export const FilterSpandrel = ({ onChange, config }: Props) => {
const { t } = useTranslation();
const [modelConfigs, { isLoading }] = useSpandrelImageToImageModels();
const tooltipLabel = useMemo(() => {
if (!modelConfigs.length || !config.model) {
return;
}
return modelConfigs.find((m) => m.key === config.model?.key)?.description;
}, [modelConfigs, config.model]);
const _onChange = useCallback(
(v: SpandrelImageToImageModelConfig | null) => {
onChange({ ...config, model: v });
},
[config, onChange]
);
const {
options,
value,
onChange: onChangeModel,
placeholder,
noOptionsMessage,
} = useModelCombobox({
modelConfigs,
onChange: _onChange,
selectedModel: config.model,
isLoading,
});
const onScaleChanged = useCallback(
(v: number) => {
onChange({ ...config, scale: v });
},
[onChange, config]
);
const onAutoscaleChanged = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
onChange({ ...config, autoScale: e.target.checked });
},
[onChange, config]
);
useEffect(() => {
if (!config.model) {
onChangeModel(options[0] ?? null);
}
}, [config.model, onChangeModel, options]);
return (
<>
<FormControl w="full" orientation="vertical">
<Flex w="full" alignItems="center">
<FormLabel m={0} flexGrow={1}>
{t('controlLayers.filter.spandrel.paramAutoScale')}
</FormLabel>
<Switch size="sm" isChecked={config.autoScale} onChange={onAutoscaleChanged} />
</Flex>
<FormHelperText>{t('controlLayers.filter.spandrel.paramAutoScaleDesc')}</FormHelperText>
</FormControl>
<FormControl isDisabled={!config.autoScale}>
<FormLabel m={0}>{t('controlLayers.filter.spandrel.paramScale')}</FormLabel>
<CompositeSlider
value={config.scale}
onChange={onScaleChanged}
defaultValue={DEFAULTS.scale}
min={1}
max={16}
/>
<CompositeNumberInput
value={config.scale}
onChange={onScaleChanged}
defaultValue={DEFAULTS.scale}
min={1}
max={16}
/>
</FormControl>
<FormControl>
<FormLabel m={0}>{t('controlLayers.filter.spandrel.paramModel')}</FormLabel>
<Tooltip label={tooltipLabel}>
<Box w="full">
<Combobox
value={value}
placeholder={placeholder}
options={options}
onChange={onChangeModel}
noOptionsMessage={noOptionsMessage}
isDisabled={options.length === 0}
/>
</Box>
</Tooltip>
</FormControl>
</>
);
};
FilterSpandrel.displayName = 'FilterSpandrel';

View File

@@ -1,24 +0,0 @@
import { Grid } from '@invoke-ai/ui-library';
import { CanvasHUDItemBbox } from 'features/controlLayers/components/HUD/CanvasHUDItemBbox';
import { CanvasHUDItemScaledBbox } from 'features/controlLayers/components/HUD/CanvasHUDItemScaledBbox';
import { memo } from 'react';
export const CanvasHUD = memo(() => {
return (
<Grid
bg="base.900"
borderBottomEndRadius="base"
p={2}
gap={1}
borderRadius="base"
templateColumns="1fr 1fr"
opacity={0.6}
minW={64}
>
<CanvasHUDItemBbox />
<CanvasHUDItemScaledBbox />
</Grid>
);
});
CanvasHUD.displayName = 'CanvasHUD';

View File

@@ -1,26 +0,0 @@
import { GridItem, Text } from '@invoke-ai/ui-library';
import type { Property } from 'csstype';
import { memo } from 'react';
type Props = {
label: string;
value: string | number;
color?: Property.Color;
};
export const CanvasHUDItem = memo(({ label, value, color }: Props) => {
return (
<>
<GridItem>
<Text textAlign="end">{label}: </Text>
</GridItem>
<GridItem fontWeight="semibold">
<Text textAlign="end" color={color}>
{value}
</Text>
</GridItem>
</>
);
});
CanvasHUDItem.displayName = 'CanvasHUDItem';

View File

@@ -1,17 +0,0 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasHUDItem } from 'features/controlLayers/components/HUD/CanvasHUDItem';
import { selectBbox } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const selectBboxRect = createSelector(selectBbox, (bbox) => bbox.rect);
export const CanvasHUDItemBbox = memo(() => {
const { t } = useTranslation();
const rect = useAppSelector(selectBboxRect);
return <CanvasHUDItem label={t('controlLayers.HUD.bbox')} value={`${rect.width}×${rect.height} px`} />;
});
CanvasHUDItemBbox.displayName = 'CanvasHUDItemBbox';

View File

@@ -1,19 +0,0 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasHUDItem } from 'features/controlLayers/components/HUD/CanvasHUDItem';
import { selectBbox } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
const selectScaledSize = createSelector(selectBbox, (bbox) => bbox.scaledSize);
export const CanvasHUDItemScaledBbox = memo(() => {
const { t } = useTranslation();
const scaledSize = useAppSelector(selectScaledSize);
return (
<CanvasHUDItem label={t('controlLayers.HUD.scaledBbox')} value={`${scaledSize.width}×${scaledSize.height} px`} />
);
});
CanvasHUDItemScaledBbox.displayName = 'CanvasHUDItemScaledBbox';

View File

@@ -1,133 +0,0 @@
import { Box, Flex, Icon, Text } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import type { Property } from 'csstype';
import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext';
import { useEntityTitle } from 'features/controlLayers/hooks/useEntityTitle';
import { useEntityTypeIsHidden } from 'features/controlLayers/hooks/useEntityTypeIsHidden';
import type { CanvasEntityAdapter } from 'features/controlLayers/konva/CanvasEntity/types';
import {
selectCanvasSlice,
selectEntityOrThrow,
selectSelectedEntityIdentifier,
} from 'features/controlLayers/store/selectors';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { atom } from 'nanostores';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiWarningCircleFill } from 'react-icons/pi';
type ContentProps = {
entityIdentifier: CanvasEntityIdentifier;
adapter: CanvasEntityAdapter;
};
const $isFilteringFallback = atom(false);
type EntityStatus = {
value: string;
color?: Property.Color;
};
const CanvasSelectedEntityStatusAlertContent = memo(({ entityIdentifier, adapter }: ContentProps) => {
const { t } = useTranslation();
const title = useEntityTitle(entityIdentifier);
const selectIsEnabled = useMemo(
() => createSelector(selectCanvasSlice, (canvas) => selectEntityOrThrow(canvas, entityIdentifier).isEnabled),
[entityIdentifier]
);
const selectIsLocked = useMemo(
() => createSelector(selectCanvasSlice, (canvas) => selectEntityOrThrow(canvas, entityIdentifier).isLocked),
[entityIdentifier]
);
const isEnabled = useAppSelector(selectIsEnabled);
const isLocked = useAppSelector(selectIsLocked);
const isHidden = useEntityTypeIsHidden(entityIdentifier.type);
const isFiltering = useStore(adapter.filterer?.$isFiltering ?? $isFilteringFallback);
const isTransforming = useStore(adapter.transformer.$isTransforming);
const status = useMemo<EntityStatus | null>(() => {
if (isFiltering) {
return {
value: t('controlLayers.HUD.entityStatus.isFiltering'),
color: 'invokeBlue.300',
};
}
if (isTransforming) {
return {
value: t('controlLayers.HUD.entityStatus.isTransforming'),
color: 'invokeBlue.300',
};
}
if (isHidden) {
return {
value: t('controlLayers.HUD.entityStatus.isHidden'),
color: 'invokePurple.300',
};
}
if (isLocked) {
return {
value: t('controlLayers.HUD.entityStatus.isLocked'),
color: 'invokeRed.300',
};
}
if (!isEnabled) {
return {
value: t('controlLayers.HUD.entityStatus.isDisabled'),
color: 'invokeRed.300',
};
}
return null;
}, [isFiltering, isTransforming, isHidden, isLocked, isEnabled, t]);
if (!status) {
return null;
}
return (
<Box position="relative" shadow="dark-lg">
<Flex
position="absolute"
top={0}
right={0}
left={0}
bottom={0}
bg={status.color}
opacity={0.3}
borderRadius="base"
borderColor="whiteAlpha.400"
borderWidth={1}
/>
<Flex px={6} py={4} gap={6} alignItems="center" justifyContent="center">
<Icon as={PiWarningCircleFill} />
<Text as="span" h={8}>
<Text as="span" fontWeight="semibold">
{title}
</Text>{' '}
{status.value}
</Text>
</Flex>
</Box>
);
});
CanvasSelectedEntityStatusAlertContent.displayName = 'CanvasSelectedEntityStatusAlertContent';
export const CanvasSelectedEntityStatusAlert = memo(() => {
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
const adapter = useEntityAdapterSafe(selectedEntityIdentifier);
if (!selectedEntityIdentifier || !adapter) {
return null;
}
return <CanvasSelectedEntityStatusAlertContent entityIdentifier={selectedEntityIdentifier} adapter={adapter} />;
});
CanvasSelectedEntityStatusAlert.displayName = 'CanvasSelectedEntityStatusAlert';

View File

@@ -0,0 +1,43 @@
import { Grid, GridItem, Text } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
import { memo } from 'react';
const selectBbox = createSelector(selectCanvasSlice, (canvas) => canvas.bbox);
export const HeadsUpDisplay = memo(() => {
const bbox = useAppSelector(selectBbox);
return (
<Grid
bg="base.900"
borderBottomEndRadius="base"
p={2}
gap={2}
borderRadius="base"
templateColumns="auto auto"
opacity={0.6}
>
<HUDItem label="BBox" value={`${bbox.rect.width}×${bbox.rect.height} px`} />
<HUDItem label="Scaled BBox" value={`${bbox.scaledSize.width}×${bbox.scaledSize.height} px`} />
</Grid>
);
});
HeadsUpDisplay.displayName = 'HeadsUpDisplay';
const HUDItem = memo(({ label, value }: { label: string; value: string | number }) => {
return (
<>
<GridItem>
<Text textAlign="end">{label}: </Text>
</GridItem>
<GridItem fontWeight="semibold">
<Text>{value}</Text>
</GridItem>
</>
);
});
HUDItem.displayName = 'HUDItem';

View File

@@ -9,7 +9,7 @@ import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/stor
import { selectOptimalDimension } from 'features/controlLayers/store/selectors';
import type { ImageWithDims } from 'features/controlLayers/store/types';
import type { ImageDraggableData, TypesafeDroppableData } from 'features/dnd/types';
import { calculateNewSize } from 'features/parameters/components/Bbox/calculateNewSize';
import { calculateNewSize } from 'features/parameters/components/DocumentSize/calculateNewSize';
import { memo, useCallback, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiRulerBold } from 'react-icons/pi';
@@ -85,7 +85,7 @@ export const IPAdapterImagePreview = memo(
/>
{controlImage && (
<Flex position="absolute" flexDir="column" top={2} insetInlineEnd={2} gap={1}>
<Flex position="absolute" flexDir="column" top={1} insetInlineEnd={1} gap={1}>
<IAIDndImageIcon
onClick={handleResetControlImage}
icon={<PiArrowCounterClockwiseBold size={16} />}

View File

@@ -1,28 +1,42 @@
import { Button, Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import {
buildSelectValidRegionalGuidanceActions,
useAddRegionalGuidanceIPAdapter,
useAddRegionalGuidanceNegativePrompt,
useAddRegionalGuidancePositivePrompt,
} from 'features/controlLayers/hooks/addLayerHooks';
import { useMemo } from 'react';
rgIPAdapterAdded,
rgNegativePromptChanged,
rgPositivePromptChanged,
} from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
import { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiPlusBold } from 'react-icons/pi';
export const RegionalGuidanceAddPromptsIPAdapterButtons = () => {
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const { t } = useTranslation();
const addRegionalGuidanceIPAdapter = useAddRegionalGuidanceIPAdapter(entityIdentifier);
const addRegionalGuidancePositivePrompt = useAddRegionalGuidancePositivePrompt(entityIdentifier);
const addRegionalGuidanceNegativePrompt = useAddRegionalGuidanceNegativePrompt(entityIdentifier);
const dispatch = useAppDispatch();
const selectValidActions = useMemo(
() => buildSelectValidRegionalGuidanceActions(entityIdentifier),
() =>
createMemoizedSelector(selectCanvasSlice, (canvas) => {
const entity = selectEntityOrThrow(canvas, entityIdentifier);
return {
canAddPositivePrompt: entity?.positivePrompt === null,
canAddNegativePrompt: entity?.negativePrompt === null,
};
}),
[entityIdentifier]
);
const validActions = useAppSelector(selectValidActions);
const addPositivePrompt = useCallback(() => {
dispatch(rgPositivePromptChanged({ entityIdentifier, prompt: '' }));
}, [dispatch, entityIdentifier]);
const addNegativePrompt = useCallback(() => {
dispatch(rgNegativePromptChanged({ entityIdentifier, prompt: '' }));
}, [dispatch, entityIdentifier]);
const addIPAdapter = useCallback(() => {
dispatch(rgIPAdapterAdded({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<Flex w="full" p={2} justifyContent="space-between">
@@ -30,7 +44,7 @@ export const RegionalGuidanceAddPromptsIPAdapterButtons = () => {
size="sm"
variant="ghost"
leftIcon={<PiPlusBold />}
onClick={addRegionalGuidancePositivePrompt}
onClick={addPositivePrompt}
isDisabled={!validActions.canAddPositivePrompt}
>
{t('common.positivePrompt')}
@@ -39,12 +53,12 @@ export const RegionalGuidanceAddPromptsIPAdapterButtons = () => {
size="sm"
variant="ghost"
leftIcon={<PiPlusBold />}
onClick={addRegionalGuidanceNegativePrompt}
onClick={addNegativePrompt}
isDisabled={!validActions.canAddNegativePrompt}
>
{t('common.negativePrompt')}
</Button>
<Button size="sm" variant="ghost" leftIcon={<PiPlusBold />} onClick={addRegionalGuidanceIPAdapter}>
<Button size="sm" variant="ghost" leftIcon={<PiPlusBold />} onClick={addIPAdapter}>
{t('common.ipAdapter')}
</Button>
</Flex>

View File

@@ -1,38 +1,53 @@
import { MenuItem } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import {
buildSelectValidRegionalGuidanceActions,
useAddRegionalGuidanceIPAdapter,
useAddRegionalGuidanceNegativePrompt,
useAddRegionalGuidancePositivePrompt,
} from 'features/controlLayers/hooks/addLayerHooks';
import { useCanvasIsBusy } from 'features/controlLayers/hooks/useCanvasIsBusy';
import { memo, useMemo } from 'react';
import {
rgIPAdapterAdded,
rgNegativePromptChanged,
rgPositivePromptChanged,
} from 'features/controlLayers/store/canvasSlice';
import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
export const RegionalGuidanceMenuItemsAddPromptsAndIPAdapter = memo(() => {
const entityIdentifier = useEntityIdentifierContext('regional_guidance');
const { t } = useTranslation();
const dispatch = useAppDispatch();
const isBusy = useCanvasIsBusy();
const addRegionalGuidanceIPAdapter = useAddRegionalGuidanceIPAdapter(entityIdentifier);
const addRegionalGuidancePositivePrompt = useAddRegionalGuidancePositivePrompt(entityIdentifier);
const addRegionalGuidanceNegativePrompt = useAddRegionalGuidanceNegativePrompt(entityIdentifier);
const selectValidActions = useMemo(
() => buildSelectValidRegionalGuidanceActions(entityIdentifier),
() =>
createMemoizedSelector(selectCanvasSlice, (canvas) => {
const entity = selectEntity(canvas, entityIdentifier);
return {
canAddPositivePrompt: entity?.positivePrompt === null,
canAddNegativePrompt: entity?.negativePrompt === null,
};
}),
[entityIdentifier]
);
const validActions = useAppSelector(selectValidActions);
const addPositivePrompt = useCallback(() => {
dispatch(rgPositivePromptChanged({ entityIdentifier, prompt: '' }));
}, [dispatch, entityIdentifier]);
const addNegativePrompt = useCallback(() => {
dispatch(rgNegativePromptChanged({ entityIdentifier, prompt: '' }));
}, [dispatch, entityIdentifier]);
const addIPAdapter = useCallback(() => {
dispatch(rgIPAdapterAdded({ entityIdentifier }));
}, [dispatch, entityIdentifier]);
return (
<>
<MenuItem onClick={addRegionalGuidancePositivePrompt} isDisabled={!validActions.canAddPositivePrompt || isBusy}>
<MenuItem onClick={addPositivePrompt} isDisabled={!validActions.canAddPositivePrompt || isBusy}>
{t('controlLayers.addPositivePrompt')}
</MenuItem>
<MenuItem onClick={addRegionalGuidanceNegativePrompt} isDisabled={!validActions.canAddNegativePrompt || isBusy}>
<MenuItem onClick={addNegativePrompt} isDisabled={!validActions.canAddNegativePrompt || isBusy}>
{t('controlLayers.addNegativePrompt')}
</MenuItem>
<MenuItem onClick={addRegionalGuidanceIPAdapter} isDisabled={isBusy}>
<MenuItem onClick={addIPAdapter} isDisabled={isBusy}>
{t('controlLayers.addIPAdapter')}
</MenuItem>
</>

View File

@@ -1,16 +1,17 @@
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectAutoSave, settingsAutoSaveToggled } from 'features/controlLayers/store/canvasSettingsSlice';
import { selectCanvasSettingsSlice, settingsAutoSaveToggled } from 'features/controlLayers/store/canvasSettingsSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
const selectAutoSave = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.autoSave);
export const CanvasSettingsAutoSaveCheckbox = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const autoSave = useAppSelector(selectAutoSave);
const onChange = useCallback(() => {
dispatch(settingsAutoSaveToggled());
}, [dispatch]);
const onChange = useCallback(() => dispatch(settingsAutoSaveToggled()), [dispatch]);
return (
<FormControl w="full">
<FormLabel flexGrow={1}>{t('controlLayers.autoSave')}</FormLabel>

View File

@@ -1,24 +0,0 @@
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { selectSnapToGrid, settingsSnapToGridToggled } from 'features/controlLayers/store/canvasSettingsSlice';
import type { ChangeEventHandler } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const CanvasSettingsSnapToGridCheckbox = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const snapToGrid = useAppSelector(selectSnapToGrid);
const onChange = useCallback<ChangeEventHandler<HTMLInputElement>>(() => {
dispatch(settingsSnapToGridToggled());
}, [dispatch]);
return (
<FormControl w="full">
<FormLabel flexGrow={1}>{t('controlLayers.settings.snapToGrid.label')}</FormLabel>
<Checkbox isChecked={snapToGrid} onChange={onChange} />
</FormControl>
);
});
CanvasSettingsSnapToGridCheckbox.displayName = 'CanvasSettingsSnapToGrid';

View File

@@ -15,7 +15,6 @@ import { CanvasSettingsClearHistoryButton } from 'features/controlLayers/compone
import { CanvasSettingsClipToBboxCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox';
import { CanvasSettingsCompositeMaskedRegionsCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsCompositeMaskedRegionsCheckbox';
import { CanvasSettingsDynamicGridSwitch } from 'features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch';
import { CanvasSettingsSnapToGridCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsGridSize';
import { CanvasSettingsInvertScrollCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox';
import { CanvasSettingsLogDebugInfoButton } from 'features/controlLayers/components/Settings/CanvasSettingsLogDebugInfo';
import { CanvasSettingsRecalculateRectsButton } from 'features/controlLayers/components/Settings/CanvasSettingsRecalculateRectsButton';
@@ -40,7 +39,6 @@ export const CanvasSettingsPopover = memo(() => {
<CanvasSettingsInvertScrollCheckbox />
<CanvasSettingsClipToBboxCheckbox />
<CanvasSettingsCompositeMaskedRegionsCheckbox />
<CanvasSettingsSnapToGridCheckbox />
<CanvasSettingsDynamicGridSwitch />
<CanvasSettingsShowHUDSwitch />
<CanvasSettingsResetButton />

View File

@@ -1,6 +1,5 @@
import { Button } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { canvasReset } from 'features/controlLayers/store/canvasSlice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
@@ -8,11 +7,9 @@ import { useTranslation } from 'react-i18next';
export const CanvasSettingsResetButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const canvasManager = useCanvasManager();
const onClick = useCallback(() => {
dispatch(canvasReset());
canvasManager.stage.fitLayersToStage();
}, [canvasManager.stage, dispatch]);
}, [dispatch]);
return (
<Button onClick={onClick} colorScheme="error" size="sm">
{t('controlLayers.resetCanvas')}

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