From e0e106367dc2586dfaf23d30179b6bd6a33f6eb8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Feb 2024 17:26:53 +1100 Subject: [PATCH 01/44] fix(nodes): do not clear invocation stats on invoke error When an invocation errored, we clear the stats for the whole graph. Later on, we check the graph for errors and see the failed invocation, and we consider the graph failed. We then attempts to log the stats for the failed graph. Except now the failed graph has no stats, and the stats raises an error. The user sees, in the terminal: - An invocation error - A stats error (scary!) - No stats for the failed graph (uninformative!) What the user should see: - An invocation error - Graph stats The fix is simple - don't reset the graph stats when an invocation has an error. --- .../invocation_processor/invocation_processor_default.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/invokeai/app/services/invocation_processor/invocation_processor_default.py b/invokeai/app/services/invocation_processor/invocation_processor_default.py index 4951b51121..bd99be1ac0 100644 --- a/invokeai/app/services/invocation_processor/invocation_processor_default.py +++ b/invokeai/app/services/invocation_processor/invocation_processor_default.py @@ -182,8 +182,6 @@ class DefaultInvocationProcessor(InvocationProcessorABC): error_type=e.__class__.__name__, error=error, ) - with suppress(GESStatsNotFoundError): - self.__invoker.services.performance_statistics.reset_stats(graph_execution_state.id) pass # Check queue to see if this is canceled, and skip if so From 810fc19e43ff1ea258456cd427ad2ff0d030b449 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Feb 2024 17:38:25 +1100 Subject: [PATCH 02/44] feat(nodes): log stats for canceled graphs When an invocation is canceled, we consider the graph canceled. Log its graph's stats before resetting its graph's stats. No reason to not log these stats. We also should stop the profiler at this point, because this graph is finished. If we don't stop it manually, it will stop itself and write the profile to disk when it is next started, but the resultant profile will include more than just its target graph. Now we get both stats and profiles for canceled graphs. --- .../invocation_processor_default.py | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/invokeai/app/services/invocation_processor/invocation_processor_default.py b/invokeai/app/services/invocation_processor/invocation_processor_default.py index bd99be1ac0..a689a0a058 100644 --- a/invokeai/app/services/invocation_processor/invocation_processor_default.py +++ b/invokeai/app/services/invocation_processor/invocation_processor_default.py @@ -157,6 +157,13 @@ class DefaultInvocationProcessor(InvocationProcessorABC): except CanceledException: with suppress(GESStatsNotFoundError): + if profiler: + profile_path = profiler.stop() + stats_path = profile_path.with_suffix(".json") + self.__invoker.services.performance_statistics.dump_stats( + graph_execution_state_id=graph_execution_state.id, output_path=stats_path + ) + self.__invoker.services.performance_statistics.log_stats(graph_execution_state.id) self.__invoker.services.performance_statistics.reset_stats(graph_execution_state.id) pass @@ -213,20 +220,20 @@ class DefaultInvocationProcessor(InvocationProcessorABC): error=traceback.format_exc(), ) elif is_complete: + self.__invoker.services.events.emit_graph_execution_complete( + queue_batch_id=queue_item.session_queue_batch_id, + queue_item_id=queue_item.session_queue_item_id, + queue_id=queue_item.session_queue_id, + graph_execution_state_id=graph_execution_state.id, + ) + if profiler: + profile_path = profiler.stop() + stats_path = profile_path.with_suffix(".json") + self.__invoker.services.performance_statistics.dump_stats( + graph_execution_state_id=graph_execution_state.id, output_path=stats_path + ) with suppress(GESStatsNotFoundError): self.__invoker.services.performance_statistics.log_stats(graph_execution_state.id) - self.__invoker.services.events.emit_graph_execution_complete( - queue_batch_id=queue_item.session_queue_batch_id, - queue_item_id=queue_item.session_queue_item_id, - queue_id=queue_item.session_queue_id, - graph_execution_state_id=graph_execution_state.id, - ) - if profiler: - profile_path = profiler.stop() - stats_path = profile_path.with_suffix(".json") - self.__invoker.services.performance_statistics.dump_stats( - graph_execution_state_id=graph_execution_state.id, output_path=stats_path - ) self.__invoker.services.performance_statistics.reset_stats(graph_execution_state.id) except KeyboardInterrupt: From 0dc6cb0535ac3cf7752fedf2056313feb00e0538 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Feb 2024 17:41:44 +1100 Subject: [PATCH 03/44] feat(nodes): do not log stats errors The stats service was logging error messages when attempting to retrieve stats for a graph that it wasn't tracking. This was rather noisy. Instead of logging these errors within the service, we now will just raise the error and let the consumer of the service decide whether or not to log. Our usage of the service at this time is to suppress errors - we don't want to log anything to the console. Note: With the improvements in the previous two commits, we shouldn't get these errors moving forward, but I still think this change is correct. --- .../invocation_stats_default.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/invokeai/app/services/invocation_stats/invocation_stats_default.py b/invokeai/app/services/invocation_stats/invocation_stats_default.py index 501a4c04e5..be58aaad2d 100644 --- a/invokeai/app/services/invocation_stats/invocation_stats_default.py +++ b/invokeai/app/services/invocation_stats/invocation_stats_default.py @@ -106,9 +106,9 @@ class InvocationStatsService(InvocationStatsServiceBase): del self._stats[graph_execution_state_id] del self._cache_stats[graph_execution_state_id] except KeyError as e: - msg = f"Attempted to clear statistics for unknown graph {graph_execution_state_id}: {e}." - logger.error(msg) - raise GESStatsNotFoundError(msg) from e + raise GESStatsNotFoundError( + f"Attempted to clear statistics for unknown graph {graph_execution_state_id}: {e}." + ) from e def get_stats(self, graph_execution_state_id: str) -> InvocationStatsSummary: graph_stats_summary = self._get_graph_summary(graph_execution_state_id) @@ -136,9 +136,9 @@ class InvocationStatsService(InvocationStatsServiceBase): try: cache_stats = self._cache_stats[graph_execution_state_id] except KeyError as e: - msg = f"Attempted to get model cache statistics for unknown graph {graph_execution_state_id}: {e}." - logger.error(msg) - raise GESStatsNotFoundError(msg) from e + raise GESStatsNotFoundError( + f"Attempted to get model cache statistics for unknown graph {graph_execution_state_id}: {e}." + ) from e return ModelCacheStatsSummary( cache_hits=cache_stats.hits, @@ -154,9 +154,9 @@ class InvocationStatsService(InvocationStatsServiceBase): try: graph_stats = self._stats[graph_execution_state_id] except KeyError as e: - msg = f"Attempted to get graph statistics for unknown graph {graph_execution_state_id}: {e}." - logger.error(msg) - raise GESStatsNotFoundError(msg) from e + raise GESStatsNotFoundError( + f"Attempted to get graph statistics for unknown graph {graph_execution_state_id}: {e}." + ) from e return graph_stats.get_graph_stats_summary(graph_execution_state_id) @@ -164,8 +164,8 @@ class InvocationStatsService(InvocationStatsServiceBase): try: graph_stats = self._stats[graph_execution_state_id] except KeyError as e: - msg = f"Attempted to get node statistics for unknown graph {graph_execution_state_id}: {e}." - logger.error(msg) - raise GESStatsNotFoundError(msg) from e + raise GESStatsNotFoundError( + f"Attempted to get node statistics for unknown graph {graph_execution_state_id}: {e}." + ) from e return graph_stats.get_node_stats_summaries() From 79ae9c4e64cb4f64d54c25b1c487501752b8fa84 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Tue, 6 Feb 2024 17:48:54 +1100 Subject: [PATCH 04/44] feat(nodes): move profiler/stats cleanup logic to function Harder to miss something going forward. --- .../invocation_processor_default.py | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/invokeai/app/services/invocation_processor/invocation_processor_default.py b/invokeai/app/services/invocation_processor/invocation_processor_default.py index a689a0a058..54342c0da1 100644 --- a/invokeai/app/services/invocation_processor/invocation_processor_default.py +++ b/invokeai/app/services/invocation_processor/invocation_processor_default.py @@ -54,6 +54,17 @@ class DefaultInvocationProcessor(InvocationProcessorABC): else None ) + def stats_cleanup(graph_execution_state_id: str) -> None: + if profiler: + profile_path = profiler.stop() + stats_path = profile_path.with_suffix(".json") + self.__invoker.services.performance_statistics.dump_stats( + graph_execution_state_id=graph_execution_state_id, output_path=stats_path + ) + with suppress(GESStatsNotFoundError): + self.__invoker.services.performance_statistics.log_stats(graph_execution_state_id) + self.__invoker.services.performance_statistics.reset_stats(graph_execution_state_id) + while not stop_event.is_set(): try: queue_item = self.__invoker.services.queue.get() @@ -156,15 +167,7 @@ class DefaultInvocationProcessor(InvocationProcessorABC): pass except CanceledException: - with suppress(GESStatsNotFoundError): - if profiler: - profile_path = profiler.stop() - stats_path = profile_path.with_suffix(".json") - self.__invoker.services.performance_statistics.dump_stats( - graph_execution_state_id=graph_execution_state.id, output_path=stats_path - ) - self.__invoker.services.performance_statistics.log_stats(graph_execution_state.id) - self.__invoker.services.performance_statistics.reset_stats(graph_execution_state.id) + stats_cleanup(graph_execution_state.id) pass except Exception as e: @@ -226,15 +229,7 @@ class DefaultInvocationProcessor(InvocationProcessorABC): queue_id=queue_item.session_queue_id, graph_execution_state_id=graph_execution_state.id, ) - if profiler: - profile_path = profiler.stop() - stats_path = profile_path.with_suffix(".json") - self.__invoker.services.performance_statistics.dump_stats( - graph_execution_state_id=graph_execution_state.id, output_path=stats_path - ) - with suppress(GESStatsNotFoundError): - self.__invoker.services.performance_statistics.log_stats(graph_execution_state.id) - self.__invoker.services.performance_statistics.reset_stats(graph_execution_state.id) + stats_cleanup(graph_execution_state.id) except KeyboardInterrupt: pass # Log something? KeyboardInterrupt is probably not going to be seen by the processor From 800c481515ec2df950a5cbcd85ee789d80a915f2 Mon Sep 17 00:00:00 2001 From: Mary Hipp Rogers Date: Wed, 7 Feb 2024 09:14:54 -0500 Subject: [PATCH 05/44] add actions for workflow library (#5669) Co-authored-by: Mary Hipp --- .../web/src/common/hooks/useDownloadImage.ts | 6 +++- .../web/src/features/gallery/store/actions.ts | 2 ++ .../hooks/useDownloadWorkflow.ts | 34 +++++++++++-------- .../hooks/useLoadWorkflowFromFile.tsx | 2 ++ .../workflowLibrary/hooks/useSaveWorkflow.ts | 2 ++ .../hooks/useSaveWorkflowAs.ts | 2 ++ .../features/workflowLibrary/store/actions.ts | 10 ++++++ 7 files changed, 43 insertions(+), 15 deletions(-) create mode 100644 invokeai/frontend/web/src/features/workflowLibrary/store/actions.ts diff --git a/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts b/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts index 5c75549eac..3195426da3 100644 --- a/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts +++ b/invokeai/frontend/web/src/common/hooks/useDownloadImage.ts @@ -1,4 +1,6 @@ import { useAppToaster } from 'app/components/Toaster'; +import { useAppDispatch } from 'app/store/storeHooks'; +import { imageDownloaded } from 'features/gallery/store/actions'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -8,6 +10,7 @@ export const useDownloadImage = () => { const toaster = useAppToaster(); const { t } = useTranslation(); const imageUrlToBlob = useImageUrlToBlob(); + const dispatch = useAppDispatch(); const downloadImage = useCallback( async (image_url: string, image_name: string) => { @@ -26,6 +29,7 @@ export const useDownloadImage = () => { document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); + dispatch(imageDownloaded()); } catch (err) { toaster({ title: t('toast.problemDownloadingImage'), @@ -36,7 +40,7 @@ export const useDownloadImage = () => { }); } }, - [t, toaster, imageUrlToBlob] + [t, toaster, imageUrlToBlob, dispatch] ); return { downloadImage }; diff --git a/invokeai/frontend/web/src/features/gallery/store/actions.ts b/invokeai/frontend/web/src/features/gallery/store/actions.ts index 4ebf4e021d..e62a350756 100644 --- a/invokeai/frontend/web/src/features/gallery/store/actions.ts +++ b/invokeai/frontend/web/src/features/gallery/store/actions.ts @@ -14,3 +14,5 @@ export const requestedBoardImagesDeletion = createAction { - const workflow = $builtWorkflow.get(); - if (!workflow) { - return; - } - const blob = new Blob([JSON.stringify(workflow, null, 2)]); - const a = document.createElement('a'); - a.href = URL.createObjectURL(blob); - a.download = `${workflow.name || 'My Workflow'}.json`; - document.body.appendChild(a); - a.click(); - a.remove(); -}; +import { workflowDownloaded } from 'features/workflowLibrary/store/actions'; +import { useCallback } from 'react'; export const useDownloadWorkflow = () => { + const dispatch = useAppDispatch(); + + const downloadWorkflow = useCallback(() => { + const workflow = $builtWorkflow.get(); + if (!workflow) { + return; + } + const blob = new Blob([JSON.stringify(workflow, null, 2)]); + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = `${workflow.name || 'My Workflow'}.json`; + document.body.appendChild(a); + a.click(); + a.remove(); + dispatch(workflowDownloaded()); + }, [dispatch]); + return downloadWorkflow; }; diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx index a6f5f16497..0b284ee7db 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useLoadWorkflowFromFile.tsx @@ -3,6 +3,7 @@ import { useAppDispatch } from 'app/store/storeHooks'; import { workflowLoadRequested } from 'features/nodes/store/actions'; import { addToast } from 'features/system/store/systemSlice'; import { makeToast } from 'features/system/util/makeToast'; +import { workflowLoadedFromFile } from 'features/workflowLibrary/store/actions'; import type { RefObject } from 'react'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -29,6 +30,7 @@ export const useLoadWorkflowFromFile: UseLoadWorkflowFromFile = ({ resetRef }) = try { const parsedJSON = JSON.parse(String(rawJSON)); dispatch(workflowLoadRequested({ workflow: parsedJSON, asCopy: true })); + dispatch(workflowLoadedFromFile()); } catch (e) { // There was a problem reading the file logger.error(t('nodes.unableToLoadWorkflow')); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts index 78d7071c20..5d484b6897 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts @@ -4,6 +4,7 @@ import { useAppDispatch } from 'app/store/storeHooks'; import { $builtWorkflow } from 'features/nodes/hooks/useWorkflowWatcher'; import { workflowIDChanged, workflowSaved } from 'features/nodes/store/workflowSlice'; import type { WorkflowV2 } from 'features/nodes/types/workflow'; +import { workflowUpdated } from 'features/workflowLibrary/store/actions'; import { useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useCreateWorkflowMutation, useUpdateWorkflowMutation, workflowsApi } from 'services/api/endpoints/workflows'; @@ -41,6 +42,7 @@ export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => { try { if (isWorkflowWithID(workflow)) { await updateWorkflow(workflow).unwrap(); + dispatch(workflowUpdated()); } else { const data = await createWorkflow(workflow).unwrap(); dispatch(workflowIDChanged(data.workflow.id)); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts index edf87e81ab..33fd5545e6 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts @@ -9,6 +9,7 @@ import { workflowSaved, } from 'features/nodes/store/workflowSlice'; import type { WorkflowCategory } from 'features/nodes/types/workflow'; +import { newWorkflowSaved } from 'features/workflowLibrary/store/actions'; import { useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useCreateWorkflowMutation, workflowsApi } from 'services/api/endpoints/workflows'; @@ -56,6 +57,7 @@ export const useSaveWorkflowAs: UseSaveWorkflowAs = () => { dispatch(workflowNameChanged(data.workflow.name)); dispatch(workflowCategoryChanged(data.workflow.meta.category)); dispatch(workflowSaved()); + dispatch(newWorkflowSaved({ category })); onSuccess && onSuccess(); toast.update(toastRef.current, { diff --git a/invokeai/frontend/web/src/features/workflowLibrary/store/actions.ts b/invokeai/frontend/web/src/features/workflowLibrary/store/actions.ts new file mode 100644 index 0000000000..33100a9c9d --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/store/actions.ts @@ -0,0 +1,10 @@ +import { createAction } from '@reduxjs/toolkit'; +import type { WorkflowCategory } from 'features/nodes/types/workflow'; + +export const workflowDownloaded = createAction('workflowLibrary/workflowDownloaded'); + +export const workflowLoadedFromFile = createAction('workflowLibrary/workflowLoadedFromFile'); + +export const newWorkflowSaved = createAction<{ category: WorkflowCategory }>('workflowLibrary/newWorkflowSaved'); + +export const workflowUpdated = createAction('workflowLibrary/workflowUpdated'); From 400d66fa5d94b4897b88bd4624943e898e28c1e4 Mon Sep 17 00:00:00 2001 From: blessedcoolant <54517381+blessedcoolant@users.noreply.github.com> Date: Wed, 7 Feb 2024 19:55:04 +0530 Subject: [PATCH 06/44] community node: BriaAI RMBG 1.4 --- docs/nodes/communityNodes.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/nodes/communityNodes.md b/docs/nodes/communityNodes.md index fa17b42411..906111f45b 100644 --- a/docs/nodes/communityNodes.md +++ b/docs/nodes/communityNodes.md @@ -42,6 +42,7 @@ To use a community workflow, download the the `.json` node graph file and load i + [Oobabooga](#oobabooga) + [Prompt Tools](#prompt-tools) + [Remote Image](#remote-image) + + [BriaAI Background Remove](#briaai-remove-background) + [Remove Background](#remove-background) + [Retroize](#retroize) + [Size Stepper Nodes](#size-stepper-nodes) @@ -434,6 +435,17 @@ See full docs here: https://github.com/skunkworxdark/Prompt-tools-nodes/edit/mai **Node Link:** https://github.com/fieldOfView/InvokeAI-remote_image +-------------------------------- + +### BriaAI Remove Background + +**Description**: Implements one click background removal with BriaAI's new version 1.4 model which seems to be be producing better results than any other previous background removal tool. + +**Node Link:** https://github.com/blessedcoolant/invoke_bria_rmbg + +**View** + + -------------------------------- ### Remove Background From 35bf7ee66d797b2bf1bfc7bc3f1e765f3aad6394 Mon Sep 17 00:00:00 2001 From: Wubbbi Date: Wed, 7 Feb 2024 17:58:28 +0100 Subject: [PATCH 07/44] Minor dep updates --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b2505ffe97..694ae707f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,10 +38,10 @@ dependencies = [ "clip_anytorch==2.5.2", # replacing "clip @ https://github.com/openai/CLIP/archive/eaa22acb90a5876642d0507623e859909230a52d.zip", "compel==2.0.2", "controlnet-aux==0.0.7", - "diffusers[torch]==0.26.1", + "diffusers[torch]==0.26.2", "invisible-watermark==0.2.0", # needed to install SDXL base and refiner using their repo_ids "mediapipe==0.10.7", # needed for "mediapipeface" controlnet model - "numpy==1.26.3", # >1.24.0 is needed to use the 'strict' argument to np.testing.assert_array_equal() + "numpy==1.26.4", # >1.24.0 is needed to use the 'strict' argument to np.testing.assert_array_equal() "onnx==1.15.0", "onnxruntime==1.16.3", "opencv-python==4.9.0.80", From 3dcbb79ef7d22f8985a0ecebf3c2ee4f768d240e Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Sun, 28 Jan 2024 23:49:22 -0500 Subject: [PATCH 08/44] chore(installer): typing pass --- installer/lib/installer.py | 21 +++++++++++++-------- installer/lib/messages.py | 15 ++++++++------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/installer/lib/installer.py b/installer/lib/installer.py index 5227f181d8..2b19d0f2c5 100644 --- a/installer/lib/installer.py +++ b/installer/lib/installer.py @@ -11,7 +11,7 @@ import sys import venv from pathlib import Path from tempfile import TemporaryDirectory -from typing import Union +from typing import Optional, Tuple SUPPORTED_PYTHON = ">=3.10.0,<=3.11.100" INSTALLER_REQS = ["rich", "semver", "requests", "plumbum", "prompt-toolkit"] @@ -78,7 +78,7 @@ class Installer: return venv_dir - def bootstrap(self, verbose: bool = False) -> TemporaryDirectory: + def bootstrap(self, verbose: bool = False) -> TemporaryDirectory | None: """ Bootstrap the installer venv with packages required at install time @@ -102,7 +102,7 @@ class Installer: except subprocess.CalledProcessError as e: print(e) - def app_venv(self, path: str = None): + def app_venv(self, path: Optional[str] = None) -> Path: """ Create a virtualenv for the InvokeAI installation """ @@ -115,6 +115,7 @@ class Installer: # experimental / testing elif not FF_VENV_IN_RUNTIME: + venv_dir_parent = "" if OS == "Windows": venv_dir_parent = os.getenv("APPDATA", "~/AppData/Roaming") elif OS == "Darwin": @@ -141,7 +142,7 @@ class Installer: return venv_dir def install( - self, root: str = "~/invokeai", version: str = "latest", yes_to_all=False, find_links: Path = None + self, root: str = "~/invokeai", version: str = "latest", yes_to_all=False, find_links: Optional[Path] = None ) -> None: """ Install the InvokeAI application into the given runtime path @@ -160,8 +161,11 @@ class Installer: messages.welcome() - default_path = os.environ.get("INVOKEAI_ROOT") or Path(root).expanduser().resolve() + default_path = Path(os.environ.get("INVOKEAI_ROOT") or Path(root).expanduser().resolve()) self.dest = default_path if yes_to_all else messages.dest_path(root) + if self.dest is None: + print("Could not find or create the destination directory. Installation cancelled.") + sys.exit(0) # create the venv for the app self.venv = self.app_venv() @@ -233,7 +237,7 @@ class InvokeAiInstance: Install PyTorch """ - from plumbum import FG, local + from plumbum import FG, local # type: ignore pip = local[self.pip] @@ -292,6 +296,7 @@ class InvokeAiInstance: next(src.glob("invokeai")) except StopIteration: print("Unable to find a wheel or perform a source install. Giving up.") + src = "" elif version == "source": # this makes an assumption about the location of the installer package in the source tree @@ -300,7 +305,7 @@ class InvokeAiInstance: # will install from PyPi src = f"invokeai=={version}" if version is not None else "invokeai" - from plumbum import FG, local + from plumbum import FG, local # type: ignore pip = local[self.pip] @@ -431,7 +436,7 @@ def set_sys_path(venv_path: Path) -> None: sys.path.append(str(Path(venv_path, lib, "site-packages").expanduser().resolve())) -def get_torch_source() -> (Union[str, None], str): +def get_torch_source() -> Tuple[str | None, str | None]: """ Determine the extra index URL for pip to use for torch installation. This depends on the OS and the graphics accelerator in use. diff --git a/installer/lib/messages.py b/installer/lib/messages.py index 6d95eaff59..c2015e6678 100644 --- a/installer/lib/messages.py +++ b/installer/lib/messages.py @@ -109,7 +109,7 @@ def user_wants_auto_configuration() -> bool: return choice.lower().startswith("a") -def dest_path(dest=None) -> Path: +def dest_path(dest=None) -> Path | None: """ Prompt the user for the destination path and create the path @@ -137,7 +137,7 @@ def dest_path(dest=None) -> Path: path_completer = PathCompleter( only_directories=True, expanduser=True, - get_paths=lambda: [browse_start], # noqa: B023 + get_paths=lambda: [str(browse_start)], # noqa: B023 # get_paths=lambda: [".."].extend(list(browse_start.iterdir())) ) @@ -217,6 +217,7 @@ def graphical_accelerator(): "idk", ) + options = [] if OS == "Windows": options = [nvidia, nvidia_with_dml, cpu] if OS == "Linux": @@ -230,7 +231,7 @@ def graphical_accelerator(): return options[0][1] # "I don't know" is always added the last option - options.append(idk) + options.append(idk) # type: ignore options = {str(i): opt for i, opt in enumerate(options, 1)} @@ -291,7 +292,7 @@ def windows_long_paths_registry() -> None: """ with open(str(Path(__file__).parent / "WinLongPathsEnabled.reg"), "r", encoding="utf-16le") as code: - syntax = Syntax(code.read(), line_numbers=True) + syntax = Syntax(code.read(), line_numbers=True, lexer="regedit") console.print( Panel( @@ -301,7 +302,7 @@ def windows_long_paths_registry() -> None: "We will now apply a registry fix to enable long paths on Windows. InvokeAI needs this to function correctly. We are asking your permission to modify the Windows Registry on your behalf.", "", "This is the change that will be applied:", - syntax, + str(syntax), ] ) ), @@ -340,7 +341,7 @@ def introduction() -> None: console.line(2) -def _platform_specific_help() -> str: +def _platform_specific_help() -> Text: if OS == "Darwin": text = Text.from_markup( """[b wheat1]macOS Users![/]\n\nPlease be sure you have the [b wheat1]Xcode command-line tools[/] installed before continuing.\nIf not, cancel with [i]Control-C[/] and follow the Xcode install instructions at [deep_sky_blue1]https://www.freecodecamp.org/news/install-xcode-command-line-tools/[/].""" @@ -354,5 +355,5 @@ def _platform_specific_help() -> str: [deep_sky_blue1]https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170[/]""" ) else: - text = "" + text = Text() return text From 82c3c7fc38e04967e9e5eb6fd509000348d275c1 Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Mon, 29 Jan 2024 00:54:05 -0500 Subject: [PATCH 09/44] tidy(installer): remove unused experimental venv location --- installer/lib/installer.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/installer/lib/installer.py b/installer/lib/installer.py index 2b19d0f2c5..de48094b3e 100644 --- a/installer/lib/installer.py +++ b/installer/lib/installer.py @@ -21,10 +21,6 @@ OS = platform.uname().system ARCH = platform.uname().machine VERSION = "latest" -### Feature flags -# Install the virtualenv into the runtime dir -FF_VENV_IN_RUNTIME = True - # Install the wheel packaged with the installer FF_USE_LOCAL_WHEEL = True @@ -113,20 +109,6 @@ class Installer: if path is not None: venv_dir = Path(path) - # experimental / testing - elif not FF_VENV_IN_RUNTIME: - venv_dir_parent = "" - if OS == "Windows": - venv_dir_parent = os.getenv("APPDATA", "~/AppData/Roaming") - elif OS == "Darwin": - # there is no environment variable on macOS to find this - # TODO: confirm this is working as expected - venv_dir_parent = "~/Library/Application Support" - elif OS == "Linux": - venv_dir_parent = os.getenv("XDG_DATA_DIR", "~/.local/share") - venv_dir = Path(venv_dir_parent).expanduser().resolve() / f"InvokeAI/{VERSION}/venv" - - # stable / current else: venv_dir = self.dest / ".venv" From 32b1e974ca216e5cd2e82235db8ccc7420f6e85a Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Mon, 29 Jan 2024 00:58:32 -0500 Subject: [PATCH 10/44] feat(installer): install from PyPi instead of using prepackaged wheel --- installer/lib/installer.py | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/installer/lib/installer.py b/installer/lib/installer.py index de48094b3e..84ac6d6cd6 100644 --- a/installer/lib/installer.py +++ b/installer/lib/installer.py @@ -21,10 +21,6 @@ OS = platform.uname().system ARCH = platform.uname().machine VERSION = "latest" -# Install the wheel packaged with the installer -FF_USE_LOCAL_WHEEL = True - - class Installer: """ Deploys an InvokeAI installation into a given path @@ -265,27 +261,7 @@ class InvokeAiInstance: version = self.version pre = None - ## TODO: only local wheel will be installed as of now; support for --version arg is TODO - if FF_USE_LOCAL_WHEEL: - # if no wheel, try to do a source install before giving up - try: - src = str(next(Path(__file__).parent.glob("InvokeAI-*.whl"))) - except StopIteration: - try: - src = Path(__file__).parents[1].expanduser().resolve() - # if the above directory contains one of these files, we'll do a source install - next(src.glob("pyproject.toml")) - next(src.glob("invokeai")) - except StopIteration: - print("Unable to find a wheel or perform a source install. Giving up.") - src = "" - - elif version == "source": - # this makes an assumption about the location of the installer package in the source tree - src = Path(__file__).parents[1].expanduser().resolve() - else: - # will install from PyPi - src = f"invokeai=={version}" if version is not None else "invokeai" + src = f"invokeai=={version}" if version is not None else "invokeai" from plumbum import FG, local # type: ignore From 7162ff04dfef5307f061d8bf8934621e4a7528af Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Mon, 29 Jan 2024 00:59:32 -0500 Subject: [PATCH 11/44] tidy(installer): do not preinstall torch separately --- installer/lib/installer.py | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/installer/lib/installer.py b/installer/lib/installer.py index 84ac6d6cd6..c041dcbf19 100644 --- a/installer/lib/installer.py +++ b/installer/lib/installer.py @@ -202,42 +202,9 @@ class InvokeAiInstance: import messages - # install torch first to ensure the correct version gets installed. - # works with either source or wheel install with negligible impact on installation times. - messages.simple_banner("Installing PyTorch :fire:") - self.install_torch(extra_index_url, find_links) - messages.simple_banner("Installing the InvokeAI Application :art:") self.install_app(extra_index_url, optional_modules, find_links) - def install_torch(self, extra_index_url=None, find_links=None): - """ - Install PyTorch - """ - - from plumbum import FG, local # type: ignore - - pip = local[self.pip] - - ( - pip[ - "install", - "--require-virtualenv", - "numpy==1.26.3", # choose versions that won't be uninstalled during phase 2 - "urllib3~=1.26.0", - "requests~=2.28.0", - "torch==2.1.2", - "torchmetrics==0.11.4", - "torchvision==0.16.2", - "--force-reinstall", - "--find-links" if find_links is not None else None, - find_links, - "--extra-index-url" if extra_index_url is not None else None, - extra_index_url, - ] - & FG - ) - def install_app(self, extra_index_url=None, optional_modules=None, find_links=None): """ Install the application with pip. From 03b1cde97d0e6f3eda9e332b069490693fa64625 Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Thu, 1 Feb 2024 17:25:50 -0500 Subject: [PATCH 12/44] tidy(installer): remove unused update scripts and references thereto --- installer/lib/installer.py | 1 - installer/templates/update.bat.in | 72 ------------------------------- installer/templates/update.sh.in | 58 ------------------------- 3 files changed, 131 deletions(-) delete mode 100644 installer/templates/update.bat.in delete mode 100644 installer/templates/update.sh.in diff --git a/installer/lib/installer.py b/installer/lib/installer.py index c041dcbf19..9027019b6c 100644 --- a/installer/lib/installer.py +++ b/installer/lib/installer.py @@ -303,7 +303,6 @@ class InvokeAiInstance: ext = "bat" if OS == "Windows" else "sh" - # scripts = ['invoke', 'update'] scripts = ["invoke"] for script in scripts: diff --git a/installer/templates/update.bat.in b/installer/templates/update.bat.in deleted file mode 100644 index 00caa2d9b7..0000000000 --- a/installer/templates/update.bat.in +++ /dev/null @@ -1,72 +0,0 @@ -@echo off -setlocal EnableExtensions EnableDelayedExpansion - -PUSHD "%~dp0" - -set INVOKE_AI_VERSION=latest -set arg=%1 -if "%arg%" neq "" ( - if "%arg:~0,2%" equ "/?" ( - echo Usage: update.bat ^ - echo Updates InvokeAI to use the indicated version of the code base. - echo Find the version or branch for the release you want, and pass it as the argument. - echo For example '.\update.bat v2.2.5' for release 2.2.5. - echo '.\update.bat main' for the latest development version - echo. - echo If no argument provided then will install the most recent release, equivalent to - echo '.\update.bat latest' - exit /b - ) else ( - set INVOKE_AI_VERSION=%arg% - ) -) - -set INVOKE_AI_SRC="https://github.com/invoke-ai/InvokeAI/archive/!INVOKE_AI_VERSION!.zip" -set INVOKE_AI_DEP=https://raw.githubusercontent.com/invoke-ai/InvokeAI/!INVOKE_AI_VERSION!/environments-and-requirements/requirements-base.txt -set INVOKE_AI_MODELS=https://raw.githubusercontent.com/invoke-ai/InvokeAI/$INVOKE_AI_VERSION/configs/INITIAL_MODELS.yaml - -call curl -I "%INVOKE_AI_DEP%" -fs >.tmp.out -if %errorlevel% neq 0 ( - echo '!INVOKE_AI_VERSION!' is not a known branch name or tag. Please check the version and try again. - echo "Press any key to continue" - pause - exit /b -) -del .tmp.out - -echo This script will update InvokeAI and all its dependencies to !INVOKE_AI_SRC!. -echo If you do not want to do this, press control-C now! -pause - -call curl -L "%INVOKE_AI_DEP%" > environments-and-requirements/requirements-base.txt -call curl -L "%INVOKE_AI_MODELS%" > configs/INITIAL_MODELS.yaml - - -call .venv\Scripts\activate.bat -call .venv\Scripts\python -mpip install -r requirements.txt -if %errorlevel% neq 0 ( - echo Installation of requirements failed. See https://invoke-ai.github.io/InvokeAI/installation/INSTALL_AUTOMATED/#troubleshooting for suggestions. - pause - exit /b -) - -call .venv\Scripts\python -mpip install !INVOKE_AI_SRC! -if %errorlevel% neq 0 ( - echo Installation of InvokeAI failed. See https://invoke-ai.github.io/InvokeAI/installation/INSTALL_AUTOMATED/#troubleshooting for suggestions. - pause - exit /b -) - -@rem call .venv\Scripts\invokeai-configure --root=. - -@rem if %errorlevel% neq 0 ( -@rem echo Configuration InvokeAI failed. See https://invoke-ai.github.io/InvokeAI/installation/INSTALL_AUTOMATED/#troubleshooting for suggestions. -@rem pause -@rem exit /b -@rem ) - -echo InvokeAI has been updated to '%INVOKE_AI_VERSION%' - -echo "Press any key to continue" -pause -endlocal diff --git a/installer/templates/update.sh.in b/installer/templates/update.sh.in deleted file mode 100644 index f69a324575..0000000000 --- a/installer/templates/update.sh.in +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash - -set -eu - -if [ $# -ge 1 ] && [ "${1:0:2}" == "-h" ]; then - echo "Usage: update.sh " - echo "Updates InvokeAI to use the indicated version of the code base." - echo "Find the version or branch for the release you want, and pass it as the argument." - echo "For example: update.sh v2.2.5 for release 2.2.5." - echo " update.sh main for the current development version." - echo "" - echo "If no argument provided then will install the version tagged with 'latest', equivalent to" - echo "update.sh latest" - exit -1 -fi - -INVOKE_AI_VERSION=${1:-latest} - -INVOKE_AI_SRC="https://github.com/invoke-ai/InvokeAI/archive/$INVOKE_AI_VERSION.zip" -INVOKE_AI_DEP=https://raw.githubusercontent.com/invoke-ai/InvokeAI/$INVOKE_AI_VERSION/environments-and-requirements/requirements-base.txt -INVOKE_AI_MODELS=https://raw.githubusercontent.com/invoke-ai/InvokeAI/$INVOKE_AI_VERSION/configs/INITIAL_MODELS.yaml - -# ensure we're in the correct folder in case user's CWD is somewhere else -scriptdir=$(dirname "$0") -cd "$scriptdir" - -function _err_exit { - if test "$1" -ne 0 - then - echo "Something went wrong while installing InvokeAI and/or its requirements." - echo "Update cannot continue. Please report this error to https://github.com/invoke-ai/InvokeAI/issues" - echo -e "Error code $1; Error caught was '$2'" - read -p "Press any key to exit..." - exit - fi -} - -if ! curl -I "$INVOKE_AI_DEP" -fs >/dev/null; then - echo \'$INVOKE_AI_VERSION\' is not a known branch name or tag. Please check the version and try again. - exit -fi - -echo This script will update InvokeAI and all its dependencies to version \'$INVOKE_AI_VERSION\'. -echo If you do not want to do this, press control-C now! -read -p "Press any key to continue, or CTRL-C to exit..." - -curl -L "$INVOKE_AI_DEP" > environments-and-requirements/requirements-base.txt -curl -L "$INVOKE_AI_MODELS" > configs/INITIAL_MODELS.yaml - -. .venv/bin/activate - -./.venv/bin/python -mpip install -r requirements.txt -_err_exit $? "The pip program failed to install InvokeAI's requirements." - -./.venv/bin/python -mpip install $INVOKE_AI_SRC -_err_exit $? "The pip program failed to install InvokeAI." - -echo InvokeAI updated to \'$INVOKE_AI_VERSION\' From d47905d2fb9cb8c3e5799f8a7fa7b626e890952a Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Thu, 1 Feb 2024 19:06:43 -0500 Subject: [PATCH 13/44] chore(installer): reorder messages in util script fail fast if there's a virtualenv activated --- installer/create_installer.sh | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/installer/create_installer.sh b/installer/create_installer.sh index b32f65d9bf..46c2783bfa 100755 --- a/installer/create_installer.sh +++ b/installer/create_installer.sh @@ -17,8 +17,16 @@ function git_show { git show -s --format='%h %s' $1 } +if [[ -v "VIRTUAL_ENV" ]]; then + # we can't just call 'deactivate' because this function is not exported + # to the environment of this script from the bash process that runs the script + echo -e "${BRED}A virtual environment is activated. Please deactivate it before proceeding.${RESET}" + exit -1 +fi + cd "$(dirname "$0")" +echo echo -e "${BYELLOW}This script must be run from the installer directory!${RESET}" echo "The current working directory is $(pwd)" read -p "If that looks right, press any key to proceed, or CTRL-C to exit..." @@ -32,13 +40,6 @@ if ! is_bin_in_path python && is_bin_in_path python3; then } fi -if [[ -v "VIRTUAL_ENV" ]]; then - # we can't just call 'deactivate' because this function is not exported - # to the environment of this script from the bash process that runs the script - echo -e "${BRED}A virtual environment is activated. Please deactivate it before proceeding.${RESET}" - exit -1 -fi - VERSION=$( cd .. python -c "from invokeai.version import __version__ as version; print(version)" From f9fa62164ee11748b6ceb7ab4a2818c8a24d5190 Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Thu, 1 Feb 2024 19:23:01 -0500 Subject: [PATCH 14/44] tidy(installer): remove .whl publishing and bundling - we now install from pypi --- installer/create_installer.sh | 36 ++--------------------------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/installer/create_installer.sh b/installer/create_installer.sh index 46c2783bfa..3b54443449 100755 --- a/installer/create_installer.sh +++ b/installer/create_installer.sh @@ -14,7 +14,7 @@ function is_bin_in_path { } function git_show { - git show -s --format='%h %s' $1 + git show -s --format=oneline --abbrev-commit "$1" | cat } if [[ -v "VIRTUAL_ENV" ]]; then @@ -48,38 +48,9 @@ PATCH="" VERSION="v${VERSION}${PATCH}" echo -e "${BGREEN}HEAD${RESET}:" -git_show +git_show HEAD echo -# ---------------------- FRONTEND ---------------------- - -pushd ../invokeai/frontend/web >/dev/null -echo -echo "Installing frontend dependencies..." -echo -pnpm i --frozen-lockfile -echo -echo "Building frontend..." -echo -pnpm build -popd - -# ---------------------- BACKEND ---------------------- - -echo -echo "Building wheel..." -echo - -# install the 'build' package in the user site packages, if needed -# could be improved by using a temporary venv, but it's tiny and harmless -if [[ $(python -c 'from importlib.util import find_spec; print(find_spec("build") is None)') == "True" ]]; then - pip install --user build -fi - -rm -rf ../build - -python -m build --wheel --outdir dist/ ../. - # ---------------------- echo @@ -98,9 +69,6 @@ done mkdir InvokeAI-Installer/lib cp lib/*.py InvokeAI-Installer/lib -# Move the wheel -mv dist/*.whl InvokeAI-Installer/lib/ - # Install scripts # Mac/Linux cp install.sh.in InvokeAI-Installer/install.sh From dfc8d1bb1055812f25ead08c2886a56f067dc040 Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Thu, 1 Feb 2024 19:25:23 -0500 Subject: [PATCH 15/44] tidy(installer): remove unused argument / env var --- installer/create_installer.sh | 2 +- installer/install.bat.in | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/installer/create_installer.sh b/installer/create_installer.sh index 3b54443449..57b681c42e 100755 --- a/installer/create_installer.sh +++ b/installer/create_installer.sh @@ -75,7 +75,7 @@ cp install.sh.in InvokeAI-Installer/install.sh chmod a+x InvokeAI-Installer/install.sh # Windows -perl -p -e "s/^set INVOKEAI_VERSION=.*/set INVOKEAI_VERSION=$VERSION/" install.bat.in >InvokeAI-Installer/install.bat +cp install.bat.in InvokeAI-Installer/install.bat cp WinLongPathsEnabled.reg InvokeAI-Installer/ # Zip everything up diff --git a/installer/install.bat.in b/installer/install.bat.in index 5fa76471de..9a487d3ee2 100644 --- a/installer/install.bat.in +++ b/installer/install.bat.in @@ -15,7 +15,6 @@ if "%1" == "use-cache" ( @rem Config @rem The version in the next line is replaced by an up to date release number @rem when create_installer.sh is run. Change the release number there. -set INVOKEAI_VERSION=latest set INSTRUCTIONS=https://invoke-ai.github.io/InvokeAI/installation/INSTALL_AUTOMATED/ set TROUBLESHOOTING=https://invoke-ai.github.io/InvokeAI/installation/INSTALL_AUTOMATED/#troubleshooting set PYTHON_URL=https://www.python.org/downloads/windows/ From 5b7b1122cbf600cf11eb5f594de04eae63fec42d Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Thu, 1 Feb 2024 19:45:46 -0500 Subject: [PATCH 16/44] tidy(installer): clean up unused code --- installer/lib/installer.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/installer/lib/installer.py b/installer/lib/installer.py index 9027019b6c..525e5f894a 100644 --- a/installer/lib/installer.py +++ b/installer/lib/installer.py @@ -28,25 +28,11 @@ class Installer: def __init__(self) -> None: self.reqs = INSTALLER_REQS - self.preflight() if os.getenv("VIRTUAL_ENV") is not None: print("A virtual environment is already activated. Please 'deactivate' before installation.") sys.exit(-1) self.bootstrap() - def preflight(self) -> None: - """ - Preflight checks - """ - - # TODO - # verify python version - # on macOS verify XCode tools are present - # verify libmesa, libglx on linux - # check that the system arch is not i386 (?) - # check that the system has a GPU, and the type of GPU - - pass def mktemp_venv(self) -> TemporaryDirectory: """ From 60eea09629e5b8bcbdd4bd0169189bc79c2ce026 Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Thu, 1 Feb 2024 19:48:57 -0500 Subject: [PATCH 17/44] feat(installer): *always* force-reinstall This has repeatedly shown itself useful in fixing install issues, especially regarding pytorch CPU/GPU version, so there is little downside to making this the default. Performance impact of this should be negligible. Packages will be reinstalled from pip cache if possible, and downloaded only if necessary. Impact may be felt on slower disks. --- installer/lib/installer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/installer/lib/installer.py b/installer/lib/installer.py index 525e5f894a..bfb30d6cca 100644 --- a/installer/lib/installer.py +++ b/installer/lib/installer.py @@ -220,10 +220,11 @@ class InvokeAiInstance: pip = local[self.pip] - ( + _ = ( pip[ "install", "--require-virtualenv", + "--force-reinstall", "--use-pep517", str(src) + (optional_modules if optional_modules else ""), "--find-links" if find_links is not None else None, From d397beaa471b9eac3e9ef9c477b6e28892b94e6c Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Thu, 1 Feb 2024 20:08:13 -0500 Subject: [PATCH 18/44] fix(installer): upgrade the temporary pip before installation --- installer/lib/installer.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/installer/lib/installer.py b/installer/lib/installer.py index bfb30d6cca..0d6d414ab9 100644 --- a/installer/lib/installer.py +++ b/installer/lib/installer.py @@ -21,6 +21,7 @@ OS = platform.uname().system ARCH = platform.uname().machine VERSION = "latest" + class Installer: """ Deploys an InvokeAI installation into a given path @@ -59,9 +60,6 @@ class Installer: def bootstrap(self, verbose: bool = False) -> TemporaryDirectory | None: """ Bootstrap the installer venv with packages required at install time - - :return: path to the virtual environment directory that was bootstrapped - :rtype: TemporaryDirectory """ print("Initializing the installer. This may take a minute - please wait...") @@ -73,9 +71,17 @@ class Installer: cmd.extend(self.reqs) try: - res = subprocess.check_output(cmd).decode() + # upgrade pip to the latest version to avoid a confusing message + res = subprocess.check_output([pip, "install", "--upgrade", "pip"]).decode() if verbose: print(res) + + # run the install prerequisites installation + res = subprocess.check_output(cmd).decode() + + if verbose: + print(res) + return venv_dir except subprocess.CalledProcessError as e: print(e) From 1c8fc908b25cd6a34beeb213742ff2d1d9574aaf Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Fri, 2 Feb 2024 20:52:14 -0500 Subject: [PATCH 19/44] fix(installer): minor logic fixes --- installer/lib/installer.py | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/installer/lib/installer.py b/installer/lib/installer.py index 0d6d414ab9..beaa119d54 100644 --- a/installer/lib/installer.py +++ b/installer/lib/installer.py @@ -27,14 +27,14 @@ class Installer: Deploys an InvokeAI installation into a given path """ + reqs: list[str] = INSTALLER_REQS + def __init__(self) -> None: - self.reqs = INSTALLER_REQS if os.getenv("VIRTUAL_ENV") is not None: print("A virtual environment is already activated. Please 'deactivate' before installation.") sys.exit(-1) self.bootstrap() - def mktemp_venv(self) -> TemporaryDirectory: """ Creates a temporary virtual environment for the installer itself @@ -86,19 +86,12 @@ class Installer: except subprocess.CalledProcessError as e: print(e) - def app_venv(self, path: Optional[str] = None) -> Path: + def app_venv(self, venv_parent) -> Path: """ Create a virtualenv for the InvokeAI installation """ - # explicit venv location - # currently unused in normal operation - # useful for testing or special cases - if path is not None: - venv_dir = Path(path) - - else: - venv_dir = self.dest / ".venv" + venv_dir = venv_parent / ".venv" # Prefer to copy python executables # so that updates to system python don't break InvokeAI @@ -131,16 +124,16 @@ class Installer: messages.welcome() - default_path = Path(os.environ.get("INVOKEAI_ROOT") or Path(root).expanduser().resolve()) - self.dest = default_path if yes_to_all else messages.dest_path(root) - if self.dest is None: + auto_dest = Path(os.environ.get("INVOKEAI_ROOT", root)).expanduser().resolve() + destination = auto_dest if yes_to_all else messages.dest_path(root) + if destination is None: print("Could not find or create the destination directory. Installation cancelled.") sys.exit(0) # create the venv for the app - self.venv = self.app_venv() + self.venv = self.app_venv(venv_parent=destination) - self.instance = InvokeAiInstance(runtime=self.dest, venv=self.venv, version=version) + self.instance = InvokeAiInstance(runtime=destination, venv=self.venv, version=version) # install dependencies and the InvokeAI application (extra_index_url, optional_modules) = get_torch_source() if not yes_to_all else (None, None) From ca2bb6f0ccc79ae489d53005004ebe67fd69e3b7 Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Fri, 2 Feb 2024 20:53:18 -0500 Subject: [PATCH 20/44] fix(installer): bubble up exceptions during install --- installer/lib/installer.py | 39 ++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/installer/lib/installer.py b/installer/lib/installer.py index beaa119d54..c28e94d720 100644 --- a/installer/lib/installer.py +++ b/installer/lib/installer.py @@ -215,25 +215,32 @@ class InvokeAiInstance: src = f"invokeai=={version}" if version is not None else "invokeai" - from plumbum import FG, local # type: ignore + from plumbum import FG, ProcessExecutionError, local # type: ignore pip = local[self.pip] + _ = pip["install", "--upgrade", "pip"] & FG - _ = ( - pip[ - "install", - "--require-virtualenv", - "--force-reinstall", - "--use-pep517", - str(src) + (optional_modules if optional_modules else ""), - "--find-links" if find_links is not None else None, - find_links, - "--extra-index-url" if extra_index_url is not None else None, - extra_index_url, - pre, - ] - & FG - ) + pipeline = pip[ + "install", + "--require-virtualenv", + "--force-reinstall", + "--use-pep517", + str(src) + (optional_modules if optional_modules else ""), + "--find-links" if find_links is not None else None, + find_links, + "--extra-index-url" if extra_index_url is not None else None, + extra_index_url, + pre, + ] + + try: + _ = pipeline & FG + except ProcessExecutionError as e: + print(f"Error: {e}") + print( + "Could not install InvokeAI. Please try downloading the latest version of the installer and install again." + ) + sys.exit(1) def configure(self): """ From 29bcc4b59524fb6c7fa39317e2656e8b95fe3488 Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Mon, 5 Feb 2024 11:51:01 -0500 Subject: [PATCH 21/44] fix(installer) slightly better typing for GPU selection --- installer/lib/installer.py | 23 +++++++++++----------- installer/lib/messages.py | 40 +++++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/installer/lib/installer.py b/installer/lib/installer.py index c28e94d720..dcd542a805 100644 --- a/installer/lib/installer.py +++ b/installer/lib/installer.py @@ -368,25 +368,26 @@ def get_torch_source() -> Tuple[str | None, str | None]: :rtype: list """ - from messages import graphical_accelerator + from messages import select_gpu - # device can be one of: "cuda", "rocm", "cpu", "idk" - device = graphical_accelerator() + # device can be one of: "cuda", "rocm", "cpu", "cuda_and_dml, autodetect" + device = select_gpu() url = None optional_modules = "[onnx]" if OS == "Linux": - if device == "rocm": + if device.value == "rocm": url = "https://download.pytorch.org/whl/rocm5.6" - elif device == "cpu": + elif device.value == "cpu": url = "https://download.pytorch.org/whl/cpu" - if device == "cuda": - url = "https://download.pytorch.org/whl/cu121" - optional_modules = "[xformers,onnx-cuda]" - if device == "cuda_and_dml": - url = "https://download.pytorch.org/whl/cu121" - optional_modules = "[xformers,onnx-directml]" + elif OS == "Windows": + if device.value == "cuda": + url = "https://download.pytorch.org/whl/cu121" + optional_modules = "[xformers,onnx-cuda]" + if device.value == "cuda_and_dml": + url = "https://download.pytorch.org/whl/cu121" + optional_modules = "[xformers,onnx-directml]" # in all other cases, Torch wheels should be coming from PyPi as of Torch 1.13 diff --git a/installer/lib/messages.py b/installer/lib/messages.py index c2015e6678..954478ba6c 100644 --- a/installer/lib/messages.py +++ b/installer/lib/messages.py @@ -5,6 +5,7 @@ Installer user interaction import os import platform +from enum import Enum from pathlib import Path from prompt_toolkit import HTML, prompt @@ -182,39 +183,42 @@ def dest_path(dest=None) -> Path | None: console.rule("Goodbye!") -def graphical_accelerator(): +class GpuType(Enum): + CUDA = "cuda" + CUDA_AND_DML = "cuda_and_dml" + ROCM = "rocm" + CPU = "cpu" + AUTODETECT = "autodetect" + + +def select_gpu() -> GpuType: """ - Prompt the user to select the graphical accelerator in their system - This does not validate user's choices (yet), but only offers choices - valid for the platform. - CUDA is the fallback. - We may be able to detect the GPU driver by shelling out to `modprobe` or `lspci`, - but this is not yet supported or reliable. Also, some users may have exotic preferences. + Prompt the user to select the GPU driver """ if ARCH == "arm64" and OS != "Darwin": print(f"Only CPU acceleration is available on {ARCH} architecture. Proceeding with that.") - return "cpu" + return GpuType.CPU nvidia = ( "an [gold1 b]NVIDIA[/] GPU (using CUDA™)", - "cuda", + GpuType.CUDA, ) nvidia_with_dml = ( "an [gold1 b]NVIDIA[/] GPU (using CUDA™, and DirectML™ for ONNX) -- ALPHA", - "cuda_and_dml", + GpuType.CUDA_AND_DML, ) amd = ( "an [gold1 b]AMD[/] GPU (using ROCm™)", - "rocm", + GpuType.ROCM, ) cpu = ( - "no compatible GPU, or specifically prefer to use the CPU", - "cpu", + "Do not install any GPU support, use CPU for generation (slow)", + GpuType.CPU, ) - idk = ( + autodetect = ( "I'm not sure what to choose", - "idk", + GpuType.AUTODETECT, ) options = [] @@ -231,7 +235,7 @@ def graphical_accelerator(): return options[0][1] # "I don't know" is always added the last option - options.append(idk) # type: ignore + options.append(autodetect) # type: ignore options = {str(i): opt for i, opt in enumerate(options, 1)} @@ -266,9 +270,9 @@ def graphical_accelerator(): ), ) - if options[choice][1] == "idk": + if options[choice][1] is GpuType.AUTODETECT: console.print( - "No problem. We will try to install a version that [i]should[/i] be compatible. :crossed_fingers:" + "No problem. We will install CUDA support first :crossed_fingers: If Invoke does not detect a GPU, please re-run the installer and select one of the other GPU types." ) return options[choice][1] From 1cb866d1fc39b9f76119b69fcfd4a4652ebdd683 Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Mon, 5 Feb 2024 15:23:29 -0500 Subject: [PATCH 22/44] fix(installer): small formatting fix in welcome banner --- installer/lib/messages.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/installer/lib/messages.py b/installer/lib/messages.py index 954478ba6c..a0b3c72cb2 100644 --- a/installer/lib/messages.py +++ b/installer/lib/messages.py @@ -39,7 +39,7 @@ else: def welcome(): @group() def text(): - if (platform_specific := _platform_specific_help()) != "": + if (platform_specific := _platform_specific_help()) is not None: yield platform_specific yield "" yield Text.from_markup( @@ -345,7 +345,7 @@ def introduction() -> None: console.line(2) -def _platform_specific_help() -> Text: +def _platform_specific_help() -> Text | None: if OS == "Darwin": text = Text.from_markup( """[b wheat1]macOS Users![/]\n\nPlease be sure you have the [b wheat1]Xcode command-line tools[/] installed before continuing.\nIf not, cancel with [i]Control-C[/] and follow the Xcode install instructions at [deep_sky_blue1]https://www.freecodecamp.org/news/install-xcode-command-line-tools/[/].""" @@ -359,5 +359,5 @@ def _platform_specific_help() -> Text: [deep_sky_blue1]https://learn.microsoft.com/en-US/cpp/windows/latest-supported-vc-redist?view=msvc-170[/]""" ) else: - text = Text() + return return text From 5a816818dc0b4cc7257535b17e3b43d3208c87d1 Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Mon, 5 Feb 2024 15:34:36 -0500 Subject: [PATCH 23/44] feat(installer): get list of (pre-)releases from github api --- installer/lib/installer.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/installer/lib/installer.py b/installer/lib/installer.py index dcd542a805..8fa73f6d9d 100644 --- a/installer/lib/installer.py +++ b/installer/lib/installer.py @@ -353,6 +353,42 @@ def set_sys_path(venv_path: Path) -> None: sys.path.append(str(Path(venv_path, lib, "site-packages").expanduser().resolve())) +def get_github_releases() -> tuple[list, list] | None: + """ + Query Github for published (pre-)release versions. + Return a tuple where the first element is a list of stable releases and the second element is a list of pre-releases. + Return None if the query fails for any reason. + """ + + import requests + + ## get latest releases using github api + url = "https://api.github.com/repos/invoke-ai/InvokeAI/releases" + releases, pre_releases = [], [] + try: + res = requests.get(url) + res.raise_for_status() + tag_info = res.json() + for tag in tag_info: + if not tag["prerelease"]: + releases.append(tag["tag_name"].lstrip("v")) + else: + pre_releases.append(tag["tag_name"].lstrip("v")) + except requests.HTTPError as e: + print(f"Error: {e}") + print("Could not fetch version information from GitHub. Please check your network connection and try again.") + return + except Exception as e: + print(f"Error: {e}") + print("An unexpected error occurred while trying to fetch version information from GitHub. Please try again.") + return + + releases.sort(reverse=True) + pre_releases.sort(reverse=True) + + return releases, pre_releases + + def get_torch_source() -> Tuple[str | None, str | None]: """ Determine the extra index URL for pip to use for torch installation. From 6a8a3b50bc3f0782892047f1c3f88aca09481d03 Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Mon, 5 Feb 2024 18:58:55 -0500 Subject: [PATCH 24/44] feat(installer): add an interactive version chooser --- installer/lib/installer.py | 50 +++++++++++++++++++------------------- installer/lib/messages.py | 29 +++++++++++++++++++++- 2 files changed, 53 insertions(+), 26 deletions(-) diff --git a/installer/lib/installer.py b/installer/lib/installer.py index 8fa73f6d9d..e46397ad5f 100644 --- a/installer/lib/installer.py +++ b/installer/lib/installer.py @@ -34,6 +34,7 @@ class Installer: print("A virtual environment is already activated. Please 'deactivate' before installation.") sys.exit(-1) self.bootstrap() + self.available_releases = get_github_releases() def mktemp_venv(self) -> TemporaryDirectory: """ @@ -105,7 +106,7 @@ class Installer: return venv_dir def install( - self, root: str = "~/invokeai", version: str = "latest", yes_to_all=False, find_links: Optional[Path] = None + self, version=None, root: str = "~/invokeai", yes_to_all=False, find_links: Optional[Path] = None ) -> None: """ Install the InvokeAI application into the given runtime path @@ -122,7 +123,9 @@ class Installer: import messages - messages.welcome() + messages.welcome(self.available_releases) + + version = messages.choose_version(self.available_releases) auto_dest = Path(os.environ.get("INVOKEAI_ROOT", root)).expanduser().resolve() destination = auto_dest if yes_to_all else messages.dest_path(root) @@ -157,7 +160,7 @@ class InvokeAiInstance: A single runtime directory *may* be shared by multiple virtual environments, though this isn't currently tested or supported. """ - def __init__(self, runtime: Path, venv: Path, version: str) -> None: + def __init__(self, runtime: Path, venv: Path, version: str = "stable") -> None: self.runtime = runtime self.venv = venv self.pip = get_pip_from_venv(venv) @@ -179,21 +182,7 @@ class InvokeAiInstance: def install(self, extra_index_url=None, optional_modules=None, find_links=None): """ - Install this instance, including dependencies and the app itself - - :param extra_index_url: the "--extra-index-url ..." line for pip to look in extra indexes. - :type extra_index_url: str - """ - - import messages - - messages.simple_banner("Installing the InvokeAI Application :art:") - self.install_app(extra_index_url, optional_modules, find_links) - - def install_app(self, extra_index_url=None, optional_modules=None, find_links=None): - """ - Install the application with pip. - Supports installation from PyPi or from a local source directory. + Install the package from PyPi. :param extra_index_url: the "--extra-index-url ..." line for pip to look in extra indexes. :type extra_index_url: str @@ -205,15 +194,26 @@ class InvokeAiInstance: :type find_links: Path """ - ## this only applies to pypi installs; TODO actually use this - if self.version == "pre": + import messages + + # not currently used, but may be useful for "install most recent version" option + if self.version == "prerelease": version = None - pre = "--pre" + pre_flag = "--pre" + elif self.version == "stable": + version = None + pre_flag = None else: version = self.version - pre = None + pre_flag = None - src = f"invokeai=={version}" if version is not None else "invokeai" + src = "invokeai" + if optional_modules: + src += optional_modules + if version: + src += f"=={version}" + + messages.simple_banner("Installing the InvokeAI Application :art:") from plumbum import FG, ProcessExecutionError, local # type: ignore @@ -225,12 +225,12 @@ class InvokeAiInstance: "--require-virtualenv", "--force-reinstall", "--use-pep517", - str(src) + (optional_modules if optional_modules else ""), + str(src), "--find-links" if find_links is not None else None, find_links, "--extra-index-url" if extra_index_url is not None else None, extra_index_url, - pre, + pre_flag, ] try: diff --git a/installer/lib/messages.py b/installer/lib/messages.py index a0b3c72cb2..3a16169b92 100644 --- a/installer/lib/messages.py +++ b/installer/lib/messages.py @@ -9,7 +9,7 @@ from enum import Enum from pathlib import Path from prompt_toolkit import HTML, prompt -from prompt_toolkit.completion import PathCompleter +from prompt_toolkit.completion import FuzzyWordCompleter, PathCompleter from prompt_toolkit.validation import Validator from rich import box, print from rich.console import Console, Group, group @@ -62,6 +62,33 @@ def welcome(): console.line() +def choose_version(available_releases: tuple | None = None) -> str: + """ + Prompt the user to choose an Invoke version to install + """ + + # short circuit if we couldn't get a version list + # still try to install the latest stable version + if available_releases is None: + return "stable" + + console.print(":grey_question: [orange3]Please choose an Invoke version to install.") + + choices = available_releases[0] + available_releases[1] + + response = prompt( + message=f" to install the recommended release ({choices[0]}). or type to pick a version: ", + complete_while_typing=True, + completer=FuzzyWordCompleter(choices), + ) + + console.print(f" Version {choices[0]} will be installed.") + console.line() + + return "stable" if response == "" else response + + return response + def confirm_install(dest: Path) -> bool: if dest.exists(): print(f":exclamation: Directory {dest} already exists :exclamation:") From 97c1545ccaef65a3aad8185161ee2010dd224e4e Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Mon, 5 Feb 2024 19:00:29 -0500 Subject: [PATCH 25/44] feat(installer): show latest versions in the welcome panel --- installer/lib/messages.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/installer/lib/messages.py b/installer/lib/messages.py index 3a16169b92..0139a2f72e 100644 --- a/installer/lib/messages.py +++ b/installer/lib/messages.py @@ -36,7 +36,7 @@ else: console = Console(style=Style(color="grey74", bgcolor="grey19")) -def welcome(): +def welcome(available_releases: tuple | None = None) -> None: @group() def text(): if (platform_specific := _platform_specific_help()) is not None: @@ -46,6 +46,16 @@ def welcome(): "Some of the installation steps take a long time to run. Please be patient. If the script appears to hang for more than 10 minutes, please interrupt with [i]Control-C[/] and retry.", justify="center", ) + if available_releases is not None: + latest_stable = available_releases[0][0] + last_pre = available_releases[1][0] + yield "" + yield Text.from_markup( + f"[red3]🠶[/] Latest stable release (recommended): [b bright_white]{latest_stable}", justify="center" + ) + yield Text.from_markup( + f"[red3]🠶[/] Last published pre-release version: [b bright_white]{last_pre}", justify="center" + ) console.rule() print( From 31b9538976aac24ec5113cccc7182f1a0787949c Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Mon, 5 Feb 2024 20:28:12 -0500 Subject: [PATCH 26/44] feat(installer): improve directory selection experience --- installer/lib/messages.py | 45 ++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/installer/lib/messages.py b/installer/lib/messages.py index 0139a2f72e..16bcd56e98 100644 --- a/installer/lib/messages.py +++ b/installer/lib/messages.py @@ -97,22 +97,6 @@ def choose_version(available_releases: tuple | None = None) -> str: return "stable" if response == "" else response - return response - -def confirm_install(dest: Path) -> bool: - if dest.exists(): - print(f":exclamation: Directory {dest} already exists :exclamation:") - dest_confirmed = Confirm.ask( - ":stop_sign: (re)install in this location?", - default=False, - ) - else: - print(f"InvokeAI will be installed in {dest}") - dest_confirmed = Confirm.ask("Use this location?", default=True) - console.line() - - return dest_confirmed - def user_wants_auto_configuration() -> bool: """Prompt the user to choose between manual and auto configuration.""" @@ -147,6 +131,22 @@ def user_wants_auto_configuration() -> bool: return choice.lower().startswith("a") +def confirm_install(dest: Path) -> bool: + if dest.exists(): + print(f":stop_sign: Directory {dest} already exists!") + print(" Is this location correct?") + default = False + else: + print(f":file_folder: InvokeAI will be installed in {dest}") + default = True + + dest_confirmed = Confirm.ask(" Please confirm:", default=default) + + console.line() + + return dest_confirmed + + def dest_path(dest=None) -> Path | None: """ Prompt the user for the destination path and create the path @@ -162,15 +162,10 @@ def dest_path(dest=None) -> Path | None: else: dest = Path.cwd().expanduser().resolve() prev_dest = init_path = dest - - dest_confirmed = confirm_install(dest) + dest_confirmed = False while not dest_confirmed: - # if the given destination already exists, the starting point for browsing is its parent directory. - # the user may have made a typo, or otherwise wants to place the root dir next to an existing one. - # if the destination dir does NOT exist, then the user must have changed their mind about the selection. - # since we can't read their mind, start browsing at Path.cwd(). - browse_start = (prev_dest.parent if prev_dest.exists() else Path.cwd()).expanduser().resolve() + browse_start = (dest or Path.cwd()).expanduser().resolve() path_completer = PathCompleter( only_directories=True, @@ -180,7 +175,8 @@ def dest_path(dest=None) -> Path | None: ) console.line() - console.print(f"[orange3]Please select the destination directory for the installation:[/] \\[{browse_start}]: ") + + console.print(f":grey_question: [orange3]Please select the install destination:[/] \\[{browse_start}]: ") selected = prompt( ">>> ", complete_in_thread=True, @@ -193,6 +189,7 @@ def dest_path(dest=None) -> Path | None: ) prev_dest = dest dest = Path(selected) + console.line() dest_confirmed = confirm_install(dest.expanduser().resolve()) From 3d1b5c57eaa2c2a2dbafdc56ba6280b10edac84d Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Tue, 6 Feb 2024 09:35:24 -0500 Subject: [PATCH 27/44] fix(installer): more reliably upgrade pip --- installer/lib/installer.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/installer/lib/installer.py b/installer/lib/installer.py index e46397ad5f..229db8b10a 100644 --- a/installer/lib/installer.py +++ b/installer/lib/installer.py @@ -73,7 +73,7 @@ class Installer: try: # upgrade pip to the latest version to avoid a confusing message - res = subprocess.check_output([pip, "install", "--upgrade", "pip"]).decode() + res = upgrade_pip(Path(venv_dir.name)) if verbose: print(res) @@ -169,6 +169,7 @@ class InvokeAiInstance: set_sys_path(venv) os.environ["INVOKEAI_ROOT"] = str(self.runtime.expanduser().resolve()) os.environ["VIRTUAL_ENV"] = str(self.venv.expanduser().resolve()) + upgrade_pip(venv) def get(self) -> tuple[Path, Path]: """ @@ -218,7 +219,6 @@ class InvokeAiInstance: from plumbum import FG, ProcessExecutionError, local # type: ignore pip = local[self.pip] - _ = pip["install", "--upgrade", "pip"] & FG pipeline = pip[ "install", @@ -330,6 +330,23 @@ def get_pip_from_venv(venv_path: Path) -> str: return str(venv_path.expanduser().resolve() / pip) +def upgrade_pip(venv_path: Path) -> str | None: + """ + Upgrade the pip executable in the given virtual environment + """ + + python = "Scripts\\python.exe" if OS == "Windows" else "bin/python" + python = str(venv_path.expanduser().resolve() / python) + + try: + result = subprocess.check_output([python, "-m", "pip", "install", "--upgrade", "pip"]).decode() + except subprocess.CalledProcessError as e: + print(e) + result = None + + return result + + def set_sys_path(venv_path: Path) -> None: """ Given a path to a virtual environment, set the sys.path, in a cross-platform fashion, From f64fc2c8b76985c68e6ecba7872020e53d51a6fa Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Tue, 6 Feb 2024 11:43:05 -0500 Subject: [PATCH 28/44] feat(installer): add a deprecation message to the in-launcher updater --- installer/templates/invoke.bat.in | 9 +++++---- installer/templates/invoke.sh.in | 6 ++++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/installer/templates/invoke.bat.in b/installer/templates/invoke.bat.in index ee6d56fc56..fb07280b6a 100644 --- a/installer/templates/invoke.bat.in +++ b/installer/templates/invoke.bat.in @@ -15,7 +15,7 @@ echo 4. Download and install models echo 5. Change InvokeAI startup options echo 6. Re-run the configure script to fix a broken install or to complete a major upgrade echo 7. Open the developer console -echo 8. Update InvokeAI +echo 8. Update InvokeAI (DEPRECATED - please use the installer) echo 9. Run the InvokeAI image database maintenance script echo 10. Command-line help echo Q - Quit @@ -52,8 +52,10 @@ IF /I "%choice%" == "1" ( echo *** Type `exit` to quit this shell and deactivate the Python virtual environment *** call cmd /k ) ELSE IF /I "%choice%" == "8" ( - echo Running invokeai-update... - python -m invokeai.frontend.install.invokeai_update + echo UPDATING FROM WITHIN THE APP IS BEING DEPRECATED. + echo Please download the installer from https://github.com/invoke-ai/InvokeAI/releases/latest and run it to update your installation. + timeout 4 + python -m invokeai.frontend.install.invokeai_update ) ELSE IF /I "%choice%" == "9" ( echo Running the db maintenance script... python .venv\Scripts\invokeai-db-maintenance.exe @@ -77,4 +79,3 @@ pause :ending exit /b - diff --git a/installer/templates/invoke.sh.in b/installer/templates/invoke.sh.in index 3230c9f442..a6bb88faec 100644 --- a/installer/templates/invoke.sh.in +++ b/installer/templates/invoke.sh.in @@ -90,7 +90,9 @@ do_choice() { ;; 8) clear - printf "Update InvokeAI\n" + printf "UPDATING FROM WITHIN THE APP IS BEING DEPRECATED\n" + printf "Please download the installer from https://github.com/invoke-ai/InvokeAI/releases/latest and run it to update your installation.\n" + sleep 4 python -m invokeai.frontend.install.invokeai_update ;; 9) @@ -122,7 +124,7 @@ do_dialog() { 5 "Change InvokeAI startup options" 6 "Re-run the configure script to fix a broken install or to complete a major upgrade" 7 "Open the developer console" - 8 "Update InvokeAI" + 8 "Update InvokeAI (DEPRECATED - please use the installer)" 9 "Run the InvokeAI image database maintenance script" 10 "Command-line help" ) From 2175fe38230a8d382d4c5aeb3f775f50bdc1d49d Mon Sep 17 00:00:00 2001 From: B N Date: Wed, 7 Feb 2024 17:01:58 +0100 Subject: [PATCH 29/44] translationBot(ui): update translation (German) Currently translated at 66.2% (938 of 1416 strings) Co-authored-by: B N Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/de.json | 179 +++++++++++++++---- 1 file changed, 143 insertions(+), 36 deletions(-) diff --git a/invokeai/frontend/web/public/locales/de.json b/invokeai/frontend/web/public/locales/de.json index e1ef94f06d..02415683d5 100644 --- a/invokeai/frontend/web/public/locales/de.json +++ b/invokeai/frontend/web/public/locales/de.json @@ -115,7 +115,8 @@ "orderBy": "Ordnen nach", "saveAs": "Speicher als", "updated": "Aktualisiert", - "copy": "Kopieren" + "copy": "Kopieren", + "aboutHeading": "Nutzen Sie Ihre kreative Energie" }, "gallery": { "generations": "Erzeugungen", @@ -146,12 +147,16 @@ "deleteImagePermanent": "Gelöschte Bilder können nicht wiederhergestellt werden.", "autoAssignBoardOnClick": "Board per Klick automatisch zuweisen", "noImageSelected": "Kein Bild ausgewählt", - "problemDeletingImagesDesc": "Eins oder mehr Bilder könnten nicht gelöscht werden", + "problemDeletingImagesDesc": "Ein oder mehrere Bilder konnten nicht gelöscht werden", "starImage": "Bild markieren", "assets": "Ressourcen", "unstarImage": "Markierung Entfernen", "image": "Bild", - "deleteSelection": "Lösche markierte" + "deleteSelection": "Lösche markierte", + "dropToUpload": "$t(gallery.drop) zum hochladen", + "dropOrUpload": "$t(gallery.drop) oder hochladen", + "drop": "Ablegen", + "problemDeletingImages": "Problem beim Löschen der Bilder" }, "hotkeys": { "keyboardShortcuts": "Tastenkürzel", @@ -357,25 +362,38 @@ "desc": "Öffnet das Menü zum Hinzufügen von Knoten" }, "cancelAndClear": { - "title": "Abbruch und leeren" + "title": "Abbruch und leeren", + "desc": "Aktuelle Berechnung abbrechen und alle wartenden löschen" }, "noHotkeysFound": "Kein Hotkey gefunden", "searchHotkeys": "Hotkeys durchsuchen", - "clearSearch": "Suche leeren" + "clearSearch": "Suche leeren", + "resetOptionsAndGallery": { + "desc": "Optionen und Galerie-Panels zurücksetzen", + "title": "Optionen und Galerie zurücksetzen" + }, + "remixImage": { + "desc": "Alle Parameter außer Seed vom aktuellen Bild verwenden", + "title": "Remix des Bilds erstellen" + }, + "toggleOptionsAndGallery": { + "title": "Optionen und Galerie umschalten", + "desc": "Optionen und Galerie-Panels öffnen und schließen" + } }, "modelManager": { "modelAdded": "Model hinzugefügt", "modelUpdated": "Model aktualisiert", "modelEntryDeleted": "Modelleintrag gelöscht", "cannotUseSpaces": "Leerzeichen können nicht verwendet werden", - "addNew": "Neue hinzufügen", - "addNewModel": "Neues Model hinzufügen", + "addNew": "Neu hinzufügen", + "addNewModel": "Neues Modell hinzufügen", "addManually": "Manuell hinzufügen", "nameValidationMsg": "Geben Sie einen Namen für Ihr Model ein", "description": "Beschreibung", "descriptionValidationMsg": "Fügen Sie eine Beschreibung für Ihr Model hinzu", "config": "Konfiguration", - "configValidationMsg": "Pfad zur Konfigurationsdatei Ihres Models.", + "configValidationMsg": "Pfad zur Konfigurationsdatei Ihres Modells.", "modelLocation": "Ort des Models", "modelLocationValidationMsg": "Pfad zum Speicherort Ihres Models", "vaeLocation": "VAE Ort", @@ -384,9 +402,9 @@ "widthValidationMsg": "Standardbreite Ihres Models.", "height": "Höhe", "heightValidationMsg": "Standardbhöhe Ihres Models.", - "addModel": "Model hinzufügen", + "addModel": "Modell hinzufügen", "updateModel": "Model aktualisieren", - "availableModels": "Verfügbare Models", + "availableModels": "Verfügbare Modelle", "search": "Suche", "load": "Laden", "active": "Aktiv", @@ -505,7 +523,9 @@ "mergedModelName": "Zusammengeführter Modellname", "checkpointOrSafetensors": "$t(common.checkpoint) / $t(common.safetensors)", "formMessageDiffusersModelLocation": "Diffusers Modell Speicherort", - "noModelSelected": "Kein Modell ausgewählt" + "noModelSelected": "Kein Modell ausgewählt", + "conversionNotSupported": "Umwandlung nicht unterstützt", + "configFile": "Konfigurationsdatei" }, "parameters": { "images": "Bilder", @@ -656,7 +676,7 @@ "redo": "Wiederherstellen", "clearCanvas": "Leinwand löschen", "canvasSettings": "Leinwand-Einstellungen", - "showIntermediates": "Zwischenprodukte anzeigen", + "showIntermediates": "Zwischenbilder anzeigen", "showGrid": "Gitternetz anzeigen", "snapToGrid": "Am Gitternetz einrasten", "darkenOutsideSelection": "Außerhalb der Auswahl verdunkeln", @@ -742,15 +762,15 @@ "deleteBoard": "Löschen Ordner", "deleteBoardAndImages": "Löschen Ordner und Bilder", "deletedBoardsCannotbeRestored": "Gelöschte Ordner könnte nicht wiederhergestellt werden", - "movingImagesToBoard_one": "Verschiebe {{count}} Bild zu Ordner", - "movingImagesToBoard_other": "Verschiebe {{count}} Bilder in Ordner" + "movingImagesToBoard_one": "Verschiebe {{count}} Bild zu Ordner:", + "movingImagesToBoard_other": "Verschiebe {{count}} Bilder in Ordner:" }, "controlnet": { "showAdvanced": "Zeige Erweitert", "contentShuffleDescription": "Mischt den Inhalt von einem Bild", "addT2IAdapter": "$t(common.t2iAdapter) hinzufügen", - "importImageFromCanvas": "Importieren Bild von Zeichenfläche", - "lineartDescription": "Konvertiere Bild zu Lineart", + "importImageFromCanvas": "Bild von Zeichenfläche importieren", + "lineartDescription": "Konvertiere Bild in Strichzeichnung", "importMaskFromCanvas": "Importiere Maske von Zeichenfläche", "hed": "HED", "hideAdvanced": "Verstecke Erweitert", @@ -786,8 +806,8 @@ "toggleControlNet": "Schalten ControlNet um", "delete": "Löschen", "controlAdapter_one": "Control Adapter", - "controlAdapter_other": "Control Adapters", - "colorMapTileSize": "Tile Größe", + "controlAdapter_other": "Control Adapter", + "colorMapTileSize": "Kachelgröße", "depthZoeDescription": "Tiefenmap erstellen mit Zoe", "setControlImageDimensions": "Setze Control Bild Auflösung auf Breite/Höhe", "handAndFace": "Hand und Gesicht", @@ -807,14 +827,14 @@ "mlsdDescription": "Minimalistischer Liniensegmentdetektor", "openPoseDescription": "Schätzung der menschlichen Pose mit Openpose", "control": "Kontrolle", - "coarse": "Coarse", + "coarse": "Grob", "crop": "Zuschneiden", "pidiDescription": "PIDI-Bildverarbeitung", "mediapipeFace": "Mediapipe Gesichter", "mlsd": "M-LSD", "controlMode": "Steuermodus", - "cannyDescription": "Canny Ecken Erkennung", - "lineart": "Lineart", + "cannyDescription": "Canny Umrisserkennung", + "lineart": "Linienzeichnung", "lineartAnimeDescription": "Lineart-Verarbeitung im Anime-Stil", "minConfidence": "Minimales Vertrauen", "megaControl": "Mega-Kontrolle", @@ -827,7 +847,7 @@ "canny": "Canny", "hedDescription": "Ganzheitlich verschachtelte Kantenerkennung", "scribble": "Scribble", - "maxFaces": "Maximal Anzahl Gesichter", + "maxFaces": "Maximale Anzahl Gesichter", "resizeSimple": "Größe ändern (einfach)", "large": "Groß", "modelSize": "Modell Größe", @@ -876,7 +896,7 @@ "enqueueing": "Stapel in der Warteschlange", "queueMaxExceeded": "Maximum von {{max_queue_size}} Elementen erreicht, würde {{skip}} Elemente überspringen", "cancelBatchFailed": "Problem beim Abbruch vom Stapel", - "clearQueueAlertDialog2": "bist du sicher die Warteschlange zu leeren?", + "clearQueueAlertDialog2": "Warteschlange wirklich leeren?", "pruneSucceeded": "{{item_count}} abgeschlossene Elemente aus der Warteschlange entfernt", "pauseSucceeded": "Prozessor angehalten", "cancelFailed": "Problem beim Stornieren des Auftrags", @@ -890,35 +910,39 @@ "resumeSucceeded": "Prozessor wieder aufgenommen", "resumeTooltip": "Prozessor wieder aufnehmen", "time": "Zeit", - "batchQueuedDesc_one": "{{count}} Eintrage ans {{direction}} der Wartschlange hinzugefügt", - "batchQueuedDesc_other": "{{count}} Einträge ans {{direction}} der Wartschlange hinzugefügt" + "batchQueuedDesc_one": "{{count}} Eintrag ans {{direction}} der Wartschlange hinzugefügt", + "batchQueuedDesc_other": "{{count}} Einträge ans {{direction}} der Wartschlange hinzugefügt", + "openQueue": "Warteschlange öffnen", + "batchFailedToQueue": "Fehler beim Einreihen in die Stapelverarbeitung", + "batchFieldValues": "Stapelverarbeitungswerte", + "batchQueued": "Stapelverarbeitung eingereiht" }, "metadata": { "negativePrompt": "Negativ Beschreibung", - "metadata": "Meta-Data", - "strength": "Bild zu Bild stärke", + "metadata": "Meta-Daten", + "strength": "Bild zu Bild Stärke", "imageDetails": "Bild Details", "model": "Modell", "noImageDetails": "Keine Bild Details gefunden", "cfgScale": "CFG-Skala", - "fit": "Bild zu Bild passen", + "fit": "Bild zu Bild anpassen", "height": "Höhe", - "noMetaData": "Keine Meta-Data gefunden", + "noMetaData": "Keine Meta-Daten gefunden", "width": "Breite", "createdBy": "Erstellt von", "steps": "Schritte", "seamless": "Nahtlos", "positivePrompt": "Positiver Prompt", "generationMode": "Generierungsmodus", - "Threshold": "Noise Schwelle", - "seed": "Samen", + "Threshold": "Rauschen-Schwelle", + "seed": "Seed", "perlin": "Perlin Noise", "hiresFix": "Optimierung für hohe Auflösungen", "initImage": "Erstes Bild", "variations": "Samengewichtspaare", "vae": "VAE", "workflow": "Arbeitsablauf", - "scheduler": "Scheduler", + "scheduler": "Planer", "noRecallParameters": "Es wurden keine Parameter zum Abrufen gefunden", "recallParameters": "Recall Parameters" }, @@ -955,13 +979,20 @@ }, "invocationCache": { "disable": "Deaktivieren", - "misses": "Cache Nötig", + "misses": "Cache nicht genutzt", "hits": "Cache Treffer", "enable": "Aktivieren", "clear": "Leeren", "maxCacheSize": "Maximale Cache Größe", "cacheSize": "Cache Größe", - "useCache": "Benutze Cache" + "useCache": "Benutze Cache", + "enableFailed": "Problem beim Aktivieren des Zwischenspeichers", + "disableFailed": "Problem bei Deaktivierung des Cache", + "enableSucceeded": "Zwischenspeicher aktiviert", + "disableSucceeded": "Aufrufcache deaktiviert", + "clearSucceeded": "Zwischenspeicher gelöscht", + "invocationCache": "Zwischenspeicher", + "clearFailed": "Problem beim Löschen des Zwischenspeichers" }, "embedding": { "noMatchingEmbedding": "Keine passenden Embeddings", @@ -1000,7 +1031,41 @@ "colorCodeEdges": "Farbkodierte Kanten", "addNodeToolTip": "Knoten hinzufügen (Umschalt+A, Leertaste)", "boardField": "Ordner", - "boardFieldDescription": "Ein Galerie Ordner" + "boardFieldDescription": "Ein Galerie Ordner", + "collectionFieldType": "{{name}} Sammlung", + "controlCollectionDescription": "Kontrollinformationen zwischen Knotenpunkten weitergegeben.", + "connectionWouldCreateCycle": "Verbindung würde einen Kreislauf/cycle schaffen", + "ipAdapterDescription": "Ein Adapter für die Bildabfrage (IP-Adapter) / Bilderprompt-Adapter.", + "controlField": "Kontrolle", + "inputFields": "Eingabefelder", + "imageField": "Bild", + "inputMayOnlyHaveOneConnection": "Eingang darf nur eine Verbindung haben", + "integerCollectionDescription": "Eine Sammlung ganzer Zahlen.", + "integerDescription": "Das sind ganze Zahlen ohne Dezimalpunkt.", + "conditioningPolymorphic": "Konditionierung polymorphisch", + "conditioningPolymorphicDescription": "Die Konditionierung kann zwischen den Knotenpunkten weitergegeben werden.", + "invalidOutputSchema": "Ungültiges Ausgabeschema", + "ipAdapterModel": "IP-Adapter Modell", + "conditioningFieldDescription": "Die Konditionierung kann zwischen den Knotenpunkten weitergegeben werden.", + "ipAdapterCollectionDescription": "Eine Sammlung von IP-Adaptern.", + "collectionDescription": "Zu erledigen", + "imageFieldDescription": "Bilder können zwischen Knoten weitergegeben werden.", + "imagePolymorphic": "Bild Polymorphie", + "imagePolymorphicDescription": "Eine Bildersammlung.", + "inputField": "Eingabefeld", + "hideLegendNodes": "Feldtyp-Legende ausblenden", + "collectionItemDescription": "Zu erledigen", + "inputNode": "Eingangsknoten", + "integer": "Ganze Zahl", + "integerCollection": "Ganzzahlige Sammlung", + "addLinearView": "Zur linearen Ansicht hinzufügen", + "currentImageDescription": "Zeigt das aktuelle Bild im Node-Editor an", + "ipAdapter": "IP-Adapter", + "hideMinimapnodes": "Miniatur-Kartenansicht ausblenden", + "imageCollection": "Bildersammlung", + "imageCollectionDescription": "Eine Sammlung von Bildern.", + "denoiseMaskField": "Entrauschen-Maske", + "ipAdapterCollection": "IP-Adapter Sammlung" }, "hrf": { "enableHrf": "Aktivieren Sie die Korrektur für hohe Auflösungen", @@ -1026,7 +1091,14 @@ "noLoRAsInstalled": "Keine LoRAs installiert", "selectLoRA": "Wählen ein LoRA aus", "esrganModel": "ESRGAN Modell", - "addLora": "LoRA hinzufügen" + "addLora": "LoRA hinzufügen", + "defaultVAE": "Standard VAE", + "noLoRAsLoaded": "Keine LoRAs geladen", + "lora": "LoRA", + "allLoRAsAdded": "Alle LoRAs hinzugefügt", + "incompatibleBaseModel": "Inkompatibles Basismodell", + "noMainModelSelected": "Kein Hauptmodell ausgewählt", + "loraAlreadyAdded": "LoRA bereits hinzugefügt" }, "accordions": { "generation": { @@ -1050,5 +1122,40 @@ "infillTab": "Füllung", "title": "Compositing" } + }, + "workflows": { + "workflows": "Arbeitsabläufe", + "noSystemWorkflows": "Keine System-Arbeitsabläufe", + "workflowName": "Arbeitsablauf-Name", + "workflowIsOpen": "Arbeitsablauf ist offen", + "saveWorkflowAs": "Arbeitsablauf speichern als", + "searchWorkflows": "Suche Arbeitsabläufe", + "newWorkflowCreated": "Neuer Arbeitsablauf erstellt", + "problemSavingWorkflow": "Problem beim Speichern des Arbeitsablaufs", + "noRecentWorkflows": "Keine kürzlichen Arbeitsabläufe", + "problemLoading": "Problem beim Laden von Arbeitsabläufen", + "downloadWorkflow": "Speichern als", + "savingWorkflow": "Speichere Arbeitsablauf...", + "saveWorkflow": "Arbeitsablauf speichern", + "noWorkflows": "Keine Arbeitsabläufe", + "workflowLibrary": "Bibliothek", + "defaultWorkflows": "Standard-Arbeitsabläufe", + "unnamedWorkflow": "Unbenannter Arbeitsablauf", + "noDescription": "Keine Beschreibung", + "clearWorkflowSearchFilter": "Suchfilter zurücksetzen", + "workflowEditorMenu": "Arbeitsablauf-Editor Menü", + "deleteWorkflow": "Arbeitsablauf löschen", + "userWorkflows": "Meine Arbeitsabläufe", + "workflowSaved": "Arbeitsablauf gespeichert", + "uploadWorkflow": "Aus Datei laden", + "projectWorkflows": "Projekt-Arbeitsabläufe", + "openWorkflow": "Arbeitsablauf öffnen", + "noUserWorkflows": "Keine Benutzer-Arbeitsabläufe", + "saveWorkflowToProject": "Arbeitsablauf in Projekt speichern", + "workflowCleared": "Arbeitsablauf gelöscht", + "loading": "Lade Arbeitsabläufe" + }, + "app": { + "storeNotInitialized": "App-Store ist nicht initialisiert" } } From c9e246ed1bf24840bbe9982f5b3bc94c0b0653e0 Mon Sep 17 00:00:00 2001 From: Eugene Brodsky Date: Thu, 8 Feb 2024 09:56:56 -0500 Subject: [PATCH 30/44] fix(installer): print correct version when a non-default version is selected --- installer/lib/messages.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/installer/lib/messages.py b/installer/lib/messages.py index 16bcd56e98..a69337969f 100644 --- a/installer/lib/messages.py +++ b/installer/lib/messages.py @@ -92,7 +92,8 @@ def choose_version(available_releases: tuple | None = None) -> str: completer=FuzzyWordCompleter(choices), ) - console.print(f" Version {choices[0]} will be installed.") + console.print(f" Version {choices[0] if response == "" else response} will be installed.") + console.line() return "stable" if response == "" else response From 6c5f743e2be49fc852556bc14c5ab6cfa5bd64f3 Mon Sep 17 00:00:00 2001 From: Brandon Rising Date: Thu, 8 Feb 2024 16:49:07 -0500 Subject: [PATCH 31/44] Upgrade version of fastapi and socketio --- invokeai/app/api/sockets.py | 2 +- pyproject.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/invokeai/app/api/sockets.py b/invokeai/app/api/sockets.py index c63297fa55..e651e43559 100644 --- a/invokeai/app/api/sockets.py +++ b/invokeai/app/api/sockets.py @@ -14,7 +14,7 @@ class SocketIO: def __init__(self, app: FastAPI): self.__sio = AsyncServer(async_mode="asgi", cors_allowed_origins="*") - self.__app = ASGIApp(socketio_server=self.__sio, socketio_path="socket.io") + self.__app = ASGIApp(socketio_server=self.__sio, socketio_path="/ws/socket.io") app.mount("/ws", self.__app) self.__sio.on("subscribe_queue", handler=self._handle_sub_queue) diff --git a/pyproject.toml b/pyproject.toml index 694ae707f1..78cbbe74ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,12 +55,12 @@ dependencies = [ "transformers==4.37.2", # Core application dependencies, pinned for reproducible builds. - "fastapi-events==0.10.0", - "fastapi==0.108.0", + "fastapi-events==0.10.1", + "fastapi==0.109.2", "huggingface-hub==0.20.3", "pydantic-settings==2.1.0", "pydantic==2.5.3", - "python-socketio==5.11.0", + "python-socketio==5.11.1", "uvicorn[standard]==0.25.0", # Auxiliary dependencies, pinned only if necessary. From 2ce70b445743efb45e7bb5a7fc69b7d974c2b6ef Mon Sep 17 00:00:00 2001 From: Jennifer Player Date: Thu, 8 Feb 2024 12:24:19 -0500 Subject: [PATCH 32/44] added button on hover for exposing fields to linear workflow ui --- .../fields/FieldLinearViewToggle.tsx | 74 +++++++++++++++++++ .../nodes/Invocation/fields/InputField.tsx | 33 ++++++--- 2 files changed, 97 insertions(+), 10 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldLinearViewToggle.tsx diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldLinearViewToggle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldLinearViewToggle.tsx new file mode 100644 index 0000000000..0dac7996eb --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldLinearViewToggle.tsx @@ -0,0 +1,74 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { + selectWorkflowSlice, + workflowExposedFieldAdded, + workflowExposedFieldRemoved, +} from 'features/nodes/store/workflowSlice'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiMinusBold, PiPlusBold } from 'react-icons/pi'; + +type Props = { + nodeId: string; + fieldName: string; + isHovered: boolean; +}; + +const FieldLinearViewToggle = ({ nodeId, fieldName, isHovered }: Props) => { + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + + const selectIsExposed = useMemo( + () => + createSelector(selectWorkflowSlice, (workflow) => { + return Boolean(workflow.exposedFields.find((f) => f.nodeId === nodeId && f.fieldName === fieldName)); + }), + [fieldName, nodeId] + ); + + const isExposed = useAppSelector(selectIsExposed); + + const handleExposeField = useCallback(() => { + dispatch(workflowExposedFieldAdded({ nodeId, fieldName })); + }, [dispatch, fieldName, nodeId]); + + const handleUnexposeField = useCallback(() => { + dispatch(workflowExposedFieldRemoved({ nodeId, fieldName })); + }, [dispatch, fieldName, nodeId]); + + const ToggleButton = useMemo(() => { + if (!isHovered) { + return null; + } else if (!isExposed) { + return ( + } + onClick={handleExposeField} + pointerEvents="auto" + size="xs" + /> + ); + } else if (isExposed) { + return ( + } + onClick={handleUnexposeField} + pointerEvents="auto" + size="xs" + /> + ); + } + }, [isHovered, handleExposeField, handleUnexposeField, isExposed, t]); + + return ToggleButton; +}; + +export default memo(FieldLinearViewToggle); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx index b33b65dfb5..bb45f4e3e3 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx @@ -4,12 +4,13 @@ import { useDoesInputHaveValue } from 'features/nodes/hooks/useDoesInputHaveValu import { useFieldInputInstance } from 'features/nodes/hooks/useFieldInputInstance'; import { useFieldInputTemplate } from 'features/nodes/hooks/useFieldInputTemplate'; import type { PropsWithChildren } from 'react'; -import { memo, useMemo } from 'react'; +import { memo, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import EditableFieldTitle from './EditableFieldTitle'; import FieldContextMenu from './FieldContextMenu'; import FieldHandle from './FieldHandle'; +import FieldLinearViewToggle from './FieldLinearViewToggle'; import InputFieldRenderer from './InputFieldRenderer'; interface Props { @@ -22,6 +23,7 @@ const InputField = ({ nodeId, fieldName }: Props) => { const fieldTemplate = useFieldInputTemplate(nodeId, fieldName); const fieldInstance = useFieldInputInstance(nodeId, fieldName); const doesFieldHaveValue = useDoesInputHaveValue(nodeId, fieldName); + const [isHovered, setIsHovered] = useState(false); const { isConnected, isConnectionInProgress, isConnectionStartField, connectionError, shouldDim } = useConnectionState({ nodeId, fieldName, kind: 'input' }); @@ -46,6 +48,14 @@ const InputField = ({ nodeId, fieldName }: Props) => { return false; }, [fieldTemplate, isConnected, doesFieldHaveValue]); + const handleMouseOver = useCallback(() => { + setIsHovered(true); + }, []); + + const handleMouseOut = useCallback(() => { + setIsHovered(false); + }, []); + if (!fieldTemplate || !fieldInstance) { return ( @@ -87,17 +97,20 @@ const InputField = ({ nodeId, fieldName }: Props) => { return ( - + {(ref) => ( - + + + + )} From 5de2288cfabea3f56cfc89cae76213c7a10e0300 Mon Sep 17 00:00:00 2001 From: Jennifer Player Date: Thu, 8 Feb 2024 17:31:56 -0500 Subject: [PATCH 33/44] addressed feedback --- .../Invocation/fields/FieldContextMenu.tsx | 84 ------------------- .../fields/FieldLinearViewToggle.tsx | 59 ++++++------- .../nodes/Invocation/fields/InputField.tsx | 32 +++---- 3 files changed, 39 insertions(+), 136 deletions(-) delete mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldContextMenu.tsx diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldContextMenu.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldContextMenu.tsx deleted file mode 100644 index ec1500db9d..0000000000 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldContextMenu.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import type { ContextMenuProps } from '@invoke-ai/ui-library'; -import { ContextMenu, MenuGroup, MenuItem, MenuList } from '@invoke-ai/ui-library'; -import { createSelector } from '@reduxjs/toolkit'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { useFieldInputKind } from 'features/nodes/hooks/useFieldInputKind'; -import { useFieldLabel } from 'features/nodes/hooks/useFieldLabel'; -import { useFieldTemplateTitle } from 'features/nodes/hooks/useFieldTemplateTitle'; -import { - selectWorkflowSlice, - workflowExposedFieldAdded, - workflowExposedFieldRemoved, -} from 'features/nodes/store/workflowSlice'; -import type { ReactNode } from 'react'; -import { memo, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiMinusBold, PiPlusBold } from 'react-icons/pi'; - -type Props = { - nodeId: string; - fieldName: string; - kind: 'input' | 'output'; - children: ContextMenuProps['children']; -}; - -const FieldContextMenu = ({ nodeId, fieldName, kind, children }: Props) => { - const dispatch = useAppDispatch(); - const label = useFieldLabel(nodeId, fieldName); - const fieldTemplateTitle = useFieldTemplateTitle(nodeId, fieldName, kind); - const input = useFieldInputKind(nodeId, fieldName); - const { t } = useTranslation(); - - const selectIsExposed = useMemo( - () => - createSelector(selectWorkflowSlice, (workflow) => { - return Boolean(workflow.exposedFields.find((f) => f.nodeId === nodeId && f.fieldName === fieldName)); - }), - [fieldName, nodeId] - ); - - const mayExpose = useMemo(() => input && ['any', 'direct'].includes(input), [input]); - - const isExposed = useAppSelector(selectIsExposed); - - const handleExposeField = useCallback(() => { - dispatch(workflowExposedFieldAdded({ nodeId, fieldName })); - }, [dispatch, fieldName, nodeId]); - - const handleUnexposeField = useCallback(() => { - dispatch(workflowExposedFieldRemoved({ nodeId, fieldName })); - }, [dispatch, fieldName, nodeId]); - - const menuItems = useMemo(() => { - const menuItems: ReactNode[] = []; - if (mayExpose && !isExposed) { - menuItems.push( - } onClick={handleExposeField}> - {t('nodes.addLinearView')} - - ); - } - if (mayExpose && isExposed) { - menuItems.push( - } onClick={handleUnexposeField}> - {t('nodes.removeLinearView')} - - ); - } - return menuItems; - }, [fieldName, handleExposeField, handleUnexposeField, isExposed, mayExpose, nodeId, t]); - - const renderMenuFunc = useCallback( - () => - !menuItems.length ? null : ( - - {menuItems} - - ), - [fieldTemplateTitle, label, menuItems, t] - ); - - return {children}; -}; - -export default memo(FieldContextMenu); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldLinearViewToggle.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldLinearViewToggle.tsx index 0dac7996eb..cb1bcba1fe 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldLinearViewToggle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/FieldLinearViewToggle.tsx @@ -13,10 +13,9 @@ import { PiMinusBold, PiPlusBold } from 'react-icons/pi'; type Props = { nodeId: string; fieldName: string; - isHovered: boolean; }; -const FieldLinearViewToggle = ({ nodeId, fieldName, isHovered }: Props) => { +const FieldLinearViewToggle = ({ nodeId, fieldName }: Props) => { const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -38,37 +37,31 @@ const FieldLinearViewToggle = ({ nodeId, fieldName, isHovered }: Props) => { dispatch(workflowExposedFieldRemoved({ nodeId, fieldName })); }, [dispatch, fieldName, nodeId]); - const ToggleButton = useMemo(() => { - if (!isHovered) { - return null; - } else if (!isExposed) { - return ( - } - onClick={handleExposeField} - pointerEvents="auto" - size="xs" - /> - ); - } else if (isExposed) { - return ( - } - onClick={handleUnexposeField} - pointerEvents="auto" - size="xs" - /> - ); - } - }, [isHovered, handleExposeField, handleUnexposeField, isExposed, t]); - - return ToggleButton; + if (!isExposed) { + return ( + } + onClick={handleExposeField} + pointerEvents="auto" + size="xs" + /> + ); + } else { + return ( + } + onClick={handleUnexposeField} + pointerEvents="auto" + size="xs" + /> + ); + } }; export default memo(FieldLinearViewToggle); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx index bb45f4e3e3..2b9f7960e4 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InputField.tsx @@ -8,7 +8,6 @@ import { memo, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import EditableFieldTitle from './EditableFieldTitle'; -import FieldContextMenu from './FieldContextMenu'; import FieldHandle from './FieldHandle'; import FieldLinearViewToggle from './FieldLinearViewToggle'; import InputFieldRenderer from './InputFieldRenderer'; @@ -48,11 +47,11 @@ const InputField = ({ nodeId, fieldName }: Props) => { return false; }, [fieldTemplate, isConnected, doesFieldHaveValue]); - const handleMouseOver = useCallback(() => { + const onMouseEnter = useCallback(() => { setIsHovered(true); }, []); - const handleMouseOut = useCallback(() => { + const onMouseLeave = useCallback(() => { setIsHovered(false); }, []); @@ -97,22 +96,17 @@ const InputField = ({ nodeId, fieldName }: Props) => { return ( - - - {(ref) => ( - - - - - )} - + + + + {isHovered && } + From a68d8fe203a36a27d8851dc883218a77191fa41d Mon Sep 17 00:00:00 2001 From: B N Date: Thu, 8 Feb 2024 23:04:07 +0100 Subject: [PATCH 34/44] translationBot(ui): update translation (German) Currently translated at 74.4% (1054 of 1416 strings) translationBot(ui): update translation (German) Currently translated at 69.6% (986 of 1416 strings) translationBot(ui): update translation (German) Currently translated at 68.6% (972 of 1416 strings) Co-authored-by: B N Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/de.json | 181 +++++++++++++++++-- 1 file changed, 169 insertions(+), 12 deletions(-) diff --git a/invokeai/frontend/web/public/locales/de.json b/invokeai/frontend/web/public/locales/de.json index 02415683d5..7b2693e1ab 100644 --- a/invokeai/frontend/web/public/locales/de.json +++ b/invokeai/frontend/web/public/locales/de.json @@ -603,7 +603,8 @@ "resetWebUIDesc2": "Wenn die Bilder nicht in der Galerie angezeigt werden oder etwas anderes nicht funktioniert, versuchen Sie bitte, die Einstellungen zurückzusetzen, bevor Sie einen Fehler auf GitHub melden.", "resetComplete": "Die Web-Oberfläche wurde zurückgesetzt.", "models": "Modelle", - "useSlidersForAll": "Schieberegler für alle Optionen verwenden" + "useSlidersForAll": "Schieberegler für alle Optionen verwenden", + "showAdvancedOptions": "Erweiterte Optionen anzeigen" }, "toast": { "tempFoldersEmptied": "Temp-Ordner geleert", @@ -714,7 +715,7 @@ "showResultsOff": "Zeige Ergebnisse (Aus)" }, "accessibility": { - "modelSelect": "Model Auswahl", + "modelSelect": "Modell-Auswahl", "uploadImage": "Bild hochladen", "previousImage": "Voriges Bild", "useThisParameter": "Benutze diesen Parameter", @@ -752,7 +753,7 @@ "selectBoard": "Ordner aussuchen", "cancel": "Abbrechen", "addBoard": "Ordner hinzufügen", - "uncategorized": "Nicht kategorisiert", + "uncategorized": "Ohne Kategorie", "downloadBoard": "Ordner runterladen", "changeBoard": "Ordner wechseln", "loading": "Laden...", @@ -948,19 +949,39 @@ }, "popovers": { "noiseUseCPU": { - "heading": "Nutze Prozessor rauschen" + "heading": "Nutze Prozessor rauschen", + "paragraphs": [ + null, + null, + "CPU-Rauschen einzuschalten beeinflusst nicht die Systemleistung." + ] }, "paramModel": { - "heading": "Modell" + "heading": "Modell", + "paragraphs": [ + "Modell für die Entrauschungsschritte.", + "Verschiedene Modelle werden in der Regel so trainiert, dass sie sich auf die Erzeugung bestimmter Ästhetik und/oder Inhalte spezialisiert." + ] }, "paramIterations": { - "heading": "Iterationen" + "heading": "Iterationen", + "paragraphs": [ + "Die Anzahl der Bilder, die erzeugt werden sollen.", + "Wenn \"Dynamische Prompts\" aktiviert ist, wird jeder einzelne Prompt so oft generiert." + ] }, "paramCFGScale": { - "heading": "CFG-Skala" + "heading": "CFG-Skala", + "paragraphs": [ + "Bestimmt, wie viel Ihr Prompt den Erzeugungsprozess beeinflusst." + ] }, "paramSteps": { - "heading": "Schritte" + "heading": "Schritte", + "paragraphs": [ + "Anzahl der Schritte, die bei jeder Generierung durchgeführt werden.", + "Höhere Schrittzahlen werden in der Regel bessere Bilder ergeben, aber mehr Zeit benötigen." + ] }, "lora": { "heading": "LoRA Gewichte" @@ -969,13 +990,56 @@ "heading": "Füllmethode" }, "paramVAE": { - "heading": "VAE" + "heading": "VAE", + "paragraphs": [ + "Verwendetes Modell, um den KI-Ausgang in das endgültige Bild zu übersetzen." + ] + }, + "paramRatio": { + "heading": "Seitenverhältnis", + "paragraphs": [ + "Das Seitenverhältnis des erzeugten Bildes.", + "Für SD1.5-Modelle wird eine Bildgröße von 512x512 Pixel empfohlen, für SDXL-Modelle sind es 1024x1024 Pixel." + ] + }, + "paramDenoisingStrength": { + "paragraphs": [ + "Wie viel Rauschen dem Eingabebild hinzugefügt wird.", + "0 wird zu einem identischen Bild führen, während 1 zu einem völlig neuen Bild führt." + ], + "heading": "Stärke der Entrauschung" + }, + "paramVAEPrecision": { + "heading": "VAE-Präzision", + "paragraphs": [ + "Die bei der VAE-Kodierung und Dekodierung verwendete Präzision. FP16/Halbpräzision ist effizienter, aber auf Kosten kleiner Bildvariationen." + ] + }, + "paramCFGRescaleMultiplier": { + "heading": "CFG Rescale Multiplikator", + "paragraphs": [ + "Rescale-Multiplikator für die CFG-Lenkung, der für Modelle verwendet wird, die mit dem zero-terminal SNR (ztsnr) trainiert wurden. Empfohlener Wert: 0,7." + ] + }, + "scaleBeforeProcessing": { + "paragraphs": [ + "Skaliert den ausgewählten Bereich auf die Größe, die für das Modell am besten geeignet ist." + ], + "heading": "Skalieren vor der Verarbeitung" + }, + "paramSeed": { + "paragraphs": [ + "Kontrolliert das für die Erzeugung verwendete Startrauschen.", + "Deaktivieren Sie “Random Seed”, um identische Ergebnisse mit den gleichen Generierungseinstellungen zu erzeugen." + ], + "heading": "Seed" } }, "ui": { "lockRatio": "Verhältnis sperren", "hideProgressImages": "Verstecke Prozess Bild", - "showProgressImages": "Zeige Prozess Bild" + "showProgressImages": "Zeige Prozess Bild", + "swapSizes": "Tausche Größen" }, "invocationCache": { "disable": "Deaktivieren", @@ -1065,10 +1129,103 @@ "imageCollection": "Bildersammlung", "imageCollectionDescription": "Eine Sammlung von Bildern.", "denoiseMaskField": "Entrauschen-Maske", - "ipAdapterCollection": "IP-Adapter Sammlung" + "ipAdapterCollection": "IP-Adapter Sammlung", + "newWorkflowDesc2": "Ihr aktueller Arbeitsablauf hat ungespeicherte Änderungen.", + "problemSettingTitle": "Problem beim Einstellen des Titels", + "noConnectionData": "Keine Verbindungsdaten", + "outputField": "Ausgabefeld", + "outputFieldInInput": "Ausgabefeld im Eingang", + "problemReadingWorkflow": "Problem beim Lesen des Arbeitsablaufs vom Bild", + "reloadNodeTemplates": "Knoten-Vorlagen neu laden", + "newWorkflow": "Neuer Arbeitsablauf", + "newWorkflowDesc": "Einen neuen Arbeitsablauf erstellen?", + "noFieldsLinearview": "Keine Felder zur linearen Ansicht hinzugefügt", + "clearWorkflow": "Arbeitsablauf löschen", + "clearWorkflowDesc": "Diesen Arbeitsablauf löschen und neu starten?", + "noConnectionInProgress": "Es besteht keine Verbindung", + "notes": "Anmerkungen", + "nodeVersion": "Knoten Version", + "noOutputSchemaName": "Kein Name des Ausgabeschemas im ref-Objekt gefunden", + "node": "Knoten", + "nodeSearch": "Knoten suchen", + "removeLinearView": "Entfernen aus Linear View", + "nodeOutputs": "Knoten-Ausgänge", + "nodeTemplate": "Knoten-Vorlage", + "nodeType": "Knotentyp", + "noFieldType": "Kein Feldtyp", + "oNNXModelField": "ONNX-Modell", + "noMatchingNodes": "Keine passenden Knoten", + "noNodeSelected": "Kein Knoten gewählt", + "noImageFoundState": "Kein Anfangsbild im Status gefunden", + "nodeOpacity": "Knoten-Deckkraft", + "noOutputRecorded": "Keine Ausgänge aufgezeichnet", + "outputSchemaNotFound": "Ausgabeschema nicht gefunden", + "oNNXModelFieldDescription": "ONNX-Modellfeld.", + "outputNode": "Ausgabeknoten", + "pickOne": "Eins auswählen", + "problemReadingMetadata": "Problem beim Lesen von Metadaten aus dem Bild", + "notesDescription": "Anmerkungen zum Arbeitsablauf hinzufügen", + "outputFields": "Ausgabefelder", + "sDXLRefinerModelField": "Refiner-Modell", + "sDXLMainModelFieldDescription": "SDXL Modellfeld.", + "clearWorkflowDesc2": "Ihr aktueller Arbeitsablauf hat ungespeicherte Änderungen.", + "skipped": "Übersprungen", + "schedulerDescription": "Zu erledigen", + "scheduler": "Planer", + "showGraphNodes": "Graph Overlay anzeigen", + "showMinimapnodes": "MiniMap anzeigen", + "sDXLMainModelField": "SDXL Modell", + "skippedReservedInput": "Reserviertes Eingabefeld übersprungen", + "sDXLRefinerModelFieldDescription": "Zu erledigen", + "showLegendNodes": "Feldtyp-Legende anzeigen", + "skippedReservedOutput": "Reserviertes Ausgangsfeld übersprungen", + "skippingInputNoTemplate": "Überspringe Eingabefeld ohne Vorlage", + "executionStateCompleted": "Erledigt", + "denoiseMaskFieldDescription": "Denoise Maske kann zwischen Knoten weitergegeben werden", + "downloadWorkflow": "Workflow JSON herunterladen", + "executionStateInProgress": "In Bearbeitung", + "snapToGridHelp": "Knoten am Gitternetz einrasten bei Bewegung", + "controlCollection": "Control-Sammlung", + "controlFieldDescription": "Control-Informationen zwischen Knotenpunkten weitergegeben.", + "latentsField": "Latents", + "mainModelFieldDescription": "Zu erledigen", + "missingTemplate": "Ungültiger Knoten: Knoten {{node}} vom Typ {{type}} fehlt Vorlage (nicht installiert?)", + "skippingUnknownInputType": "Überspringe unbekannten Eingabe-Feldtyp", + "stringCollectionDescription": "Eine Sammlung von Zeichenfolgen.", + "string": "Zeichenfolge", + "stringCollection": "Sammlung von Zeichenfolgen", + "stringDescription": "Zeichenfolgen (Strings) sind Text.", + "fieldTypesMustMatch": "Feldtypen müssen übereinstimmen", + "fitViewportNodes": "An Ansichtsgröße anpassen", + "missingCanvaInitMaskImages": "Fehlende Startbilder und Masken auf der Arbeitsfläche", + "missingCanvaInitImage": "Fehlendes Startbild auf der Arbeitsfläche", + "ipAdapterModelDescription": "IP-Adapter-Modellfeld", + "latentsPolymorphicDescription": "Zwischen Nodes können Latents weitergegeben werden.", + "loadingNodes": "Lade Nodes...", + "latentsCollectionDescription": "Zwischen Knoten können Latents weitergegeben werden.", + "mismatchedVersion": "Ungültiger Knoten: Knoten {{node}} vom Typ {{type}} hat keine passende Version (Update versuchen?)", + "colorCollectionDescription": "Zu erledigen", + "ipAdapterPolymorphicDescription": "Eine Sammlung von IP-Adaptern.", + "fullyContainNodesHelp": "Nodes müssen vollständig innerhalb der Auswahlbox sein, um ausgewählt werden zu können", + "latentsFieldDescription": "Zwischen Nodes können Latents weitergegeben werden.", + "noWorkflow": "Kein Workflow", + "hideGraphNodes": "Graph Overlay verbergen", + "sourceNode": "Quellknoten", + "executionStateError": "Fehler", + "latentsCollection": "Latents Sammlung", + "maybeIncompatible": "Möglicherweise inkompatibel mit installierten", + "nodePack": "Knoten-Pack", + "skippingUnknownOutputType": "Überspringe unbekannten Ausgabe-Feldtyp", + "loadWorkflow": "Lade Workflow", + "snapToGrid": "Am Gitternetz einrasten", + "skippingReservedFieldType": "Überspringe reservierten Feldtyp", + "loRAModelField": "LoRA", + "loRAModelFieldDescription": "Zu erledigen", + "mainModelField": "Modell", + "doesNotExist": "existiert nicht" }, "hrf": { - "enableHrf": "Aktivieren Sie die Korrektur für hohe Auflösungen", + "enableHrf": "Korrektur für hohe Auflösungen", "upscaleMethod": "Vergrößerungsmethoden", "enableHrfTooltip": "Generieren Sie mit einer niedrigeren Anfangsauflösung, skalieren Sie auf die Basisauflösung hoch und führen Sie dann Image-to-Image aus.", "metadata": { From c65d497cbc3bf1e573d1ea6c6eea14f7d901fb91 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 9 Feb 2024 11:12:07 +1100 Subject: [PATCH 35/44] fix(ui): filter disabled LoRAs on sdxl --- .../src/features/nodes/util/graph/addSDXLLoRAstoGraph.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addSDXLLoRAstoGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addSDXLLoRAstoGraph.ts index 76927ad0f0..9553568922 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addSDXLLoRAstoGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addSDXLLoRAstoGraph.ts @@ -1,7 +1,7 @@ import type { RootState } from 'app/store/store'; import type { LoRAMetadataItem } from 'features/nodes/types/metadata'; import { zLoRAMetadataItem } from 'features/nodes/types/metadata'; -import { forEach, size } from 'lodash-es'; +import { filter, size } from 'lodash-es'; import type { NonNullableGraph, SDXLLoraLoaderInvocation } from 'services/api/types'; import { @@ -31,8 +31,8 @@ export const addSDXLLoRAsToGraph = ( * So we need to inject a LoRA chain into the graph. */ - const { loras } = state.lora; - const loraCount = size(loras); + const enabledLoRAs = filter(state.lora.loras, (l) => l.isEnabled ?? false); + const loraCount = size(enabledLoRAs); if (loraCount === 0) { return; @@ -59,7 +59,7 @@ export const addSDXLLoRAsToGraph = ( let lastLoraNodeId = ''; let currentLoraIndex = 0; - forEach(loras, (lora) => { + enabledLoRAs.forEach((lora) => { const { model_name, base_model, weight } = lora; const currentLoraNodeId = `${LORA_LOADER}_${model_name.replace('.', '_')}`; From da6e5b2ba1d93cfbceb37e94701dc332641aebfb Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 9 Feb 2024 11:16:25 +1100 Subject: [PATCH 36/44] fix(ui): fix lora count badge when none enabled --- .../GenerationSettingsAccordion.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx index ea6fd3563d..077875a8a7 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/GenerationSettingsAccordion/GenerationSettingsAccordion.tsx @@ -23,7 +23,7 @@ import ParamMainModelSelect from 'features/parameters/components/MainModel/Param import { selectGenerationSlice } from 'features/parameters/store/generationSlice'; import { useExpanderToggle } from 'features/settingsAccordions/hooks/useExpanderToggle'; import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; -import { filter, size } from 'lodash-es'; +import { filter } from 'lodash-es'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -33,7 +33,7 @@ const formLabelProps: FormLabelProps = { const badgesSelector = createMemoizedSelector(selectLoraSlice, selectGenerationSlice, (lora, generation) => { const enabledLoRAsCount = filter(lora.loras, (l) => !!l.isEnabled).length; - const loraTabBadges = size(lora.loras) ? [enabledLoRAsCount] : []; + const loraTabBadges = enabledLoRAsCount ? [enabledLoRAsCount] : []; const accordionBadges: (string | number)[] = []; if (generation.model) { accordionBadges.push(generation.model.model_name); From a60e2b7c776cace2aafd19bcbd04b702c9fd4b85 Mon Sep 17 00:00:00 2001 From: skunkworxdark Date: Thu, 8 Feb 2024 18:29:29 +0000 Subject: [PATCH 37/44] fix existing graphs with cfg_RescaleMultiplier not used --- .../features/nodes/util/graph/buildCanvasImageToImageGraph.ts | 1 + .../nodes/util/graph/buildCanvasSDXLImageToImageGraph.ts | 1 + .../features/nodes/util/graph/buildCanvasSDXLTextToImageGraph.ts | 1 + .../src/features/nodes/util/graph/buildCanvasTextToImageGraph.ts | 1 + .../features/nodes/util/graph/buildLinearImageToImageGraph.ts | 1 + .../nodes/util/graph/buildLinearSDXLImageToImageGraph.ts | 1 + .../features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts | 1 + 7 files changed, 7 insertions(+) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasImageToImageGraph.ts index 7a4ca0096f..3002e05441 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasImageToImageGraph.ts @@ -123,6 +123,7 @@ export const buildCanvasImageToImageGraph = (state: RootState, initialImage: Ima id: DENOISE_LATENTS, is_intermediate, cfg_scale, + cfg_rescale_multiplier, scheduler, steps, denoising_start: 1 - strength, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLImageToImageGraph.ts index 40f71930a3..1b586371a0 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLImageToImageGraph.ts @@ -124,6 +124,7 @@ export const buildCanvasSDXLImageToImageGraph = (state: RootState, initialImage: id: SDXL_DENOISE_LATENTS, is_intermediate, cfg_scale, + cfg_rescale_multiplier, scheduler, steps, denoising_start: refinerModel ? Math.min(refinerStart, 1 - strength) : 1 - strength, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLTextToImageGraph.ts index 772990ee0e..91d9da4cb5 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLTextToImageGraph.ts @@ -117,6 +117,7 @@ export const buildCanvasSDXLTextToImageGraph = (state: RootState): NonNullableGr id: SDXL_DENOISE_LATENTS, is_intermediate, cfg_scale, + cfg_rescale_multiplier, scheduler, steps, denoising_start: 0, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasTextToImageGraph.ts index 1869619346..967dd3ff4a 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasTextToImageGraph.ts @@ -115,6 +115,7 @@ export const buildCanvasTextToImageGraph = (state: RootState): NonNullableGraph id: DENOISE_LATENTS, is_intermediate, cfg_scale, + cfg_rescale_multiplier, scheduler, steps, denoising_start: 0, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearImageToImageGraph.ts index 15d46053b9..c76776d94d 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearImageToImageGraph.ts @@ -123,6 +123,7 @@ export const buildLinearImageToImageGraph = (state: RootState): NonNullableGraph type: 'denoise_latents', id: DENOISE_LATENTS, cfg_scale, + cfg_rescale_multiplier, scheduler, steps, denoising_start: 1 - strength, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLImageToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLImageToImageGraph.ts index 585d80fd41..9ae602bcac 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLImageToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLImageToImageGraph.ts @@ -126,6 +126,7 @@ export const buildLinearSDXLImageToImageGraph = (state: RootState): NonNullableG type: 'denoise_latents', id: SDXL_DENOISE_LATENTS, cfg_scale, + cfg_rescale_multiplier, scheduler, steps, denoising_start: refinerModel ? Math.min(refinerStart, 1 - strength) : 1 - strength, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts index 3af044e46d..222dc1a359 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildLinearSDXLTextToImageGraph.ts @@ -109,6 +109,7 @@ export const buildLinearSDXLTextToImageGraph = (state: RootState): NonNullableGr type: 'denoise_latents', id: SDXL_DENOISE_LATENTS, cfg_scale, + cfg_rescale_multiplier, scheduler, steps, denoising_start: 0, From c9c150f85009d8db7f6f1614763acd32b17ca3c7 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 9 Feb 2024 18:51:26 +1100 Subject: [PATCH 38/44] feat(ui): use cfgRescaleMultiplier on canvas graphs --- .../src/features/nodes/util/graph/buildCanvasInpaintGraph.ts | 3 +++ .../src/features/nodes/util/graph/buildCanvasOutpaintGraph.ts | 3 +++ .../features/nodes/util/graph/buildCanvasSDXLInpaintGraph.ts | 3 +++ .../features/nodes/util/graph/buildCanvasSDXLOutpaintGraph.ts | 3 +++ 4 files changed, 12 insertions(+) diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasInpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasInpaintGraph.ts index 407581c181..bb52a44a8e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasInpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasInpaintGraph.ts @@ -58,6 +58,7 @@ export const buildCanvasInpaintGraph = ( negativePrompt, model, cfgScale: cfg_scale, + cfgRescaleMultiplier: cfg_rescale_multiplier, scheduler, steps, img2imgStrength: strength, @@ -152,6 +153,7 @@ export const buildCanvasInpaintGraph = ( is_intermediate, steps: steps, cfg_scale: cfg_scale, + cfg_rescale_multiplier, scheduler: scheduler, denoising_start: 1 - strength, denoising_end: 1, @@ -175,6 +177,7 @@ export const buildCanvasInpaintGraph = ( is_intermediate, steps: canvasCoherenceSteps, cfg_scale: cfg_scale, + cfg_rescale_multiplier, scheduler: scheduler, denoising_start: 1 - canvasCoherenceStrength, denoising_end: 1, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasOutpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasOutpaintGraph.ts index 5614aa0604..b82b55cfee 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasOutpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasOutpaintGraph.ts @@ -60,6 +60,7 @@ export const buildCanvasOutpaintGraph = ( negativePrompt, model, cfgScale: cfg_scale, + cfgRescaleMultiplier: cfg_rescale_multiplier, scheduler, steps, img2imgStrength: strength, @@ -161,6 +162,7 @@ export const buildCanvasOutpaintGraph = ( is_intermediate, steps: steps, cfg_scale: cfg_scale, + cfg_rescale_multiplier, scheduler: scheduler, denoising_start: 1 - strength, denoising_end: 1, @@ -184,6 +186,7 @@ export const buildCanvasOutpaintGraph = ( is_intermediate, steps: canvasCoherenceSteps, cfg_scale: cfg_scale, + cfg_rescale_multiplier, scheduler: scheduler, denoising_start: 1 - canvasCoherenceStrength, denoising_end: 1, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLInpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLInpaintGraph.ts index afc0fd9386..00fea9a37e 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLInpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLInpaintGraph.ts @@ -60,6 +60,7 @@ export const buildCanvasSDXLInpaintGraph = ( negativePrompt, model, cfgScale: cfg_scale, + cfgRescaleMultiplier: cfg_rescale_multiplier, scheduler, steps, seed, @@ -151,6 +152,7 @@ export const buildCanvasSDXLInpaintGraph = ( is_intermediate, steps: steps, cfg_scale: cfg_scale, + cfg_rescale_multiplier, scheduler: scheduler, denoising_start: refinerModel ? Math.min(refinerStart, 1 - strength) : 1 - strength, denoising_end: refinerModel ? refinerStart : 1, @@ -174,6 +176,7 @@ export const buildCanvasSDXLInpaintGraph = ( is_intermediate, steps: canvasCoherenceSteps, cfg_scale: cfg_scale, + cfg_rescale_multiplier, scheduler: scheduler, denoising_start: 1 - canvasCoherenceStrength, denoising_end: 1, diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLOutpaintGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLOutpaintGraph.ts index 2d70dc219b..f85760d8f2 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLOutpaintGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/buildCanvasSDXLOutpaintGraph.ts @@ -62,6 +62,7 @@ export const buildCanvasSDXLOutpaintGraph = ( negativePrompt, model, cfgScale: cfg_scale, + cfgRescaleMultiplier: cfg_rescale_multiplier, scheduler, steps, seed, @@ -160,6 +161,7 @@ export const buildCanvasSDXLOutpaintGraph = ( is_intermediate, steps: steps, cfg_scale: cfg_scale, + cfg_rescale_multiplier, scheduler: scheduler, denoising_start: refinerModel ? Math.min(refinerStart, 1 - strength) : 1 - strength, denoising_end: refinerModel ? refinerStart : 1, @@ -183,6 +185,7 @@ export const buildCanvasSDXLOutpaintGraph = ( is_intermediate, steps: canvasCoherenceSteps, cfg_scale: cfg_scale, + cfg_rescale_multiplier, scheduler: scheduler, denoising_start: 1 - canvasCoherenceStrength, denoising_end: 1, From d20f98fb4fd3c2bbcdb9a6f6d2837b296c0d2da8 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 9 Feb 2024 20:28:19 +1100 Subject: [PATCH 39/44] fix(nodes): deep copy graph inputs The change to memory session storage brings a subtle behaviour change. Previously, we serialized and deserialized everything (e.g. field state, invocation outputs, etc) constantly. The meant we were effectively working with deep-copied objects at all time. We could mutate objects freely without worrying about other references to the object. With memory storage, objects are now passed around by reference, and we cannot handle them in the same way. This is problematic for nodes that mutate their own inputs. There are two ways this causes a problem: - An output is used as input for multiple nodes. If the first node mutates the output object while `invoke`ing, the next node will get the mutated object. - The invocation cache stores live python objects. When a node mutates an output pulled from the cache, the next node that uses the cached object will get the mutated object. The solution is to deep-copy a node's inputs as they are set, effectively reproducing the same behaviour as we had with the SQLite session storage. Nodes can safely mutate their inputs and those changes never leave the node's scope. Closes #5665 --- invokeai/app/services/shared/graph.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/invokeai/app/services/shared/graph.py b/invokeai/app/services/shared/graph.py index 854defc945..80f56b49d3 100644 --- a/invokeai/app/services/shared/graph.py +++ b/invokeai/app/services/shared/graph.py @@ -2,7 +2,7 @@ import copy import itertools -from typing import Annotated, Any, Optional, Union, get_args, get_origin, get_type_hints +from typing import Annotated, Any, Optional, TypeVar, Union, get_args, get_origin, get_type_hints import networkx as nx from pydantic import BaseModel, ConfigDict, field_validator, model_validator @@ -141,6 +141,16 @@ def are_connections_compatible( return are_connection_types_compatible(from_node_field, to_node_field) +T = TypeVar("T") + + +def copydeep(obj: T) -> T: + """Deep-copies an object. If it is a pydantic model, use the model's copy method.""" + if isinstance(obj, BaseModel): + return obj.model_copy(deep=True) + return copy.deepcopy(obj) + + class NodeAlreadyInGraphError(ValueError): pass @@ -1118,17 +1128,22 @@ class GraphExecutionState(BaseModel): def _prepare_inputs(self, node: BaseInvocation): input_edges = [e for e in self.execution_graph.edges if e.destination.node_id == node.id] + # Inputs must be deep-copied, else if a node mutates the object, other nodes that get the same input + # will see the mutation. if isinstance(node, CollectInvocation): output_collection = [ - getattr(self.results[edge.source.node_id], edge.source.field) + copydeep(getattr(self.results[edge.source.node_id], edge.source.field)) for edge in input_edges if edge.destination.field == "item" ] node.collection = output_collection else: for edge in input_edges: - output_value = getattr(self.results[edge.source.node_id], edge.source.field) - setattr(node, edge.destination.field, output_value) + setattr( + node, + edge.destination.field, + copydeep(getattr(self.results[edge.source.node_id], edge.source.field)), + ) # TODO: Add API for modifying underlying graph that checks if the change will be valid given the current execution state def _is_edge_valid(self, edge: Edge) -> bool: From b10d745dae9521051dde82e876c6e1ec02be3a0a Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Fri, 9 Feb 2024 09:16:07 +1100 Subject: [PATCH 40/44] fix(ui): when using control image dimensions, round to 8 The control image dimensions were set directly without rounding them to 8, causing an error during generation if they weren't a multiple of 8. --- .../components/ControlAdapterImagePreview.tsx | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx index 32aca81185..9321687219 100644 --- a/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx +++ b/invokeai/frontend/web/src/features/controlAdapters/components/ControlAdapterImagePreview.tsx @@ -5,6 +5,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; +import { roundToMultiple } from 'common/util/roundDownToMultiple'; import { setBoundingBoxDimensions } from 'features/canvas/store/canvasSlice'; import { useControlAdapterControlImage } from 'features/controlAdapters/hooks/useControlAdapterControlImage'; import { useControlAdapterProcessedControlImage } from 'features/controlAdapters/hooks/useControlAdapterProcessedControlImage'; @@ -91,19 +92,14 @@ const ControlAdapterImagePreview = ({ isSmall, id }: Props) => { return; } + const width = roundToMultiple(controlImage.width, 8); + const height = roundToMultiple(controlImage.height, 8); + if (activeTabName === 'unifiedCanvas') { - dispatch( - setBoundingBoxDimensions( - { - width: controlImage.width, - height: controlImage.height, - }, - optimalDimension - ) - ); + dispatch(setBoundingBoxDimensions({ width, height }, optimalDimension)); } else { - dispatch(widthChanged(controlImage.width)); - dispatch(heightChanged(controlImage.height)); + dispatch(widthChanged(width)); + dispatch(heightChanged(height)); } }, [controlImage, activeTabName, dispatch, optimalDimension]); From c5f069a255f3606e544780655e2c58de60ea9f14 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 11 Feb 2024 00:11:33 +1100 Subject: [PATCH 41/44] feat(backend): remove dependency on basicsr `basicsr` has a hard dependency on torchvision <= 0.16 and is unmaintained. Extract the code we need from it and remove the dep. Closes #5108 --- invokeai/app/invocations/upscale.py | 2 +- invokeai/backend/image_util/basicsr/LICENSE | 201 ++++++++++++++++++ .../backend/image_util/basicsr/__init__.py | 18 ++ .../backend/image_util/basicsr/arch_util.py | 75 +++++++ .../image_util/basicsr/rrdbnet_arch.py | 125 +++++++++++ .../image_util/realesrgan/realesrgan.py | 2 +- pyproject.toml | 1 - 7 files changed, 421 insertions(+), 3 deletions(-) create mode 100644 invokeai/backend/image_util/basicsr/LICENSE create mode 100644 invokeai/backend/image_util/basicsr/__init__.py create mode 100644 invokeai/backend/image_util/basicsr/arch_util.py create mode 100644 invokeai/backend/image_util/basicsr/rrdbnet_arch.py diff --git a/invokeai/app/invocations/upscale.py b/invokeai/app/invocations/upscale.py index fa86dead55..5f715c1a7e 100644 --- a/invokeai/app/invocations/upscale.py +++ b/invokeai/app/invocations/upscale.py @@ -5,12 +5,12 @@ from typing import Literal import cv2 import numpy as np import torch -from basicsr.archs.rrdbnet_arch import RRDBNet from PIL import Image from pydantic import ConfigDict from invokeai.app.invocations.primitives import ImageField, ImageOutput from invokeai.app.services.image_records.image_records_common import ImageCategory, ResourceOrigin +from invokeai.backend.image_util.basicsr.rrdbnet_arch import RRDBNet from invokeai.backend.image_util.realesrgan.realesrgan import RealESRGAN from invokeai.backend.util.devices import choose_torch_device diff --git a/invokeai/backend/image_util/basicsr/LICENSE b/invokeai/backend/image_util/basicsr/LICENSE new file mode 100644 index 0000000000..1c9b5b800e --- /dev/null +++ b/invokeai/backend/image_util/basicsr/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2022 BasicSR Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/invokeai/backend/image_util/basicsr/__init__.py b/invokeai/backend/image_util/basicsr/__init__.py new file mode 100644 index 0000000000..1d14b8e81e --- /dev/null +++ b/invokeai/backend/image_util/basicsr/__init__.py @@ -0,0 +1,18 @@ +""" +Adapted from https://github.com/XPixelGroup/BasicSR +License: Apache-2.0 + +As of Feb 2024, `basicsr` appears to be unmaintained. It imports a function from `torchvision` that is removed in +`torchvision` 0.17. Here is the deprecation warning: + + UserWarning: The torchvision.transforms.functional_tensor module is deprecated in 0.15 and will be **removed in + 0.17**. Please don't rely on it. You probably just need to use APIs in torchvision.transforms.functional or in + torchvision.transforms.v2.functional. + +As a result, a dependency on `basicsr` means we cannot keep our `torchvision` dependency up to date. + +Because we only rely on a single class `RRDBNet` from `basicsr`, we've copied the relevant code here and removed the +dependency on `basicsr`. + +The code is almost unchanged, only a few type annotations have been added. The license is also copied. +""" diff --git a/invokeai/backend/image_util/basicsr/arch_util.py b/invokeai/backend/image_util/basicsr/arch_util.py new file mode 100644 index 0000000000..45b3029ff8 --- /dev/null +++ b/invokeai/backend/image_util/basicsr/arch_util.py @@ -0,0 +1,75 @@ +from typing import Type + +import torch +from torch import nn as nn +from torch.nn import init as init +from torch.nn.modules.batchnorm import _BatchNorm + + +@torch.no_grad() +def default_init_weights( + module_list: list[nn.Module] | nn.Module, scale: float = 1, bias_fill: float = 0, **kwargs +) -> None: + """Initialize network weights. + + Args: + module_list (list[nn.Module] | nn.Module): Modules to be initialized. + scale (float): Scale initialized weights, especially for residual + blocks. Default: 1. + bias_fill (float): The value to fill bias. Default: 0 + kwargs (dict): Other arguments for initialization function. + """ + if not isinstance(module_list, list): + module_list = [module_list] + for module in module_list: + for m in module.modules(): + if isinstance(m, nn.Conv2d): + init.kaiming_normal_(m.weight, **kwargs) + m.weight.data *= scale + if m.bias is not None: + m.bias.data.fill_(bias_fill) + elif isinstance(m, nn.Linear): + init.kaiming_normal_(m.weight, **kwargs) + m.weight.data *= scale + if m.bias is not None: + m.bias.data.fill_(bias_fill) + elif isinstance(m, _BatchNorm): + init.constant_(m.weight, 1) + if m.bias is not None: + m.bias.data.fill_(bias_fill) + + +def make_layer(basic_block: Type[nn.Module], num_basic_block: int, **kwarg) -> nn.Sequential: + """Make layers by stacking the same blocks. + + Args: + basic_block (Type[nn.Module]): nn.Module class for basic block. + num_basic_block (int): number of blocks. + + Returns: + nn.Sequential: Stacked blocks in nn.Sequential. + """ + layers = [] + for _ in range(num_basic_block): + layers.append(basic_block(**kwarg)) + return nn.Sequential(*layers) + + +# TODO: may write a cpp file +def pixel_unshuffle(x: torch.Tensor, scale: int) -> torch.Tensor: + """Pixel unshuffle. + + Args: + x (Tensor): Input feature with shape (b, c, hh, hw). + scale (int): Downsample ratio. + + Returns: + Tensor: the pixel unshuffled feature. + """ + b, c, hh, hw = x.size() + out_channel = c * (scale**2) + assert hh % scale == 0 and hw % scale == 0 + h = hh // scale + w = hw // scale + x_view = x.view(b, c, h, scale, w, scale) + return x_view.permute(0, 1, 3, 5, 2, 4).reshape(b, out_channel, h, w) diff --git a/invokeai/backend/image_util/basicsr/rrdbnet_arch.py b/invokeai/backend/image_util/basicsr/rrdbnet_arch.py new file mode 100644 index 0000000000..cdb77f3c21 --- /dev/null +++ b/invokeai/backend/image_util/basicsr/rrdbnet_arch.py @@ -0,0 +1,125 @@ +import torch +from torch import nn as nn +from torch.nn import functional as F + +from .arch_util import default_init_weights, make_layer, pixel_unshuffle + + +class ResidualDenseBlock(nn.Module): + """Residual Dense Block. + + Used in RRDB block in ESRGAN. + + Args: + num_feat (int): Channel number of intermediate features. + num_grow_ch (int): Channels for each growth. + """ + + def __init__(self, num_feat: int = 64, num_grow_ch: int = 32) -> None: + super(ResidualDenseBlock, self).__init__() + self.conv1 = nn.Conv2d(num_feat, num_grow_ch, 3, 1, 1) + self.conv2 = nn.Conv2d(num_feat + num_grow_ch, num_grow_ch, 3, 1, 1) + self.conv3 = nn.Conv2d(num_feat + 2 * num_grow_ch, num_grow_ch, 3, 1, 1) + self.conv4 = nn.Conv2d(num_feat + 3 * num_grow_ch, num_grow_ch, 3, 1, 1) + self.conv5 = nn.Conv2d(num_feat + 4 * num_grow_ch, num_feat, 3, 1, 1) + + self.lrelu = nn.LeakyReLU(negative_slope=0.2, inplace=True) + + # initialization + default_init_weights([self.conv1, self.conv2, self.conv3, self.conv4, self.conv5], 0.1) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + x1 = self.lrelu(self.conv1(x)) + x2 = self.lrelu(self.conv2(torch.cat((x, x1), 1))) + x3 = self.lrelu(self.conv3(torch.cat((x, x1, x2), 1))) + x4 = self.lrelu(self.conv4(torch.cat((x, x1, x2, x3), 1))) + x5 = self.conv5(torch.cat((x, x1, x2, x3, x4), 1)) + # Empirically, we use 0.2 to scale the residual for better performance + return x5 * 0.2 + x + + +class RRDB(nn.Module): + """Residual in Residual Dense Block. + + Used in RRDB-Net in ESRGAN. + + Args: + num_feat (int): Channel number of intermediate features. + num_grow_ch (int): Channels for each growth. + """ + + def __init__(self, num_feat: int, num_grow_ch: int = 32) -> None: + super(RRDB, self).__init__() + self.rdb1 = ResidualDenseBlock(num_feat, num_grow_ch) + self.rdb2 = ResidualDenseBlock(num_feat, num_grow_ch) + self.rdb3 = ResidualDenseBlock(num_feat, num_grow_ch) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + out = self.rdb1(x) + out = self.rdb2(out) + out = self.rdb3(out) + # Empirically, we use 0.2 to scale the residual for better performance + return out * 0.2 + x + + +class RRDBNet(nn.Module): + """Networks consisting of Residual in Residual Dense Block, which is used + in ESRGAN. + + ESRGAN: Enhanced Super-Resolution Generative Adversarial Networks. + + We extend ESRGAN for scale x2 and scale x1. + Note: This is one option for scale 1, scale 2 in RRDBNet. + We first employ the pixel-unshuffle (an inverse operation of pixelshuffle to reduce the spatial size + and enlarge the channel size before feeding inputs into the main ESRGAN architecture. + + Args: + num_in_ch (int): Channel number of inputs. + num_out_ch (int): Channel number of outputs. + num_feat (int): Channel number of intermediate features. + Default: 64 + num_block (int): Block number in the trunk network. Defaults: 23 + num_grow_ch (int): Channels for each growth. Default: 32. + """ + + def __init__( + self, + num_in_ch: int, + num_out_ch: int, + scale: int = 4, + num_feat: int = 64, + num_block: int = 23, + num_grow_ch: int = 32, + ) -> None: + super(RRDBNet, self).__init__() + self.scale = scale + if scale == 2: + num_in_ch = num_in_ch * 4 + elif scale == 1: + num_in_ch = num_in_ch * 16 + self.conv_first = nn.Conv2d(num_in_ch, num_feat, 3, 1, 1) + self.body = make_layer(RRDB, num_block, num_feat=num_feat, num_grow_ch=num_grow_ch) + self.conv_body = nn.Conv2d(num_feat, num_feat, 3, 1, 1) + # upsample + self.conv_up1 = nn.Conv2d(num_feat, num_feat, 3, 1, 1) + self.conv_up2 = nn.Conv2d(num_feat, num_feat, 3, 1, 1) + self.conv_hr = nn.Conv2d(num_feat, num_feat, 3, 1, 1) + self.conv_last = nn.Conv2d(num_feat, num_out_ch, 3, 1, 1) + + self.lrelu = nn.LeakyReLU(negative_slope=0.2, inplace=True) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + if self.scale == 2: + feat = pixel_unshuffle(x, scale=2) + elif self.scale == 1: + feat = pixel_unshuffle(x, scale=4) + else: + feat = x + feat = self.conv_first(feat) + body_feat = self.conv_body(self.body(feat)) + feat = feat + body_feat + # upsample + feat = self.lrelu(self.conv_up1(F.interpolate(feat, scale_factor=2, mode="nearest"))) + feat = self.lrelu(self.conv_up2(F.interpolate(feat, scale_factor=2, mode="nearest"))) + out = self.conv_last(self.lrelu(self.conv_hr(feat))) + return out diff --git a/invokeai/backend/image_util/realesrgan/realesrgan.py b/invokeai/backend/image_util/realesrgan/realesrgan.py index 4d41dabc1e..c06504b608 100644 --- a/invokeai/backend/image_util/realesrgan/realesrgan.py +++ b/invokeai/backend/image_util/realesrgan/realesrgan.py @@ -7,10 +7,10 @@ import cv2 import numpy as np import numpy.typing as npt import torch -from basicsr.archs.rrdbnet_arch import RRDBNet from cv2.typing import MatLike from tqdm import tqdm +from invokeai.backend.image_util.basicsr.rrdbnet_arch import RRDBNet from invokeai.backend.util.devices import choose_torch_device """ diff --git a/pyproject.toml b/pyproject.toml index 78cbbe74ae..7f85c565c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,6 @@ classifiers = [ dependencies = [ # Core generation dependencies, pinned for reproducible builds. "accelerate==0.26.1", - "basicsr==1.4.2", "clip_anytorch==2.5.2", # replacing "clip @ https://github.com/openai/CLIP/archive/eaa22acb90a5876642d0507623e859909230a52d.zip", "compel==2.0.2", "controlnet-aux==0.0.7", From 83a7c9059fb011b7085fdce73ba10f0361cb4210 Mon Sep 17 00:00:00 2001 From: B N Date: Sat, 10 Feb 2024 17:02:07 +0100 Subject: [PATCH 42/44] translationBot(ui): update translation (German) Currently translated at 78.1% (1107 of 1416 strings) Co-authored-by: B N Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/de/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/de.json | 105 +++++++++++++------ 1 file changed, 71 insertions(+), 34 deletions(-) diff --git a/invokeai/frontend/web/public/locales/de.json b/invokeai/frontend/web/public/locales/de.json index 7b2693e1ab..db4d270ae5 100644 --- a/invokeai/frontend/web/public/locales/de.json +++ b/invokeai/frontend/web/public/locales/de.json @@ -56,7 +56,7 @@ "nodeEditor": "Knoten Editor", "statusMergingModels": "Modelle zusammenführen", "ipAdapter": "IP Adapter", - "controlAdapter": "Control Adapter", + "controlAdapter": "Control-Adapter", "auto": "Automatisch", "controlNet": "ControlNet", "imageFailedToLoad": "Kann Bild nicht laden", @@ -75,12 +75,12 @@ "linear": "Linear", "imagePrompt": "Bild Prompt", "checkpoint": "Checkpoint", - "inpaint": "inpaint", + "inpaint": "Inpaint", "simple": "Einfach", "template": "Vorlage", "outputs": "Ausgabe", "data": "Daten", - "safetensors": "Safetensors", + "safetensors": "Safe-Tensors", "outpaint": "Ausmalen", "details": "Details", "format": "Format", @@ -161,16 +161,16 @@ "hotkeys": { "keyboardShortcuts": "Tastenkürzel", "appHotkeys": "App-Tastenkombinationen", - "generalHotkeys": "Allgemeine Tastenkürzel", - "galleryHotkeys": "Galerie Tastenkürzel", - "unifiedCanvasHotkeys": "Unified Canvas Tastenkürzel", + "generalHotkeys": "Allgemein", + "galleryHotkeys": "Galerie", + "unifiedCanvasHotkeys": "Leinwand", "invoke": { "desc": "Ein Bild erzeugen", "title": "Invoke" }, "cancel": { "title": "Abbrechen", - "desc": "Bilderzeugung abbrechen" + "desc": "Aktuelle Bilderzeugung abbrechen" }, "focusPrompt": { "title": "Fokussiere Prompt", @@ -356,7 +356,7 @@ "title": "Staging-Bild akzeptieren", "desc": "Akzeptieren Sie das aktuelle Bild des Staging-Bereichs" }, - "nodesHotkeys": "Knoten Tastenkürzel", + "nodesHotkeys": "Knoten", "addNodes": { "title": "Knotenpunkt hinzufügen", "desc": "Öffnet das Menü zum Hinzufügen von Knoten" @@ -399,7 +399,7 @@ "vaeLocation": "VAE Ort", "vaeLocationValidationMsg": "Pfad zum Speicherort Ihres VAE.", "width": "Breite", - "widthValidationMsg": "Standardbreite Ihres Models.", + "widthValidationMsg": "Standardbreite Ihres Modells.", "height": "Höhe", "heightValidationMsg": "Standardbhöhe Ihres Models.", "addModel": "Modell hinzufügen", @@ -501,7 +501,7 @@ "quickAdd": "Schnell hinzufügen", "simpleModelDesc": "Geben Sie einen Pfad zu einem lokalen Diffusers-Modell, einem lokalen Checkpoint-/Safetensors-Modell, einer HuggingFace-Repo-ID oder einer Checkpoint-/Diffusers-Modell-URL an.", "modelDeleted": "Modell gelöscht", - "inpainting": "v1 Inpainting", + "inpainting": "V1-Inpainting", "modelUpdateFailed": "Modellaktualisierung fehlgeschlagen", "useCustomConfig": "Benutzerdefinierte Konfiguration verwenden", "settings": "Einstellungen", @@ -518,7 +518,7 @@ "interpolationType": "Interpolationstyp", "oliveModels": "Olives", "variant": "Variante", - "loraModels": "LoRAs", + "loraModels": "\"LoRAs\"", "modelDeleteFailed": "Modell konnte nicht gelöscht werden", "mergedModelName": "Zusammengeführter Modellname", "checkpointOrSafetensors": "$t(common.checkpoint) / $t(common.safetensors)", @@ -647,7 +647,7 @@ "upscale": "Verwenden Sie ESRGAN, um das Bild unmittelbar nach der Erzeugung zu vergrößern.", "faceCorrection": "Gesichtskorrektur mit GFPGAN oder Codeformer: Der Algorithmus erkennt Gesichter im Bild und korrigiert alle Fehler. Ein hoher Wert verändert das Bild stärker, was zu attraktiveren Gesichtern führt. Codeformer mit einer höheren Genauigkeit bewahrt das Originalbild auf Kosten einer stärkeren Gesichtskorrektur.", "imageToImage": "Bild zu Bild lädt ein beliebiges Bild als Ausgangsbild, aus dem dann zusammen mit dem Prompt ein neues Bild erzeugt wird. Je höher der Wert ist, desto stärker wird das Ergebnisbild verändert. Werte von 0,0 bis 1,0 sind möglich, der empfohlene Bereich ist .25-.75", - "boundingBox": "Der Begrenzungsrahmen ist derselbe wie die Einstellungen für Breite und Höhe bei Text zu Bild oder Bild zu Bild. Es wird nur der Bereich innerhalb des Rahmens verarbeitet.", + "boundingBox": "Der Begrenzungsrahmen ist derselbe wie die Einstellungen für Breite und Höhe bei Text-zu-Bild oder Bild-zu-Bild. Es wird nur der Bereich innerhalb des Rahmens verarbeitet.", "seamCorrection": "Steuert die Behandlung von sichtbaren Übergängen, die zwischen den erzeugten Bildern auf der Leinwand auftreten.", "infillAndScaling": "Verwalten Sie Infill-Methoden (für maskierte oder gelöschte Bereiche der Leinwand) und Skalierung (nützlich für kleine Begrenzungsrahmengrößen)." } @@ -727,11 +727,11 @@ "modifyConfig": "Optionen einstellen", "toggleAutoscroll": "Auroscroll ein/ausschalten", "toggleLogViewer": "Log Betrachter ein/ausschalten", - "showOptionsPanel": "Zeige Optionen", + "showOptionsPanel": "Seitenpanel anzeigen", "reset": "Zurücksetzten", "nextImage": "Nächstes Bild", "zoomOut": "Verkleinern", - "rotateCounterClockwise": "Gegen den Uhrzeigersinn verdrehen", + "rotateCounterClockwise": "Gegen den Uhrzeigersinn drehen", "showGalleryPanel": "Galeriefenster anzeigen", "exitViewer": "Betrachten beenden", "menu": "Menü", @@ -785,7 +785,7 @@ "depthMidasDescription": "Tiefenmap erstellen mit Midas", "controlnet": "$t(controlnet.controlAdapter_one) #{{number}} ($t(common.controlNet))", "t2iEnabledControlNetDisabled": "$t(common.t2iAdapter) ist aktiv, $t(common.controlNet) ist deaktiviert", - "weight": "Breite", + "weight": "Einfluss", "selectModel": "Wähle ein Modell", "depthMidas": "Tiefe (Midas)", "w": "W", @@ -810,14 +810,14 @@ "controlAdapter_other": "Control Adapter", "colorMapTileSize": "Kachelgröße", "depthZoeDescription": "Tiefenmap erstellen mit Zoe", - "setControlImageDimensions": "Setze Control Bild Auflösung auf Breite/Höhe", + "setControlImageDimensions": "Setze Control-Bild Auflösung auf Breite/Höhe", "handAndFace": "Hand und Gesicht", "enableIPAdapter": "Aktiviere IP Adapter", "resize": "Größe ändern", "resetControlImage": "Zurücksetzen vom Referenz Bild", "balanced": "Ausgewogen", "prompt": "Prompt", - "resizeMode": "Größenänderungsmodus", + "resizeMode": "Größe", "processor": "Prozessor", "saveControlImage": "Speichere Referenz Bild", "safe": "Speichern", @@ -842,10 +842,10 @@ "autoConfigure": "Prozessor automatisch konfigurieren", "normalBaeDescription": "Normale BAE-Verarbeitung", "noneDescription": "Es wurde keine Verarbeitung angewendet", - "openPose": "Openpose", - "lineartAnime": "Lineart Anime", + "openPose": "Openpose / \"Pose nutzen\"", + "lineartAnime": "Lineart Anime / \"Strichzeichnung Anime\"", "mediapipeFaceDescription": "Gesichtserkennung mit Mediapipe", - "canny": "Canny", + "canny": "\"Canny\"", "hedDescription": "Ganzheitlich verschachtelte Kantenerkennung", "scribble": "Scribble", "maxFaces": "Maximale Anzahl Gesichter", @@ -854,7 +854,7 @@ "modelSize": "Modell Größe", "small": "Klein", "base": "Basis", - "depthAnything": "Depth Anything", + "depthAnything": "Depth Anything / \"Tiefe irgendwas\"", "depthAnythingDescription": "Erstellung einer Tiefenkarte mit der Depth Anything-Technik" }, "queue": { @@ -916,7 +916,9 @@ "openQueue": "Warteschlange öffnen", "batchFailedToQueue": "Fehler beim Einreihen in die Stapelverarbeitung", "batchFieldValues": "Stapelverarbeitungswerte", - "batchQueued": "Stapelverarbeitung eingereiht" + "batchQueued": "Stapelverarbeitung eingereiht", + "graphQueued": "Graph eingereiht", + "graphFailedToQueue": "Fehler beim Einreihen des Graphen" }, "metadata": { "negativePrompt": "Negativ Beschreibung", @@ -937,22 +939,22 @@ "generationMode": "Generierungsmodus", "Threshold": "Rauschen-Schwelle", "seed": "Seed", - "perlin": "Perlin Noise", + "perlin": "Perlin-Rauschen", "hiresFix": "Optimierung für hohe Auflösungen", "initImage": "Erstes Bild", - "variations": "Samengewichtspaare", + "variations": "Seed-Gewichtungs-Paare", "vae": "VAE", "workflow": "Arbeitsablauf", "scheduler": "Planer", "noRecallParameters": "Es wurden keine Parameter zum Abrufen gefunden", - "recallParameters": "Recall Parameters" + "recallParameters": "Parameter wiederherstellen" }, "popovers": { "noiseUseCPU": { "heading": "Nutze Prozessor rauschen", "paragraphs": [ - null, - null, + "Entscheidet, ob auf der CPU oder GPU Rauschen erzeugt wird.", + "Mit aktiviertem CPU-Rauschen wird ein bestimmter Seedwert das gleiche Bild auf jeder Maschine erzeugen.", "CPU-Rauschen einzuschalten beeinflusst nicht die Systemleistung." ] }, @@ -984,10 +986,16 @@ ] }, "lora": { - "heading": "LoRA Gewichte" + "heading": "LoRA Gewichte", + "paragraphs": [ + "Höhere LoRA-Wichtungen führen zu größeren Auswirkungen auf das endgültige Bild." + ] }, "infillMethod": { - "heading": "Füllmethode" + "heading": "Füllmethode", + "paragraphs": [ + "Infill-Methode für den ausgewählten Bereich." + ] }, "paramVAE": { "heading": "VAE", @@ -1033,6 +1041,21 @@ "Deaktivieren Sie “Random Seed”, um identische Ergebnisse mit den gleichen Generierungseinstellungen zu erzeugen." ], "heading": "Seed" + }, + "dynamicPromptsMaxPrompts": { + "paragraphs": [ + "Beschränkt die Anzahl der Prompts, die von \"Dynamic Prompts\" generiert werden können." + ], + "heading": "Maximale Prompts" + }, + "dynamicPromptsSeedBehaviour": { + "paragraphs": [ + "Bestimmt, wie der Seed-Wert beim Erzeugen von Prompts verwendet wird.", + "Verwenden Sie dies, um schnelle Variationen eines einzigen Seeds zu erkunden.", + "Wenn Sie z. B. 5 Prompts haben, wird jedes Bild den selben Seed-Wert verwenden.", + "\"Per Bild\" wird einen einzigartigen Seed-Wert für jedes Bild verwenden. Dies bietet mehr Variationen." + ], + "heading": "Seed-Verhalten" } }, "ui": { @@ -1053,7 +1076,7 @@ "enableFailed": "Problem beim Aktivieren des Zwischenspeichers", "disableFailed": "Problem bei Deaktivierung des Cache", "enableSucceeded": "Zwischenspeicher aktiviert", - "disableSucceeded": "Aufrufcache deaktiviert", + "disableSucceeded": "Invocation-Cache deaktiviert", "clearSucceeded": "Zwischenspeicher gelöscht", "invocationCache": "Zwischenspeicher", "clearFailed": "Problem beim Löschen des Zwischenspeichers" @@ -1099,15 +1122,15 @@ "collectionFieldType": "{{name}} Sammlung", "controlCollectionDescription": "Kontrollinformationen zwischen Knotenpunkten weitergegeben.", "connectionWouldCreateCycle": "Verbindung würde einen Kreislauf/cycle schaffen", - "ipAdapterDescription": "Ein Adapter für die Bildabfrage (IP-Adapter) / Bilderprompt-Adapter.", + "ipAdapterDescription": "Ein Adapter für die Bildabfrage (IP-Adapter) / Bildprompt-Adapter.", "controlField": "Kontrolle", "inputFields": "Eingabefelder", "imageField": "Bild", "inputMayOnlyHaveOneConnection": "Eingang darf nur eine Verbindung haben", "integerCollectionDescription": "Eine Sammlung ganzer Zahlen.", - "integerDescription": "Das sind ganze Zahlen ohne Dezimalpunkt.", + "integerDescription": "\"Integer\" sind ganze Zahlen ohne Dezimalpunkt.", "conditioningPolymorphic": "Konditionierung polymorphisch", - "conditioningPolymorphicDescription": "Die Konditionierung kann zwischen den Knotenpunkten weitergegeben werden.", + "conditioningPolymorphicDescription": "Die Konditionierung kann zwischen den Knoten weitergegeben werden.", "invalidOutputSchema": "Ungültiges Ausgabeschema", "ipAdapterModel": "IP-Adapter Modell", "conditioningFieldDescription": "Die Konditionierung kann zwischen den Knotenpunkten weitergegeben werden.", @@ -1222,7 +1245,21 @@ "loRAModelField": "LoRA", "loRAModelFieldDescription": "Zu erledigen", "mainModelField": "Modell", - "doesNotExist": "existiert nicht" + "doesNotExist": "existiert nicht", + "vaeField": "VAE", + "unknownOutput": "Unbekannte Ausgabe: {{name}}", + "updateNode": "Knoten updaten", + "edge": "Rand / Kante", + "sourceNodeDoesNotExist": "Ungültiger Rand: Quell- / Ausgabe-Knoten {{node}} existiert nicht", + "updateAllNodes": "Update Knoten", + "allNodesUpdated": "Alle Knoten aktualisiert", + "unknownTemplate": "Unbekannte Vorlage", + "floatDescription": "Floats sind Zahlen mit einem Dezimalpunkt.", + "updateApp": "Update App", + "vaeFieldDescription": "VAE Submodell.", + "unknownInput": "Unbekannte Eingabe: {{name}}", + "unknownNodeType": "Unbekannter Knotentyp", + "float": "Kommazahlen" }, "hrf": { "enableHrf": "Korrektur für hohe Auflösungen", From 763816ca0ccbc0429f97365c5e9996309f3a07af Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 11 Feb 2024 00:34:47 +1100 Subject: [PATCH 43/44] chore: bump deps - pydantic 2.5.3 -> 2.6.1 - uvicorn 0.25.0 -> 0.27.1 --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7f85c565c6..d877bac4ff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,9 +58,9 @@ dependencies = [ "fastapi==0.109.2", "huggingface-hub==0.20.3", "pydantic-settings==2.1.0", - "pydantic==2.5.3", + "pydantic==2.6.1", "python-socketio==5.11.1", - "uvicorn[standard]==0.25.0", + "uvicorn[standard]==0.27.1", # Auxiliary dependencies, pinned only if necessary. "albumentations", From c45a43519a2cb5179a3017f322ed71801cbee892 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sun, 11 Feb 2024 00:36:54 +1100 Subject: [PATCH 44/44] chore: bump deps - ruff 0.1.11 -> 0.2.1 - update config format --- pyproject.toml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d877bac4ff..86a692f984 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -110,7 +110,7 @@ dependencies = [ ] "dev" = ["jurigged", "pudb", "snakeviz", "gprof2dot"] "test" = [ - "ruff==0.1.11", + "ruff==0.2.1", "ruff-lsp", "mypy", "pre-commit", @@ -139,7 +139,7 @@ dependencies = [ "invokeai-merge2" = "invokeai.frontend.merge.merge_diffusers2:main" "invokeai-ti" = "invokeai.frontend.training:invokeai_textual_inversion" "invokeai-model-install" = "invokeai.frontend.install.model_install:main" -"invokeai-model-install2" = "invokeai.frontend.install.model_install2:main" # will eventually be renamed to invokeai-model-install +"invokeai-model-install2" = "invokeai.frontend.install.model_install2:main" # will eventually be renamed to invokeai-model-install "invokeai-migrate3" = "invokeai.backend.install.migrate_to_3:main" "invokeai-update" = "invokeai.frontend.install.invokeai_update:main" "invokeai-metadata" = "invokeai.backend.image_util.invoke_metadata:main" @@ -206,13 +206,6 @@ output = "coverage/index.xml" #=== Begin: Ruff [tool.ruff] line-length = 120 -ignore = [ - "E501", # https://docs.astral.sh/ruff/rules/line-too-long/ - "C901", # https://docs.astral.sh/ruff/rules/complex-structure/ - "B008", # https://docs.astral.sh/ruff/rules/function-call-in-default-argument/ - "B904", # https://docs.astral.sh/ruff/rules/raise-without-from-inside-except/ -] -select = ["B", "C", "E", "F", "W", "I"] exclude = [ ".git", "__pycache__", @@ -221,6 +214,15 @@ exclude = [ "invokeai/frontend/web/node_modules/", ".venv*", ] + +[tool.ruff.lint] +ignore = [ + "E501", # https://docs.astral.sh/ruff/rules/line-too-long/ + "C901", # https://docs.astral.sh/ruff/rules/complex-structure/ + "B008", # https://docs.astral.sh/ruff/rules/function-call-in-default-argument/ + "B904", # https://docs.astral.sh/ruff/rules/raise-without-from-inside-except/ +] +select = ["B", "C", "E", "F", "W", "I"] #=== End: Ruff #=== Begin: MyPy