Compare commits

...

14 Commits

Author SHA1 Message Date
d8258c8e2a feat(mm): hide missing models from API response
Check if the model file exists. If not, omit it from the list of models.

Closes #6117
2024-04-03 20:37:44 +11:00
8584171a49 docs: fix broken link (#6116)
## Summary

Fix a broken link

## Related Issues / Discussions


https://discord.com/channels/1020123559063990373/1049495067846524939/1224970148058763376

## QA Instructions

n/a

## Merge Plan

n/a

## Checklist

- [x] _The PR has a short but descriptive title, suitable for a
changelog_
- [ ] _Tests added / updated (if applicable)_ n/a
- [x] _Documentation added / updated (if applicable)_
2024-04-03 12:35:17 +05:30
50951439bd docs: fix broken link 2024-04-03 17:36:15 +11:00
7b93b554d7 fix(ui): add default coherence mode to generation slice migration
The valid values for this parameter changed when inpainting changed to gradient denoise. The generation slice's redux migration wasn't updated, resulting in a generation error until you change the setting or reset web UI.
2024-04-03 08:46:31 +11:00
21b9e96a45 Run typegen, bump version 2024-04-02 10:14:39 -04:00
b6ad33ac1a perf(ui): reduce canvas max history to 100
This should further insulate canvas from excessive GCs.
2024-04-02 08:48:18 -04:00
69ec14c7bb perf(ui): use rfdc for deep copying of objects
- Add and use more performant `deepClone` method for deep copying throughout the UI.

Benchmarks indicate the Really Fast Deep Clone library (`rfdc`) is the best all-around way to deep-clone large objects.

This is particularly relevant in canvas. When drawing or otherwise manipulating canvas objects, we need to do a lot of deep cloning of the canvas layer state objects.

Previously, we were using lodash's `cloneDeep`.

I did some fairly realistic benchmarks with a handful of deep-cloning algorithms/libraries (including the native `structuredClone`). I used a snapshot of the canvas state as the data to be copied:

On Chromium, `rfdc` is by far the fastest, over an order of magnitude faster than `cloneDeep`.

On FF, `fastest-json-copy` and `recursiveDeepCopy` are even faster, but are rather limited in data types. `rfdc`, while only half as fast as the former 2, is still nearly an order of magnitude faster than `cloneDeep`.

On Safari, `structuredClone` is the fastest, about 2x as fast as `cloneDeep`. `rfdc` is only 30% faster than `cloneDeep`.

`rfdc`'s peak memory usage is about 10% more than `cloneDeep` on Chrome. I couldn't get memory measurements from FF and Safari, but let's just assume the memory usage is similar relative to the other algos.

Overall, `rfdc` is the best choice for a single algo for all browsers. It's definitely the best for Chromium, by far the most popular desktop browser and thus our primary target.

A future enhancement might be to detect the browser and use that to determine which algorithm to use.
2024-04-02 08:48:18 -04:00
a6c91979af fix(ui): prevent canvas history leak
There were two ways the canvas history could grow too large (past the `MAX_HISTORY` setting):
- Sometimes, when pushing to history, we didn't `shift` an item out when we exceeded the max history size.
- If the max history size was exceeded by more than one item, we still only `shift`, which removes one item.

These issue could appear after an extended canvas session, resulting in a memory leak and recurring major GCs/browser performance issues.

To fix these issues, a helper function is added for both past and future layer states, which uses slicing to ensure history never grows too large.
2024-04-02 08:48:18 -04:00
e655399324 fix(config): handle windows paths in invokeai.yaml migration for legacy_conf_dir
The logic incorrectly set the `legacy_conf_dir` on windows, where the slashes go the other direction. Handle this case and update tests to catch it.
2024-04-02 08:06:59 -04:00
f75de8a35c feat(db): add migration 9 - empty session queue
Empties the session queue. This is done to prevent any lingering session queue items from causing pydantic errors due to changed schemas.
2024-04-02 13:25:14 +11:00
d4be945dde fix(nodes): gracefully handle custom nodes init error
Previously, exceptions raised as custom nodes are initialized were fatal errors, causing the app to exit.

With this change, any error on import is caught and the error message printed. App continues to start up without the node.

For example, a custom node that isn't updated for v4.0.0 may raise an error on import if it is attempting to import things that no longer exist.
2024-04-02 13:25:14 +11:00
ab33acad5c translationBot(ui): update translation (Russian)
Currently translated at 99.5% (1119 of 1124 strings)

Co-authored-by: Васянатор <ilabulanov339@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ru/
Translation: InvokeAI/Web UI
2024-04-02 13:15:11 +11:00
8f3d7b2946 translationBot(ui): update translation (Italian)
Currently translated at 98.3% (1106 of 1124 strings)

translationBot(ui): update translation (Italian)

Currently translated at 98.3% (1104 of 1122 strings)

Co-authored-by: Riccardo Giovanetti <riccardo.giovanetti@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/
Translation: InvokeAI/Web UI
2024-04-02 13:15:11 +11:00
54a30f66cb translationBot(ui): update translation (German)
Currently translated at 72.4% (813 of 1122 strings)

Co-authored-by: Alexander Eichhorn <pfannkuchensack@einfach-doof.de>
Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/
Translation: InvokeAI/Web UI
2024-04-02 13:15:11 +11:00
27 changed files with 240 additions and 116 deletions

View File

@ -32,5 +32,5 @@ As described in the [frontend dev toolchain] docs, you can run the UI using a de
[Fork and clone]: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo
[InvokeAI repo]: https://github.com/invoke-ai/InvokeAI
[frontend dev toolchain]: ../contributing/frontend/OVERVIEW.md
[manual installation]: installation/020_INSTALL_MANUAL.md
[manual installation]: ./020_INSTALL_MANUAL.md
[editable install]: https://pip.pypa.io/en/latest/cli/pip_install/#cmdoption-e

View File

@ -117,10 +117,14 @@ async def list_model_records(
found_models.extend(
record_store.search_by_attr(model_type=model_type, model_name=model_name, model_format=model_format)
)
for model in found_models:
models_path = ApiDependencies.invoker.services.configuration.models_path
existant_models = [m for m in found_models if (models_path / m.path).exists()]
for model in existant_models:
cover_image = ApiDependencies.invoker.services.model_images.get_url(model.key)
model.cover_image = cover_image
return ModelsList(models=found_models)
return ModelsList(models=existant_models)
@model_manager_router.get(

View File

@ -3,6 +3,7 @@ Invoke-managed custom node loader. See README.md for more information.
"""
import sys
import traceback
from importlib.util import module_from_spec, spec_from_file_location
from pathlib import Path
@ -41,11 +42,15 @@ for d in Path(__file__).parent.iterdir():
logger.info(f"Loading node pack {module_name}")
module = module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
try:
module = module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
loaded_count += 1
loaded_count += 1
except Exception:
full_error = traceback.format_exc()
logger.error(f"Failed to load node pack {module_name}:\n{full_error}")
del init, module_name

View File

@ -373,13 +373,16 @@ def migrate_v3_config_dict(config_dict: dict[str, Any]) -> InvokeAIAppConfig:
if k == "conf_path":
parsed_config_dict["legacy_models_yaml_path"] = v
if k == "legacy_conf_dir":
# The old default for this was "configs/stable-diffusion". If if the incoming config has that as the value, we won't set it.
# Else if the path ends in "stable-diffusion", we assume the parent is the new correct path.
# Else we do not attempt to migrate this setting
if v != "configs/stable-diffusion":
parsed_config_dict["legacy_conf_dir"] = v
# The old default for this was "configs/stable-diffusion" ("configs\stable-diffusion" on Windows).
if v == "configs/stable-diffusion" or v == "configs\\stable-diffusion":
# If if the incoming config has the default value, skip
continue
elif Path(v).name == "stable-diffusion":
# Else if the path ends in "stable-diffusion", we assume the parent is the new correct path.
parsed_config_dict["legacy_conf_dir"] = str(Path(v).parent)
else:
# Else we do not attempt to migrate this setting
parsed_config_dict["legacy_conf_dir"] = v
elif k in InvokeAIAppConfig.model_fields:
# skip unknown fields
parsed_config_dict[k] = v

View File

@ -11,6 +11,7 @@ from invokeai.app.services.shared.sqlite_migrator.migrations.migration_5 import
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_6 import build_migration_6
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_7 import build_migration_7
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_8 import build_migration_8
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_9 import build_migration_9
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_impl import SqliteMigrator
@ -39,6 +40,7 @@ def init_db(config: InvokeAIAppConfig, logger: Logger, image_files: ImageFileSto
migrator.register_migration(build_migration_6())
migrator.register_migration(build_migration_7())
migrator.register_migration(build_migration_8(app_config=config))
migrator.register_migration(build_migration_9())
migrator.run_migrations()
return db

View File

@ -0,0 +1,29 @@
import sqlite3
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import Migration
class Migration9Callback:
def __call__(self, cursor: sqlite3.Cursor) -> None:
self._empty_session_queue(cursor)
def _empty_session_queue(self, cursor: sqlite3.Cursor) -> None:
"""Empties the session queue. This is done to prevent any lingering session queue items from causing pydantic errors due to changed schemas."""
cursor.execute("DELETE FROM session_queue;")
def build_migration_9() -> Migration:
"""
Build the migration from database version 8 to 9.
This migration does the following:
- Empties the session queue. This is done to prevent any lingering session queue items from causing pydantic errors due to changed schemas.
"""
migration_9 = Migration(
from_version=8,
to_version=9,
callback=Migration9Callback(),
)
return migration_9

View File

@ -94,6 +94,7 @@
"reactflow": "^11.10.4",
"redux-dynamic-middlewares": "^2.2.0",
"redux-remember": "^5.1.0",
"rfdc": "^1.3.1",
"roarr": "^7.21.1",
"serialize-error": "^11.0.3",
"socket.io-client": "^4.7.5",

View File

@ -137,6 +137,9 @@ dependencies:
redux-remember:
specifier: ^5.1.0
version: 5.1.0(redux@5.0.1)
rfdc:
specifier: ^1.3.1
version: 1.3.1
roarr:
specifier: ^7.21.1
version: 7.21.1
@ -12128,6 +12131,10 @@ packages:
resolution: {integrity: sha512-/x8uIPdTafBqakK0TmPNJzgkLP+3H+yxpUJhCQHsLBg1rYEVNR2D8BRYNWQhVBjyOd7oo1dZRVzIkwMY2oqfYQ==}
dev: true
/rfdc@1.3.1:
resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==}
dev: false
/rimraf@2.6.3:
resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==}
hasBin: true

View File

@ -4,7 +4,7 @@
"reportBugLabel": "Fehler melden",
"settingsLabel": "Einstellungen",
"img2img": "Bild zu Bild",
"nodes": "Knoten Editor",
"nodes": "Arbeitsabläufe",
"upload": "Hochladen",
"load": "Laden",
"statusDisconnected": "Getrennt",
@ -74,7 +74,8 @@
"updated": "Aktualisiert",
"copy": "Kopieren",
"aboutHeading": "Nutzen Sie Ihre kreative Energie",
"toResolve": "Lösen"
"toResolve": "Lösen",
"add": "Hinzufügen"
},
"gallery": {
"galleryImageSize": "Bildgröße",
@ -104,11 +105,16 @@
"dropToUpload": "$t(gallery.drop) zum hochladen",
"dropOrUpload": "$t(gallery.drop) oder hochladen",
"drop": "Ablegen",
"problemDeletingImages": "Problem beim Löschen der Bilder"
"problemDeletingImages": "Problem beim Löschen der Bilder",
"bulkDownloadRequested": "Download vorbereiten",
"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"
},
"hotkeys": {
"keyboardShortcuts": "Tastenkürzel",
"appHotkeys": "App-Tastenkombinationen",
"appHotkeys": "App",
"generalHotkeys": "Allgemein",
"galleryHotkeys": "Galerie",
"unifiedCanvasHotkeys": "Leinwand",
@ -757,7 +763,9 @@
"scheduler": "Planer",
"noRecallParameters": "Es wurden keine Parameter zum Abrufen gefunden",
"recallParameters": "Parameter wiederherstellen",
"cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)"
"cfgRescaleMultiplier": "$t(parameters.cfgRescaleMultiplier)",
"allPrompts": "Alle Prompts",
"imageDimensions": "Bilder Auslösungen"
},
"popovers": {
"noiseUseCPU": {
@ -1068,5 +1076,10 @@
},
"dynamicPrompts": {
"showDynamicPrompts": "Dynamische Prompts anzeigen"
},
"prompt": {
"noMatchingTriggers": "Keine passenden Auslöser",
"addPromptTrigger": "Auslöse Text hinzufügen",
"compatibleEmbeddings": "Kompatible Einbettungen"
}
}

View File

@ -73,7 +73,8 @@
"ai": "ia",
"file": "File",
"toResolve": "Da risolvere",
"add": "Aggiungi"
"add": "Aggiungi",
"loglevel": "Livello di log"
},
"gallery": {
"galleryImageSize": "Dimensione dell'immagine",
@ -934,7 +935,9 @@
"base": "Base",
"lineart": "Linea",
"controlnet": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.controlNet))",
"mediapipeFace": "Mediapipe Volto"
"mediapipeFace": "Mediapipe Volto",
"ip_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.ipAdapter))",
"t2i_adapter": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.t2iAdapter))"
},
"queue": {
"queueFront": "Aggiungi all'inizio della coda",
@ -1490,7 +1493,8 @@
"title": "Generazione"
},
"advanced": {
"title": "Avanzate"
"title": "Avanzate",
"options": "Opzioni $t(accordions.advanced.title)"
},
"image": {
"title": "Immagine"

View File

@ -75,7 +75,8 @@
"copy": "Копировать",
"localSystem": "Локальная система",
"aboutDesc": "Используя Invoke для работы? Проверьте это:",
"add": "Добавить"
"add": "Добавить",
"loglevel": "Уровень логов"
},
"gallery": {
"galleryImageSize": "Размер изображений",
@ -1505,7 +1506,8 @@
"title": "Генерация"
},
"advanced": {
"title": "Расширенные"
"title": "Расширенные",
"options": "Опции $t(accordions.advanced.title)"
},
"image": {
"title": "Изображение"

View File

@ -1,7 +1,7 @@
import type { UnknownAction } from '@reduxjs/toolkit';
import { deepClone } from 'common/util/deepClone';
import { isAnyGraphBuilt } from 'features/nodes/store/actions';
import { nodeTemplatesBuilt } from 'features/nodes/store/nodesSlice';
import { cloneDeep } from 'lodash-es';
import { appInfoApi } from 'services/api/endpoints/appInfo';
import type { Graph } from 'services/api/types';
import { socketGeneratorProgress } from 'services/events/actions';
@ -33,7 +33,7 @@ export const actionSanitizer = <A extends UnknownAction>(action: A): A => {
}
if (socketGeneratorProgress.match(action)) {
const sanitized = cloneDeep(action);
const sanitized = deepClone(action);
if (sanitized.payload.data.progress_image) {
sanitized.payload.data.progress_image.dataURL = '<Progress image omitted>';
}

View File

@ -1,4 +1,5 @@
import { cloneDeep, merge } from 'lodash-es';
import { deepClone } from 'common/util/deepClone';
import { merge } from 'lodash-es';
import { ClickScrollPlugin, OverlayScrollbars } from 'overlayscrollbars';
import type { UseOverlayScrollbarsParams } from 'overlayscrollbars-react';
@ -22,7 +23,7 @@ export const getOverlayScrollbarsParams = (
overflowX: 'hidden' | 'scroll' = 'hidden',
overflowY: 'hidden' | 'scroll' = 'scroll'
) => {
const params = cloneDeep(overlayScrollbarsParams);
const params = deepClone(overlayScrollbarsParams);
merge(params, { options: { overflow: { y: overflowY, x: overflowX } } });
return params;
};

View File

@ -0,0 +1,15 @@
import rfdc from 'rfdc';
const _rfdc = rfdc();
/**
* Deep-clones an object using Really Fast Deep Clone.
* This is the fastest deep clone library on Chrome, but not the fastest on FF. Still, it's much faster than lodash
* and structuredClone, so it's the best all-around choice.
*
* Simple Benchmark: https://www.measurethat.net/Benchmarks/Show/30358/0/lodash-clonedeep-vs-jsonparsejsonstringify-vs-recursive
* Repo: https://github.com/davidmarkclements/rfdc
*
* @param obj The object to deep-clone
* @returns The cloned object
*/
export const deepClone = <T>(obj: T): T => _rfdc(obj);

View File

@ -1,6 +1,7 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
import { roundDownToMultiple, roundToMultiple } from 'common/util/roundDownToMultiple';
import calculateCoordinates from 'features/canvas/util/calculateCoordinates';
import calculateScale from 'features/canvas/util/calculateScale';
@ -13,7 +14,7 @@ import { modelChanged } from 'features/parameters/store/generationSlice';
import type { PayloadActionWithOptimalDimension } from 'features/parameters/store/types';
import { getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
import type { IRect, Vector2d } from 'konva/lib/types';
import { clamp, cloneDeep } from 'lodash-es';
import { clamp } from 'lodash-es';
import type { RgbaColor } from 'react-colorful';
import { queueApi } from 'services/api/endpoints/queue';
import type { ImageDTO } from 'services/api/types';
@ -36,7 +37,7 @@ import { CANVAS_GRID_SIZE_FINE } from './constants';
/**
* The maximum history length to keep in the past/future layer states.
*/
const MAX_HISTORY = 128;
const MAX_HISTORY = 100;
const initialLayerState: CanvasLayerState = {
objects: [],
@ -121,7 +122,7 @@ export const canvasSlice = createSlice({
state.brushSize = action.payload;
},
clearMask: (state) => {
state.pastLayerStates.push(cloneDeep(state.layerState));
pushToPrevLayerStates(state);
state.layerState.objects = state.layerState.objects.filter((obj) => !isCanvasMaskLine(obj));
state.futureLayerStates = [];
state.shouldPreserveMaskedArea = false;
@ -163,10 +164,10 @@ export const canvasSlice = createSlice({
state.boundingBoxDimensions = newBoundingBoxDimensions;
state.boundingBoxCoordinates = newBoundingBoxCoordinates;
state.pastLayerStates.push(cloneDeep(state.layerState));
pushToPrevLayerStates(state);
state.layerState = {
...cloneDeep(initialLayerState),
...deepClone(initialLayerState),
objects: [
{
kind: 'image',
@ -261,11 +262,7 @@ export const canvasSlice = createSlice({
return;
}
state.pastLayerStates.push(cloneDeep(state.layerState));
if (state.pastLayerStates.length > MAX_HISTORY) {
state.pastLayerStates.shift();
}
pushToPrevLayerStates(state);
state.layerState.stagingArea.images.push({
kind: 'image',
@ -279,13 +276,9 @@ export const canvasSlice = createSlice({
state.futureLayerStates = [];
},
discardStagedImages: (state) => {
state.pastLayerStates.push(cloneDeep(state.layerState));
pushToPrevLayerStates(state);
if (state.pastLayerStates.length > MAX_HISTORY) {
state.pastLayerStates.shift();
}
state.layerState.stagingArea = cloneDeep(cloneDeep(initialLayerState)).stagingArea;
state.layerState.stagingArea = deepClone(initialLayerState.stagingArea);
state.futureLayerStates = [];
state.shouldShowStagingOutline = true;
@ -294,11 +287,7 @@ export const canvasSlice = createSlice({
},
discardStagedImage: (state) => {
const { images, selectedImageIndex } = state.layerState.stagingArea;
state.pastLayerStates.push(cloneDeep(state.layerState));
if (state.pastLayerStates.length > MAX_HISTORY) {
state.pastLayerStates.shift();
}
pushToPrevLayerStates(state);
if (!images.length) {
return;
@ -320,11 +309,7 @@ export const canvasSlice = createSlice({
addFillRect: (state) => {
const { boundingBoxCoordinates, boundingBoxDimensions, brushColor } = state;
state.pastLayerStates.push(cloneDeep(state.layerState));
if (state.pastLayerStates.length > MAX_HISTORY) {
state.pastLayerStates.shift();
}
pushToPrevLayerStates(state);
state.layerState.objects.push({
kind: 'fillRect',
@ -339,11 +324,7 @@ export const canvasSlice = createSlice({
addEraseRect: (state) => {
const { boundingBoxCoordinates, boundingBoxDimensions } = state;
state.pastLayerStates.push(cloneDeep(state.layerState));
if (state.pastLayerStates.length > MAX_HISTORY) {
state.pastLayerStates.shift();
}
pushToPrevLayerStates(state);
state.layerState.objects.push({
kind: 'eraseRect',
@ -367,11 +348,7 @@ export const canvasSlice = createSlice({
// set & then spread this to only conditionally add the "color" key
const newColor = layer === 'base' && tool === 'brush' ? { color: brushColor } : {};
state.pastLayerStates.push(cloneDeep(state.layerState));
if (state.pastLayerStates.length > MAX_HISTORY) {
state.pastLayerStates.shift();
}
pushToPrevLayerStates(state);
const newLine: CanvasMaskLine | CanvasBaseLine = {
kind: 'line',
@ -409,11 +386,7 @@ export const canvasSlice = createSlice({
return;
}
state.futureLayerStates.unshift(cloneDeep(state.layerState));
if (state.futureLayerStates.length > MAX_HISTORY) {
state.futureLayerStates.pop();
}
pushToFutureLayerStates(state);
state.layerState = targetState;
},
@ -424,11 +397,7 @@ export const canvasSlice = createSlice({
return;
}
state.pastLayerStates.push(cloneDeep(state.layerState));
if (state.pastLayerStates.length > MAX_HISTORY) {
state.pastLayerStates.shift();
}
pushToPrevLayerStates(state);
state.layerState = targetState;
},
@ -445,8 +414,8 @@ export const canvasSlice = createSlice({
state.shouldShowIntermediates = action.payload;
},
resetCanvas: (state) => {
state.pastLayerStates.push(cloneDeep(state.layerState));
state.layerState = cloneDeep(initialLayerState);
pushToPrevLayerStates(state);
state.layerState = deepClone(initialLayerState);
state.futureLayerStates = [];
state.batchIds = [];
state.boundingBoxCoordinates = {
@ -540,11 +509,7 @@ export const canvasSlice = createSlice({
const { images, selectedImageIndex } = state.layerState.stagingArea;
state.pastLayerStates.push(cloneDeep(state.layerState));
if (state.pastLayerStates.length > MAX_HISTORY) {
state.pastLayerStates.shift();
}
pushToPrevLayerStates(state);
const imageToCommit = images[selectedImageIndex];
@ -553,7 +518,7 @@ export const canvasSlice = createSlice({
...imageToCommit,
});
}
state.layerState.stagingArea = cloneDeep(initialLayerState).stagingArea;
state.layerState.stagingArea = deepClone(initialLayerState.stagingArea);
state.futureLayerStates = [];
state.shouldShowStagingOutline = true;
@ -623,7 +588,7 @@ export const canvasSlice = createSlice({
};
},
setMergedCanvas: (state, action: PayloadAction<CanvasImage>) => {
state.pastLayerStates.push(cloneDeep(state.layerState));
pushToPrevLayerStates(state);
state.futureLayerStates = [];
@ -743,3 +708,17 @@ export const canvasPersistConfig: PersistConfig<CanvasState> = {
migrate: migrateCanvasState,
persistDenylist: [],
};
const pushToPrevLayerStates = (state: CanvasState) => {
state.pastLayerStates.push(deepClone(state.layerState));
if (state.pastLayerStates.length > MAX_HISTORY) {
state.pastLayerStates = state.pastLayerStates.slice(-MAX_HISTORY);
}
};
const pushToFutureLayerStates = (state: CanvasState) => {
state.futureLayerStates.unshift(deepClone(state.layerState));
if (state.futureLayerStates.length > MAX_HISTORY) {
state.futureLayerStates = state.futureLayerStates.slice(0, MAX_HISTORY);
}
};

View File

@ -2,10 +2,11 @@ import type { PayloadAction, Update } from '@reduxjs/toolkit';
import { createEntityAdapter, createSlice, isAnyOf } from '@reduxjs/toolkit';
import { getSelectorsOptions } from 'app/store/createMemoizedSelector';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
import { buildControlAdapter } from 'features/controlAdapters/util/buildControlAdapter';
import { buildControlAdapterProcessor } from 'features/controlAdapters/util/buildControlAdapterProcessor';
import { zModelIdentifierField } from 'features/nodes/types/common';
import { cloneDeep, merge, uniq } from 'lodash-es';
import { merge, uniq } from 'lodash-es';
import type { ControlNetModelConfig, IPAdapterModelConfig, T2IAdapterModelConfig } from 'services/api/types';
import { socketInvocationError } from 'services/events/actions';
import { v4 as uuidv4 } from 'uuid';
@ -114,7 +115,7 @@ export const controlAdaptersSlice = createSlice({
if (!controlAdapter) {
return;
}
const newControlAdapter = merge(cloneDeep(controlAdapter), {
const newControlAdapter = merge(deepClone(controlAdapter), {
id: newId,
isEnabled: true,
});
@ -270,7 +271,7 @@ export const controlAdaptersSlice = createSlice({
return;
}
const processorNode = merge(cloneDeep(cn.processorNode), params);
const processorNode = merge(deepClone(cn.processorNode), params);
caAdapter.updateOne(state, {
id,
@ -293,7 +294,7 @@ export const controlAdaptersSlice = createSlice({
return;
}
const processorNode = cloneDeep(
const processorNode = deepClone(
CONTROLNET_PROCESSORS[processorType].buildDefaults(cn.model?.base)
) as RequiredControlAdapterProcessorNode;
@ -333,7 +334,7 @@ export const controlAdaptersSlice = createSlice({
caAdapter.updateOne(state, update);
},
controlAdaptersReset: () => {
return cloneDeep(initialControlAdaptersState);
return deepClone(initialControlAdaptersState);
},
pendingControlImagesCleared: (state) => {
state.pendingControlImages = [];
@ -406,7 +407,7 @@ const migrateControlAdaptersState = (state: any): any => {
state._version = 1;
}
if (state._version === 1) {
state = cloneDeep(initialControlAdaptersState);
state = deepClone(initialControlAdaptersState);
}
return state;
};

View File

@ -1,3 +1,4 @@
import { deepClone } from 'common/util/deepClone';
import { CONTROLNET_PROCESSORS } from 'features/controlAdapters/store/constants';
import type {
ControlAdapterConfig,
@ -7,7 +8,7 @@ import type {
RequiredCannyImageProcessorInvocation,
T2IAdapterConfig,
} from 'features/controlAdapters/store/types';
import { cloneDeep, merge } from 'lodash-es';
import { merge } from 'lodash-es';
export const initialControlNet: Omit<ControlNetConfig, 'id'> = {
type: 'controlnet',
@ -57,11 +58,11 @@ export const buildControlAdapter = (
): ControlAdapterConfig => {
switch (type) {
case 'controlnet':
return merge(cloneDeep(initialControlNet), { id, ...overrides });
return merge(deepClone(initialControlNet), { id, ...overrides });
case 't2i_adapter':
return merge(cloneDeep(initialT2IAdapter), { id, ...overrides });
return merge(deepClone(initialT2IAdapter), { id, ...overrides });
case 'ip_adapter':
return merge(cloneDeep(initialIPAdapter), { id, ...overrides });
return merge(deepClone(initialIPAdapter), { id, ...overrides });
default:
throw new Error(`Unknown control adapter type: ${type}`);
}

View File

@ -1,9 +1,9 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
import { zModelIdentifierField } from 'features/nodes/types/common';
import type { ParameterLoRAModel } from 'features/parameters/types/parameterSchemas';
import { cloneDeep } from 'lodash-es';
import type { LoRAModelConfig } from 'services/api/types';
export type LoRA = {
@ -58,7 +58,7 @@ export const loraSlice = createSlice({
}
lora.isEnabled = isEnabled;
},
lorasReset: () => cloneDeep(initialLoraState),
lorasReset: () => deepClone(initialLoraState),
},
});
@ -74,7 +74,7 @@ const migrateLoRAState = (state: any): any => {
}
if (state._version === 1) {
// Model type has changed, so we need to reset the state - too risky to migrate
state = cloneDeep(initialLoraState);
state = deepClone(initialLoraState);
}
return state;
};

View File

@ -1,6 +1,7 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice, isAnyOf } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
import { workflowLoaded } from 'features/nodes/store/actions';
import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants';
import type {
@ -44,7 +45,7 @@ import {
} from 'features/nodes/types/field';
import type { AnyNode, InvocationTemplate, NodeExecutionState } from 'features/nodes/types/invocation';
import { isInvocationNode, isNotesNode, zNodeStatus } from 'features/nodes/types/invocation';
import { cloneDeep, forEach } from 'lodash-es';
import { forEach } from 'lodash-es';
import type {
Connection,
Edge,
@ -571,8 +572,23 @@ export const nodesSlice = createSlice({
);
},
selectionCopied: (state) => {
state.nodesToCopy = state.nodes.filter((n) => n.selected).map(cloneDeep);
state.edgesToCopy = state.edges.filter((e) => e.selected).map(cloneDeep);
const nodesToCopy: AnyNode[] = [];
const edgesToCopy: Edge[] = [];
for (const node of state.nodes) {
if (node.selected) {
nodesToCopy.push(deepClone(node));
}
}
for (const edge of state.edges) {
if (edge.selected) {
edgesToCopy.push(deepClone(edge));
}
}
state.nodesToCopy = nodesToCopy;
state.edgesToCopy = edgesToCopy;
if (state.nodesToCopy.length > 0) {
const averagePosition = { x: 0, y: 0 };
@ -594,11 +610,21 @@ export const nodesSlice = createSlice({
},
selectionPasted: (state, action: PayloadAction<{ cursorPosition?: XYPosition }>) => {
const { cursorPosition } = action.payload;
const newNodes = state.nodesToCopy.map(cloneDeep);
const newNodes: AnyNode[] = [];
for (const node of state.nodesToCopy) {
newNodes.push(deepClone(node));
}
const oldNodeIds = newNodes.map((n) => n.data.id);
const newEdges = state.edgesToCopy
.filter((e) => oldNodeIds.includes(e.source) && oldNodeIds.includes(e.target))
.map(cloneDeep);
const newEdges: Edge[] = [];
for (const edge of state.edgesToCopy) {
if (oldNodeIds.includes(edge.source) && oldNodeIds.includes(edge.target)) {
newEdges.push(deepClone(edge));
}
}
newEdges.forEach((e) => (e.selected = true));

View File

@ -1,6 +1,7 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import type { PersistConfig, RootState } from 'app/store/store';
import { deepClone } from 'common/util/deepClone';
import { workflowLoaded } from 'features/nodes/store/actions';
import { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesChanged, nodesDeleted } from 'features/nodes/store/nodesSlice';
import type {
@ -11,7 +12,7 @@ import type {
import type { FieldIdentifier } from 'features/nodes/types/field';
import { isInvocationNode } from 'features/nodes/types/invocation';
import type { WorkflowCategory, WorkflowV3 } from 'features/nodes/types/workflow';
import { cloneDeep, isEqual, omit, uniqBy } from 'lodash-es';
import { isEqual, omit, uniqBy } from 'lodash-es';
const blankWorkflow: Omit<WorkflowV3, 'nodes' | 'edges'> = {
name: '',
@ -131,8 +132,8 @@ export const workflowSlice = createSlice({
});
return {
...cloneDeep(initialWorkflowState),
...cloneDeep(workflowExtra),
...deepClone(initialWorkflowState),
...deepClone(workflowExtra),
originalExposedFieldValues,
mode: state.mode,
};
@ -144,7 +145,7 @@ export const workflowSlice = createSlice({
});
});
builder.addCase(nodeEditorReset, () => cloneDeep(initialWorkflowState));
builder.addCase(nodeEditorReset, () => deepClone(initialWorkflowState));
builder.addCase(nodesChanged, (state, action) => {
// Not all changes to nodes should result in the workflow being marked touched

View File

@ -1,8 +1,9 @@
import { deepClone } from 'common/util/deepClone';
import { satisfies } from 'compare-versions';
import { NodeUpdateError } from 'features/nodes/types/error';
import type { InvocationNode, InvocationTemplate } from 'features/nodes/types/invocation';
import { zParsedSemver } from 'features/nodes/types/semver';
import { cloneDeep, defaultsDeep, keys, pick } from 'lodash-es';
import { defaultsDeep, keys, pick } from 'lodash-es';
import { buildInvocationNode } from './buildInvocationNode';
@ -50,7 +51,7 @@ export const updateNode = (node: InvocationNode, template: InvocationTemplate):
// The updateability of a node, via semver comparison, relies on the this kind of recursive merge
// being valid. We rely on the template's major version to be majorly incremented if this kind of
// merge would result in an invalid node.
const clone = cloneDeep(node);
const clone = deepClone(node);
clone.data.version = template.version;
defaultsDeep(clone, defaults); // mutates!

View File

@ -1,11 +1,12 @@
import { logger } from 'app/logging/logger';
import { deepClone } from 'common/util/deepClone';
import { parseify } from 'common/util/serialize';
import type { NodesState, WorkflowsState } from 'features/nodes/store/types';
import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation';
import type { WorkflowV3 } from 'features/nodes/types/workflow';
import { zWorkflowV3 } from 'features/nodes/types/workflow';
import i18n from 'i18n';
import { cloneDeep, pick } from 'lodash-es';
import { pick } from 'lodash-es';
import { fromZodError } from 'zod-validation-error';
export type BuildWorkflowArg = {
@ -30,7 +31,7 @@ const workflowKeys = [
type BuildWorkflowFunction = (arg: BuildWorkflowArg) => WorkflowV3;
export const buildWorkflowFast: BuildWorkflowFunction = ({ nodes, edges, workflow }: BuildWorkflowArg): WorkflowV3 => {
const clonedWorkflow = pick(cloneDeep(workflow), workflowKeys);
const clonedWorkflow = pick(deepClone(workflow), workflowKeys);
const newWorkflow: WorkflowV3 = {
...clonedWorkflow,
@ -43,14 +44,14 @@ export const buildWorkflowFast: BuildWorkflowFunction = ({ nodes, edges, workflo
newWorkflow.nodes.push({
id: node.id,
type: node.type,
data: cloneDeep(node.data),
data: deepClone(node.data),
position: { ...node.position },
});
} else if (isNotesNode(node) && node.type) {
newWorkflow.nodes.push({
id: node.id,
type: node.type,
data: cloneDeep(node.data),
data: deepClone(node.data),
position: { ...node.position },
});
}

View File

@ -1,4 +1,5 @@
import { $store } from 'app/store/nanostores/store';
import { deepClone } from 'common/util/deepClone';
import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/types/error';
import type { FieldType } from 'features/nodes/types/field';
import type { InvocationNodeData } from 'features/nodes/types/invocation';
@ -11,7 +12,7 @@ import { zWorkflowV2 } from 'features/nodes/types/v2/workflow';
import type { WorkflowV3 } from 'features/nodes/types/workflow';
import { zWorkflowV3 } from 'features/nodes/types/workflow';
import { t } from 'i18next';
import { cloneDeep, forEach } from 'lodash-es';
import { forEach } from 'lodash-es';
import { z } from 'zod';
/**
@ -89,7 +90,7 @@ export const parseAndMigrateWorkflow = (data: unknown): WorkflowV3 => {
throw new WorkflowVersionError(t('nodes.unableToGetWorkflowVersion'));
}
let workflow = cloneDeep(data) as WorkflowV1 | WorkflowV2 | WorkflowV3;
let workflow = deepClone(data) as WorkflowV1 | WorkflowV2 | WorkflowV3;
if (workflow.meta.version === '1.0.0') {
const v1 = zWorkflowV1.parse(workflow);

View File

@ -280,6 +280,7 @@ const migrateGenerationState = (state: any): GenerationState => {
// The signature of the model has changed, so we need to reset it
state._version = 2;
state.model = null;
state.canvasCoherenceMode = initialGenerationState.canvasCoherenceMode;
}
return state;
};

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
__version__ = "4.0.0"
__version__ = "4.0.1"

View File

@ -98,6 +98,32 @@ def test_migrate_v3_config_from_file(tmp_path: Path, patch_rootdir: None):
assert not hasattr(config, "esrgan")
@pytest.mark.parametrize(
"legacy_conf_dir,expected_value,expected_is_set",
[
# not set, expected value is the default value
("configs/stable-diffusion", Path("configs"), False),
# not set, expected value is the default value
("configs\\stable-diffusion", Path("configs"), False),
# set, best-effort resolution of the path
("partial_custom_path/stable-diffusion", Path("partial_custom_path"), True),
# set, exact path
("full/custom/path", Path("full/custom/path"), True),
],
)
def test_migrate_v3_legacy_conf_dir_defaults(
tmp_path: Path, patch_rootdir: None, legacy_conf_dir: str, expected_value: Path, expected_is_set: bool
):
"""Test reading configuration from a file."""
config_content = f"InvokeAI:\n Paths:\n legacy_conf_dir: {legacy_conf_dir}"
temp_config_file = tmp_path / "temp_invokeai.yaml"
temp_config_file.write_text(config_content)
config = load_and_migrate_config(temp_config_file)
assert config.legacy_conf_dir == expected_value
assert ("legacy_conf_dir" in config.model_fields_set) is expected_is_set
def test_migrate_v3_backup(tmp_path: Path, patch_rootdir: None):
"""Test the backup of the config file."""
temp_config_file = tmp_path / "temp_invokeai.yaml"