From bb2787584d65e9d3121b14e1a439eeb04e588e06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jan 2024 15:12:58 +0000 Subject: [PATCH 01/22] chore(deps-dev): bump vite in /invokeai/frontend/web Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.0.11 to 5.0.12. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v5.0.12/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v5.0.12/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- invokeai/frontend/web/package.json | 2 +- invokeai/frontend/web/pnpm-lock.yaml | 81 +++++++++++++--------------- 2 files changed, 37 insertions(+), 46 deletions(-) diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index ecc27350bc..d7399309d2 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -152,7 +152,7 @@ "storybook": "^7.6.10", "ts-toolbelt": "^9.6.0", "typescript": "^5.3.3", - "vite": "^5.0.11", + "vite": "^5.0.12", "vite-plugin-css-injected-by-js": "^3.3.1", "vite-plugin-dts": "^3.7.1", "vite-plugin-eslint": "^1.8.1", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index 98a307c7d9..a5ffce6809 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -209,7 +209,7 @@ devDependencies: version: 7.6.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) '@storybook/react-vite': specifier: ^7.6.10 - version: 7.6.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)(vite@5.0.11) + version: 7.6.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)(vite@5.0.12) '@storybook/test': specifier: ^7.6.10 version: 7.6.10 @@ -242,7 +242,7 @@ devDependencies: version: 6.19.0(eslint@8.56.0)(typescript@5.3.3) '@vitejs/plugin-react-swc': specifier: ^3.5.0 - version: 3.5.0(vite@5.0.11) + version: 3.5.0(vite@5.0.12) concurrently: specifier: ^8.2.2 version: 8.2.2 @@ -301,20 +301,20 @@ devDependencies: specifier: ^5.3.3 version: 5.3.3 vite: - specifier: ^5.0.11 - version: 5.0.11(@types/node@20.11.5) + specifier: ^5.0.12 + version: 5.0.12(@types/node@20.11.5) vite-plugin-css-injected-by-js: specifier: ^3.3.1 - version: 3.3.1(vite@5.0.11) + version: 3.3.1(vite@5.0.12) vite-plugin-dts: specifier: ^3.7.1 - version: 3.7.1(@types/node@20.11.5)(typescript@5.3.3)(vite@5.0.11) + version: 3.7.1(@types/node@20.11.5)(typescript@5.3.3)(vite@5.0.12) vite-plugin-eslint: specifier: ^1.8.1 - version: 1.8.1(eslint@8.56.0)(vite@5.0.11) + version: 1.8.1(eslint@8.56.0)(vite@5.0.12) vite-tsconfig-paths: specifier: ^4.3.1 - version: 4.3.1(typescript@5.3.3)(vite@5.0.11) + version: 4.3.1(typescript@5.3.3)(vite@5.0.12) packages: @@ -3713,7 +3713,7 @@ packages: chalk: 4.1.2 dev: true - /@joshwooding/vite-plugin-react-docgen-typescript@0.3.0(typescript@5.3.3)(vite@5.0.11): + /@joshwooding/vite-plugin-react-docgen-typescript@0.3.0(typescript@5.3.3)(vite@5.0.12): resolution: {integrity: sha512-2D6y7fNvFmsLmRt6UCOFJPvFoPMJGT0Uh1Wg0RaigUp7kdQPs6yYn8Dmx6GZkOH/NW0yMTwRz/p0SRMMRo50vA==} peerDependencies: typescript: '>= 4.3.x' @@ -3727,7 +3727,7 @@ packages: magic-string: 0.27.0 react-docgen-typescript: 2.2.2(typescript@5.3.3) typescript: 5.3.3 - vite: 5.0.11(@types/node@20.11.5) + vite: 5.0.12(@types/node@20.11.5) dev: true /@jridgewell/gen-mapping@0.3.3: @@ -4962,7 +4962,7 @@ packages: - supports-color dev: true - /@storybook/builder-vite@7.6.10(typescript@5.3.3)(vite@5.0.11): + /@storybook/builder-vite@7.6.10(typescript@5.3.3)(vite@5.0.12): resolution: {integrity: sha512-qxe19axiNJVdIKj943e1ucAmADwU42fTGgMSdBzzrvfH3pSOmx2057aIxRzd8YtBRnj327eeqpgCHYIDTunMYQ==} peerDependencies: '@preact/preset-vite': '*' @@ -4994,7 +4994,7 @@ packages: magic-string: 0.30.5 rollup: 3.29.4 typescript: 5.3.3 - vite: 5.0.11(@types/node@20.11.5) + vite: 5.0.12(@types/node@20.11.5) transitivePeerDependencies: - encoding - supports-color @@ -5350,7 +5350,7 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true - /@storybook/react-vite@7.6.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)(vite@5.0.11): + /@storybook/react-vite@7.6.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3)(vite@5.0.12): resolution: {integrity: sha512-YE2+J1wy8nO+c6Nv/hBMu91Edew3K184L1KSnfoZV8vtq2074k1Me/8pfe0QNuq631AncpfCYNb37yBAXQ/80w==} engines: {node: '>=16'} peerDependencies: @@ -5358,16 +5358,16 @@ packages: react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 vite: ^3.0.0 || ^4.0.0 || ^5.0.0 dependencies: - '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.3.3)(vite@5.0.11) + '@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.3.3)(vite@5.0.12) '@rollup/pluginutils': 5.1.0 - '@storybook/builder-vite': 7.6.10(typescript@5.3.3)(vite@5.0.11) + '@storybook/builder-vite': 7.6.10(typescript@5.3.3)(vite@5.0.12) '@storybook/react': 7.6.10(react-dom@18.2.0)(react@18.2.0)(typescript@5.3.3) - '@vitejs/plugin-react': 3.1.0(vite@5.0.11) + '@vitejs/plugin-react': 3.1.0(vite@5.0.12) magic-string: 0.30.5 react: 18.2.0 react-docgen: 7.0.3 react-dom: 18.2.0(react@18.2.0) - vite: 5.0.11(@types/node@20.11.5) + vite: 5.0.12(@types/node@20.11.5) transitivePeerDependencies: - '@preact/preset-vite' - encoding @@ -6442,18 +6442,18 @@ packages: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} dev: true - /@vitejs/plugin-react-swc@3.5.0(vite@5.0.11): + /@vitejs/plugin-react-swc@3.5.0(vite@5.0.12): resolution: {integrity: sha512-1PrOvAaDpqlCV+Up8RkAh9qaiUjoDUcjtttyhXDKw53XA6Ve16SOp6cCOpRs8Dj8DqUQs6eTW5YkLcLJjrXAig==} peerDependencies: vite: ^4 || ^5 dependencies: '@swc/core': 1.3.101 - vite: 5.0.11(@types/node@20.11.5) + vite: 5.0.12(@types/node@20.11.5) transitivePeerDependencies: - '@swc/helpers' dev: true - /@vitejs/plugin-react@3.1.0(vite@5.0.11): + /@vitejs/plugin-react@3.1.0(vite@5.0.12): resolution: {integrity: sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -6464,7 +6464,7 @@ packages: '@babel/plugin-transform-react-jsx-source': 7.23.3(@babel/core@7.23.7) magic-string: 0.27.0 react-refresh: 0.14.0 - vite: 5.0.11(@types/node@20.11.5) + vite: 5.0.12(@types/node@20.11.5) transitivePeerDependencies: - supports-color dev: true @@ -8405,7 +8405,7 @@ packages: dependencies: debug: 4.3.4 is-url: 1.2.4 - postcss: 8.4.32 + postcss: 8.4.33 postcss-values-parser: 2.0.1 transitivePeerDependencies: - supports-color @@ -8416,8 +8416,8 @@ packages: engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dependencies: is-url: 1.2.4 - postcss: 8.4.32 - postcss-values-parser: 6.0.2(postcss@8.4.32) + postcss: 8.4.33 + postcss-values-parser: 6.0.2(postcss@8.4.33) dev: true /detective-sass@3.0.2: @@ -11558,7 +11558,7 @@ packages: uniq: 1.0.1 dev: true - /postcss-values-parser@6.0.2(postcss@8.4.32): + /postcss-values-parser@6.0.2(postcss@8.4.33): resolution: {integrity: sha512-YLJpK0N1brcNJrs9WatuJFtHaV9q5aAOj+S4DI5S7jgHlRfm0PIbDCAFRYMQD5SHq7Fy6xsDhyutgS0QOAs0qw==} engines: {node: '>=10'} peerDependencies: @@ -11566,19 +11566,10 @@ packages: dependencies: color-name: 1.1.4 is-url-superb: 4.0.0 - postcss: 8.4.32 + postcss: 8.4.33 quote-unquote: 1.0.0 dev: true - /postcss@8.4.32: - resolution: {integrity: sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==} - engines: {node: ^10 || ^12 || >=14} - dependencies: - nanoid: 3.3.7 - picocolors: 1.0.0 - source-map-js: 1.0.2 - dev: true - /postcss@8.4.33: resolution: {integrity: sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==} engines: {node: ^10 || ^12 || >=14} @@ -13824,15 +13815,15 @@ packages: engines: {node: '>= 0.8'} dev: true - /vite-plugin-css-injected-by-js@3.3.1(vite@5.0.11): + /vite-plugin-css-injected-by-js@3.3.1(vite@5.0.12): resolution: {integrity: sha512-PjM/X45DR3/V1K1fTRs8HtZHEQ55kIfdrn+dzaqNBFrOYO073SeSNCxp4j7gSYhV9NffVHaEnOL4myoko0ePAg==} peerDependencies: vite: '>2.0.0-0' dependencies: - vite: 5.0.11(@types/node@20.11.5) + vite: 5.0.12(@types/node@20.11.5) dev: true - /vite-plugin-dts@3.7.1(@types/node@20.11.5)(typescript@5.3.3)(vite@5.0.11): + /vite-plugin-dts@3.7.1(@types/node@20.11.5)(typescript@5.3.3)(vite@5.0.12): resolution: {integrity: sha512-VZJckNFpVfRAkmOxhGT5OgTUVWVXxkNQqLpBUuiNGAr9HbtvmvsPLo2JB3Xhn+o/Z9+CT6YZfYa4bX9SGR5hNw==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -13848,7 +13839,7 @@ packages: debug: 4.3.4 kolorist: 1.8.0 typescript: 5.3.3 - vite: 5.0.11(@types/node@20.11.5) + vite: 5.0.12(@types/node@20.11.5) vue-tsc: 1.8.27(typescript@5.3.3) transitivePeerDependencies: - '@types/node' @@ -13856,7 +13847,7 @@ packages: - supports-color dev: true - /vite-plugin-eslint@1.8.1(eslint@8.56.0)(vite@5.0.11): + /vite-plugin-eslint@1.8.1(eslint@8.56.0)(vite@5.0.12): resolution: {integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==} peerDependencies: eslint: '>=7' @@ -13866,10 +13857,10 @@ packages: '@types/eslint': 8.56.0 eslint: 8.56.0 rollup: 2.79.1 - vite: 5.0.11(@types/node@20.11.5) + vite: 5.0.12(@types/node@20.11.5) dev: true - /vite-tsconfig-paths@4.3.1(typescript@5.3.3)(vite@5.0.11): + /vite-tsconfig-paths@4.3.1(typescript@5.3.3)(vite@5.0.12): resolution: {integrity: sha512-cfgJwcGOsIxXOLU/nELPny2/LUD/lcf1IbfyeKTv2bsupVbTH/xpFtdQlBmIP1GEK2CjjLxYhFfB+QODFAx5aw==} peerDependencies: vite: '*' @@ -13880,14 +13871,14 @@ packages: debug: 4.3.4 globrex: 0.1.2 tsconfck: 3.0.1(typescript@5.3.3) - vite: 5.0.11(@types/node@20.11.5) + vite: 5.0.12(@types/node@20.11.5) transitivePeerDependencies: - supports-color - typescript dev: true - /vite@5.0.11(@types/node@20.11.5): - resolution: {integrity: sha512-XBMnDjZcNAw/G1gEiskiM1v6yzM4GE5aMGvhWTlHAYYhxb7S3/V1s3m2LDHa8Vh6yIWYYB0iJwsEaS523c4oYA==} + /vite@5.0.12(@types/node@20.11.5): + resolution: {integrity: sha512-4hsnEkG3q0N4Tzf1+t6NdN9dg/L3BM+q8SWgbSPnJvrgH2kgdyzfVJwbR1ic69/4uMJJ/3dqDZZE5/WwqW8U1w==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: From 1e855f829014f5bbe5d0d724de4b762261bfe4d9 Mon Sep 17 00:00:00 2001 From: Wubbbi Date: Wed, 31 Jan 2024 05:54:56 +0100 Subject: [PATCH 02/22] Update safetensors and transformers to their latest versions (#5562) * Update Safetensors to the lastest version * Update Transformers while at it * Update transformers again --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 10536e0c15..11c0e60dad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,13 +46,13 @@ dependencies = [ "onnxruntime==1.16.3", "opencv-python==4.9.0.80", "pytorch-lightning==2.1.3", - "safetensors==0.4.1", + "safetensors==0.4.2", "timm==0.6.13", # needed to override timm latest in controlnet_aux, see https://github.com/isl-org/ZoeDepth/issues/26 "torch==2.1.2", "torchmetrics==0.11.4", "torchsde==0.2.6", "torchvision==0.16.2", - "transformers==4.37.0", + "transformers==4.37.2", # Core application dependencies, pinned for reproducible builds. "fastapi-events==0.10.0", From 0d4de4cc632311f90b563fa63c94ac5dc2763632 Mon Sep 17 00:00:00 2001 From: Rohinish <92542124+rohinish404@users.noreply.github.com> Date: Wed, 31 Jan 2024 10:27:16 +0530 Subject: [PATCH 03/22] changed hotkeys (#5542) Adds adds ctrl/meta + scroll to change brush size on canvas. * changed hotkeys * new hotkey as an additional * lint fixed" * added ctrl scroll and removed hotkey * using * added fix * feedbck_changes * brush size change logic * feat(ui): also check for meta key when modifying brush size * feat(ui): add comment linking to where brush size algo was determined --------- Co-authored-by: psychedelicious <4822129+psychedelicious@users.noreply.github.com> --- .../features/canvas/hooks/useCanvasZoom.ts | 71 +++++++++++-------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts index fbcc133f7f..bb1a90c50f 100644 --- a/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts +++ b/invokeai/frontend/web/src/features/canvas/hooks/useCanvasZoom.ts @@ -1,7 +1,8 @@ +import { $ctrl, $meta } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { $isMoveStageKeyHeld } from 'features/canvas/store/canvasNanostore'; -import { setStageCoordinates, setStageScale } from 'features/canvas/store/canvasSlice'; +import { setBrushSize, setStageCoordinates, setStageScale } from 'features/canvas/store/canvasSlice'; import { CANVAS_SCALE_BY, MAX_CANVAS_SCALE, MIN_CANVAS_SCALE } from 'features/canvas/util/constants'; import type Konva from 'konva'; import type { KonvaEventObject } from 'konva/lib/Node'; @@ -13,6 +14,7 @@ const useCanvasWheel = (stageRef: MutableRefObject) => { const dispatch = useAppDispatch(); const stageScale = useAppSelector((s) => s.canvas.stageScale); const isMoveStageKeyHeld = useStore($isMoveStageKeyHeld); + const brushSize = useAppSelector((s) => s.canvas.brushSize); return useCallback( (e: KonvaEventObject) => { @@ -23,36 +25,49 @@ const useCanvasWheel = (stageRef: MutableRefObject) => { e.evt.preventDefault(); - const cursorPos = stageRef.current.getPointerPosition(); + // checking for ctrl key is pressed or not, + // so that brush size can be controlled using ctrl + scroll up/down - if (!cursorPos) { - return; + if ($ctrl.get() || $meta.get()) { + // This equation was derived by fitting a curve to the desired brush sizes and deltas + // see https://github.com/invoke-ai/InvokeAI/pull/5542#issuecomment-1915847565 + const targetDelta = Math.sign(e.evt.deltaY) * 0.7363 * Math.pow(1.0394, brushSize); + // This needs to be clamped to prevent the delta from getting too large + const finalDelta = clamp(targetDelta, -20, 20); + // The new brush size is also clamped to prevent it from getting too large or small + const newBrushSize = clamp(brushSize + finalDelta, 1, 500); + + dispatch(setBrushSize(newBrushSize)); + } else { + const cursorPos = stageRef.current.getPointerPosition(); + let delta = e.evt.deltaY; + + if (!cursorPos) { + return; + } + + const mousePointTo = { + x: (cursorPos.x - stageRef.current.x()) / stageScale, + y: (cursorPos.y - stageRef.current.y()) / stageScale, + }; + // when we zoom on trackpad, e.evt.ctrlKey is true + // in that case lets revert direction + if (e.evt.ctrlKey) { + delta = -delta; + } + + const newScale = clamp(stageScale * CANVAS_SCALE_BY ** delta, MIN_CANVAS_SCALE, MAX_CANVAS_SCALE); + + const newCoordinates = { + x: cursorPos.x - mousePointTo.x * newScale, + y: cursorPos.y - mousePointTo.y * newScale, + }; + + dispatch(setStageScale(newScale)); + dispatch(setStageCoordinates(newCoordinates)); } - - const mousePointTo = { - x: (cursorPos.x - stageRef.current.x()) / stageScale, - y: (cursorPos.y - stageRef.current.y()) / stageScale, - }; - - let delta = e.evt.deltaY; - - // when we zoom on trackpad, e.evt.ctrlKey is true - // in that case lets revert direction - if (e.evt.ctrlKey) { - delta = -delta; - } - - const newScale = clamp(stageScale * CANVAS_SCALE_BY ** delta, MIN_CANVAS_SCALE, MAX_CANVAS_SCALE); - - const newCoordinates = { - x: cursorPos.x - mousePointTo.x * newScale, - y: cursorPos.y - mousePointTo.y * newScale, - }; - - dispatch(setStageScale(newScale)); - dispatch(setStageCoordinates(newCoordinates)); }, - [stageRef, isMoveStageKeyHeld, stageScale, dispatch] + [stageRef, isMoveStageKeyHeld, stageScale, dispatch, brushSize] ); }; From f70c0936ca9ee1697be99d2cbd1967fc842a605c Mon Sep 17 00:00:00 2001 From: Josh Corbett Date: Tue, 30 Jan 2024 22:50:03 -0700 Subject: [PATCH 04/22] feat: :sparkles: disable/enable LoRas with a switch (#5591) * feat: :sparkles: disable/enable LorRas with a switch * feat: :sparkles: visually display previous weight when disabled * style: :rotating_light: linting * feat: :sparkles: lora badge count reflects active loras * style: :rotating_light: linting * feat: :sparkles: track disabled lora on state instead of weight * style: :rotating_light: linting * feat: :sparkles: it all works now tracking isEnabled on lora state, disabled slider when disabled, removed disabled loras from graph, updated badge counting and renamed lora add function * style: :rotating_light: linting * fix: :bug: enabledLoRAs filter nullish coalescing * refactor: :art: minor changes renamed lora toggle action, removed errent comment, removed extraneous type annotation * style: :rotating_light: linting --- .../src/features/lora/components/LoRACard.tsx | 35 +++++++++++++------ .../web/src/features/lora/store/loraSlice.ts | 23 ++++++++++-- .../nodes/util/graph/addLoRAsToGraph.ts | 8 ++--- .../GenerationSettingsAccordion.tsx | 5 +-- 4 files changed, 51 insertions(+), 20 deletions(-) diff --git a/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx b/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx index f0c8e3fcd3..caedde875a 100644 --- a/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx +++ b/invokeai/frontend/web/src/features/lora/components/LoRACard.tsx @@ -4,12 +4,14 @@ import { CardHeader, CompositeNumberInput, CompositeSlider, + Flex, IconButton, + Switch, Text, } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import type { LoRA } from 'features/lora/store/loraSlice'; -import { loraRemoved, loraWeightChanged } from 'features/lora/store/loraSlice'; +import { loraIsEnabledChanged, loraRemoved, loraWeightChanged } from 'features/lora/store/loraSlice'; import { memo, useCallback } from 'react'; import { PiTrashSimpleBold } from 'react-icons/pi'; @@ -28,6 +30,10 @@ export const LoRACard = memo((props: LoRACardProps) => { [dispatch, lora.id] ); + const handleSetLoraToggle = useCallback(() => { + dispatch(loraIsEnabledChanged({ id: lora.id, isEnabled: !lora.isEnabled })); + }, [dispatch, lora.id, lora.isEnabled]); + const handleRemoveLora = useCallback(() => { dispatch(loraRemoved(lora.id)); }, [dispatch, lora.id]); @@ -35,16 +41,21 @@ export const LoRACard = memo((props: LoRACardProps) => { return ( - - {lora.model_name} - - } - /> + + + {lora.model_name} + + + + } + /> + + { step={0.01} marks={marks} defaultValue={0.75} + isDisabled={!lora.isEnabled} /> { w={20} flexShrink={0} defaultValue={0.75} + isDisabled={!lora.isEnabled} /> diff --git a/invokeai/frontend/web/src/features/lora/store/loraSlice.ts b/invokeai/frontend/web/src/features/lora/store/loraSlice.ts index 8562090fbc..7906443d7f 100644 --- a/invokeai/frontend/web/src/features/lora/store/loraSlice.ts +++ b/invokeai/frontend/web/src/features/lora/store/loraSlice.ts @@ -7,10 +7,12 @@ import type { LoRAModelConfigEntity } from 'services/api/endpoints/models'; export type LoRA = ParameterLoRAModel & { id: string; weight: number; + isEnabled?: boolean; }; -export const defaultLoRAConfig = { +export const defaultLoRAConfig: Pick = { weight: 0.75, + isEnabled: true, }; export type LoraState = { @@ -58,11 +60,26 @@ export const loraSlice = createSlice({ } lora.weight = defaultLoRAConfig.weight; }, + loraIsEnabledChanged: (state, action: PayloadAction>) => { + const { id, isEnabled } = action.payload; + const lora = state.loras[id]; + if (!lora) { + return; + } + lora.isEnabled = isEnabled; + }, }, }); -export const { loraAdded, loraRemoved, loraWeightChanged, loraWeightReset, lorasCleared, loraRecalled } = - loraSlice.actions; +export const { + loraAdded, + loraRemoved, + loraWeightChanged, + loraWeightReset, + loraIsEnabledChanged, + lorasCleared, + loraRecalled, +} = loraSlice.actions; export default loraSlice.reducer; diff --git a/invokeai/frontend/web/src/features/nodes/util/graph/addLoRAsToGraph.ts b/invokeai/frontend/web/src/features/nodes/util/graph/addLoRAsToGraph.ts index d1b5ddde84..3ed71b7529 100644 --- a/invokeai/frontend/web/src/features/nodes/util/graph/addLoRAsToGraph.ts +++ b/invokeai/frontend/web/src/features/nodes/util/graph/addLoRAsToGraph.ts @@ -1,5 +1,5 @@ import type { RootState } from 'app/store/store'; -import { forEach, size } from 'lodash-es'; +import { filter, size } from 'lodash-es'; import type { CoreMetadataInvocation, LoraLoaderInvocation, NonNullableGraph } from 'services/api/types'; import { @@ -28,8 +28,8 @@ export const addLoRAsToGraph = ( * 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; @@ -47,7 +47,7 @@ export const addLoRAsToGraph = ( let currentLoraIndex = 0; const loraMetadata: CoreMetadataInvocation['loras'] = []; - forEach(loras, (lora) => { + enabledLoRAs.forEach((lora) => { const { model_name, base_model, weight } = lora; const currentLoraNodeId = `${LORA_LOADER}_${model_name.replace('.', '_')}`; 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 d641ea9929..ea6fd3563d 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 { size } from 'lodash-es'; +import { filter, size } from 'lodash-es'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -32,7 +32,8 @@ const formLabelProps: FormLabelProps = { }; const badgesSelector = createMemoizedSelector(selectLoraSlice, selectGenerationSlice, (lora, generation) => { - const loraTabBadges = size(lora.loras) ? [size(lora.loras)] : []; + const enabledLoRAsCount = filter(lora.loras, (l) => !!l.isEnabled).length; + const loraTabBadges = size(lora.loras) ? [enabledLoRAsCount] : []; const accordionBadges: (string | number)[] = []; if (generation.model) { accordionBadges.push(generation.model.model_name); From 4602efd59877e1dcce6cd08097ee735364bcc5e1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 31 Jan 2024 21:51:57 +1100 Subject: [PATCH 05/22] feat: add profiler util (#5601) * feat(config): add profiling config settings - `profile_graphs` enables graph profiling with cProfile - `profiles_dir` sets the output for profiles * feat(nodes): add Profiler util Simple wrapper around cProfile. * feat(nodes): use Profiler in invocation processor * scripts: add generate_profile_graphs.sh script Helper to generate graphs for profiles. * pkg: add snakeviz and gprof2dot to dev deps These are useful for profiling. * tests: add tests for profiler util * fix(profiler): handle previous profile not stopped cleanly * feat(profiler): add profile_prefix config setting The prefix is used when writing profile output files. Useful to organise profiles into sessions. * tidy(profiler): add `_` to private API * feat(profiler): simplify API * feat(profiler): use child logger for profiler logs * chore(profiler): update docstrings * feat(profiler): stop() returns output path * chore(profiler): fix docstring * tests(profiler): update tests * chore: ruff --- .../app/services/config/config_default.py | 9 +++ .../invocation_processor_default.py | 19 +++++- invokeai/app/util/profiler.py | 67 +++++++++++++++++++ pyproject.toml | 2 +- scripts/generate_profile_graphs.sh | 27 ++++++++ tests/test_profiler.py | 53 +++++++++++++++ 6 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 invokeai/app/util/profiler.py create mode 100755 scripts/generate_profile_graphs.sh create mode 100644 tests/test_profiler.py diff --git a/invokeai/app/services/config/config_default.py b/invokeai/app/services/config/config_default.py index 83a831524d..3af906bd04 100644 --- a/invokeai/app/services/config/config_default.py +++ b/invokeai/app/services/config/config_default.py @@ -251,7 +251,11 @@ class InvokeAIAppConfig(InvokeAISettings): log_level : Literal["debug", "info", "warning", "error", "critical"] = Field(default="info", description="Emit logging messages at this level or higher", json_schema_extra=Categories.Logging) log_sql : bool = Field(default=False, description="Log SQL queries", json_schema_extra=Categories.Logging) + # Development dev_reload : bool = Field(default=False, description="Automatically reload when Python sources are changed.", json_schema_extra=Categories.Development) + profile_graphs : bool = Field(default=False, description="Enable graph profiling", json_schema_extra=Categories.Development) + profile_prefix : Optional[str] = Field(default=None, description="An optional prefix for profile output files.", json_schema_extra=Categories.Development) + profiles_dir : Path = Field(default=Path('profiles'), description="Directory for graph profiles", json_schema_extra=Categories.Development) version : bool = Field(default=False, description="Show InvokeAI version and exit", json_schema_extra=Categories.Other) @@ -449,6 +453,11 @@ class InvokeAIAppConfig(InvokeAISettings): disabled_in_config = not self.xformers_enabled return disabled_in_config and self.attention_type != "xformers" + @property + def profiles_path(self) -> Path: + """Path to the graph profiles directory.""" + return self._resolve(self.profiles_dir) + @staticmethod def find_root() -> Path: """Choose the runtime root directory when not specified on command line or init file.""" diff --git a/invokeai/app/services/invocation_processor/invocation_processor_default.py b/invokeai/app/services/invocation_processor/invocation_processor_default.py index 09608dca2b..1cfb0c1822 100644 --- a/invokeai/app/services/invocation_processor/invocation_processor_default.py +++ b/invokeai/app/services/invocation_processor/invocation_processor_default.py @@ -6,6 +6,7 @@ from typing import Optional import invokeai.backend.util.logging as logger from invokeai.app.invocations.baseinvocation import InvocationContext from invokeai.app.services.invocation_queue.invocation_queue_common import InvocationQueueItem +from invokeai.app.util.profiler import Profiler from ..invoker import Invoker from .invocation_processor_base import InvocationProcessorABC @@ -18,7 +19,7 @@ class DefaultInvocationProcessor(InvocationProcessorABC): __invoker: Invoker __threadLimit: BoundedSemaphore - def start(self, invoker) -> None: + def start(self, invoker: Invoker) -> None: # if we do want multithreading at some point, we could make this configurable self.__threadLimit = BoundedSemaphore(1) self.__invoker = invoker @@ -39,6 +40,16 @@ class DefaultInvocationProcessor(InvocationProcessorABC): self.__threadLimit.acquire() queue_item: Optional[InvocationQueueItem] = None + profiler = ( + Profiler( + logger=self.__invoker.services.logger, + output_dir=self.__invoker.services.configuration.profiles_path, + prefix=self.__invoker.services.configuration.profile_prefix, + ) + if self.__invoker.services.configuration.profile_graphs + else None + ) + while not stop_event.is_set(): try: queue_item = self.__invoker.services.queue.get() @@ -49,6 +60,10 @@ class DefaultInvocationProcessor(InvocationProcessorABC): # do not hammer the queue time.sleep(0.5) continue + + if profiler and profiler.profile_id != queue_item.graph_execution_state_id: + profiler.start(profile_id=queue_item.graph_execution_state_id) + try: graph_execution_state = self.__invoker.services.graph_execution_manager.get( queue_item.graph_execution_state_id @@ -201,6 +216,8 @@ class DefaultInvocationProcessor(InvocationProcessorABC): queue_id=queue_item.session_queue_id, graph_execution_state_id=graph_execution_state.id, ) + if profiler: + profiler.stop() except KeyboardInterrupt: pass # Log something? KeyboardInterrupt is probably not going to be seen by the processor diff --git a/invokeai/app/util/profiler.py b/invokeai/app/util/profiler.py new file mode 100644 index 0000000000..d1ce126b04 --- /dev/null +++ b/invokeai/app/util/profiler.py @@ -0,0 +1,67 @@ +import cProfile +from logging import Logger +from pathlib import Path +from typing import Optional + + +class Profiler: + """ + Simple wrapper around cProfile. + + Usage + ``` + # Create a profiler + profiler = Profiler(logger, output_dir, "sql_query_perf") + # Start a new profile + profiler.start("my_profile") + # Do stuff + profiler.stop() + ``` + + Visualize a profile as a flamegraph with [snakeviz](https://jiffyclub.github.io/snakeviz/) + ```sh + snakeviz my_profile.prof + ``` + + Visualize a profile as directed graph with [graphviz](https://graphviz.org/download/) & [gprof2dot](https://github.com/jrfonseca/gprof2dot) + ```sh + gprof2dot -f pstats my_profile.prof | dot -Tpng -o my_profile.png + # SVG or PDF may be nicer - you can search for function names + gprof2dot -f pstats my_profile.prof | dot -Tsvg -o my_profile.svg + gprof2dot -f pstats my_profile.prof | dot -Tpdf -o my_profile.pdf + ``` + """ + + def __init__(self, logger: Logger, output_dir: Path, prefix: Optional[str] = None) -> None: + self._logger = logger.getChild(f"profiler.{prefix}" if prefix else "profiler") + self._output_dir = output_dir + self._output_dir.mkdir(parents=True, exist_ok=True) + self._profiler: Optional[cProfile.Profile] = None + self._prefix = prefix + + self.profile_id: Optional[str] = None + + def start(self, profile_id: str) -> None: + if self._profiler: + self.stop() + + self.profile_id = profile_id + + self._profiler = cProfile.Profile() + self._profiler.enable() + self._logger.info(f"Started profiling {self.profile_id}.") + + def stop(self) -> Path: + if not self._profiler: + raise RuntimeError("Profiler not initialized. Call start() first.") + self._profiler.disable() + + filename = f"{self._prefix}_{self.profile_id}.prof" if self._prefix else f"{self.profile_id}.prof" + path = Path(self._output_dir, filename) + + self._profiler.dump_stats(path) + self._logger.info(f"Stopped profiling, profile dumped to {path}.") + self._profiler = None + self.profile_id = None + + return path diff --git a/pyproject.toml b/pyproject.toml index 11c0e60dad..b96a6756f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,7 +109,7 @@ dependencies = [ "mkdocs-git-revision-date-localized-plugin", "mkdocs-redirects==1.2.0", ] -"dev" = ["jurigged", "pudb"] +"dev" = ["jurigged", "pudb", "snakeviz", "gprof2dot"] "test" = [ "ruff==0.1.11", "ruff-lsp", diff --git a/scripts/generate_profile_graphs.sh b/scripts/generate_profile_graphs.sh new file mode 100755 index 0000000000..fa6cd9c103 --- /dev/null +++ b/scripts/generate_profile_graphs.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Accepts a path to a directory containing .prof files and generates a graphs +# for each of them. The default output format is pdf, but can be changed by +# providing a second argument. + +# Usage: ./generate_profile_graphs.sh +# is the path to the directory containing the .prof files +# is the type of graph to generate. Defaults to 'pdf' if not provided. +# Valid types are: 'svg', 'png' and 'pdf'. + +# Requires: +# - graphviz: https://graphviz.org/download/ +# - gprof2dot: https://github.com/jrfonseca/gprof2dot + +if [ -z "$1" ]; then + echo "Missing path to profiles directory" + exit 1 +fi + +type=${2:-pdf} + +for file in $1/*.prof; do + base_name=$(basename "$file" .prof) + gprof2dot -f pstats "$file" | dot -T$type -Glabel="Session ID ${base_name}" -Glabelloc="t" -o "$1/$base_name.$type" + echo "Generated $1/$base_name.$type" +done diff --git a/tests/test_profiler.py b/tests/test_profiler.py new file mode 100644 index 0000000000..ab152e1a2b --- /dev/null +++ b/tests/test_profiler.py @@ -0,0 +1,53 @@ +import re +from logging import Logger +from pathlib import Path +from tempfile import TemporaryDirectory + +import pytest + +from invokeai.app.util.profiler import Profiler + + +def test_profiler_starts(): + with TemporaryDirectory() as tempdir: + profiler = Profiler(logger=Logger("test_profiler"), output_dir=Path(tempdir)) + assert not profiler._profiler + assert not profiler.profile_id + profiler.start("test") + assert profiler._profiler + assert profiler.profile_id == "test" + profiler.stop() + assert not profiler._profiler + assert not profiler.profile_id + profiler.start("test2") + assert profiler._profiler + assert profiler.profile_id == "test2" + profiler.stop() + + +def test_profiler_profiles(): + with TemporaryDirectory() as tempdir: + profiler = Profiler(logger=Logger("test_profiler"), output_dir=Path(tempdir)) + profiler.start("test") + for _ in range(1000000): + pass + profiler.stop() + assert (Path(tempdir) / "test.prof").exists() + + +def test_profiler_profiles_with_prefix(): + with TemporaryDirectory() as tempdir: + profiler = Profiler(logger=Logger("test_profiler"), output_dir=Path(tempdir), prefix="prefix") + profiler.start("test") + for _ in range(1000000): + pass + profiler.stop() + assert (Path(tempdir) / "prefix_test.prof").exists() + + +def test_profile_fails_if_not_set_up(): + with TemporaryDirectory() as tempdir: + profiler = Profiler(logger=Logger("test_profiler"), output_dir=Path(tempdir)) + match = re.escape("Profiler not initialized. Call start() first.") + with pytest.raises(RuntimeError, match=match): + profiler.stop() From 8c6b3efd39edf51e8e07b3e48e5d310e4abbf847 Mon Sep 17 00:00:00 2001 From: Thomas Mello Date: Wed, 31 Jan 2024 04:16:15 +0300 Subject: [PATCH 06/22] fix(ui): remove hard reset of cursor on canvas during state reset Remove resetting cursor when resetting state letting event handlers to take care of presentation --- .../frontend/web/src/features/canvas/store/canvasNanostore.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasNanostore.ts b/invokeai/frontend/web/src/features/canvas/store/canvasNanostore.ts index 81ed62addf..f971886d5e 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasNanostore.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasNanostore.ts @@ -22,7 +22,6 @@ export const $isModifyingBoundingBox = computed( export const resetCanvasInteractionState = () => { $cursorPosition.set(null); $isDrawing.set(false); - $isMouseOverBoundingBox.set(false); $isMoveBoundingBoxKeyHeld.set(false); $isMoveStageKeyHeld.set(false); $isMovingBoundingBox.set(false); @@ -31,7 +30,6 @@ export const resetCanvasInteractionState = () => { export const resetToolInteractionState = () => { $isTransformingBoundingBox.set(false); - $isMouseOverBoundingBox.set(false); $isMovingBoundingBox.set(false); $isMovingStage.set(false); }; From 6aceae5c22f2fd4fed709e3f67cba9809c2b90d7 Mon Sep 17 00:00:00 2001 From: Riccardo Giovanetti Date: Wed, 31 Jan 2024 10:01:57 +0100 Subject: [PATCH 07/22] translationBot(ui): update translation (Italian) Currently translated at 97.2% (1388 of 1427 strings) Co-authored-by: Riccardo Giovanetti Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/it/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/it.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/invokeai/frontend/web/public/locales/it.json b/invokeai/frontend/web/public/locales/it.json index c7ba4b782b..c363ba1faf 100644 --- a/invokeai/frontend/web/public/locales/it.json +++ b/invokeai/frontend/web/public/locales/it.json @@ -125,7 +125,8 @@ "localSystem": "Sistema locale", "green": "Verde", "blue": "Blu", - "alpha": "Alfa" + "alpha": "Alfa", + "copy": "Copia" }, "gallery": { "generations": "Generazioni", From 191203ea0c457939576bc1788b6452d3d31172a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ufuk=20Sarp=20Sel=C3=A7ok?= Date: Wed, 31 Jan 2024 10:01:57 +0100 Subject: [PATCH 08/22] translationBot(ui): update translation (Turkish) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 36.1% (516 of 1427 strings) Co-authored-by: Ufuk Sarp Selçok Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/tr/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/tr.json | 446 ++++++++++++++++--- 1 file changed, 381 insertions(+), 65 deletions(-) diff --git a/invokeai/frontend/web/public/locales/tr.json b/invokeai/frontend/web/public/locales/tr.json index 77f13f291d..7bb9985e77 100644 --- a/invokeai/frontend/web/public/locales/tr.json +++ b/invokeai/frontend/web/public/locales/tr.json @@ -1,10 +1,10 @@ { "accessibility": { - "invokeProgressBar": "Invoke ilerleme durumu", - "nextImage": "Sonraki İmaj", + "invokeProgressBar": "Invoke durum çubuğu", + "nextImage": "Sonraki Görsel", "useThisParameter": "Bu ayarları kullan", - "copyMetadataJson": "Metadata verilerini kopyala (JSON)", - "exitViewer": "Görüntüleme Modundan Çık", + "copyMetadataJson": "Üstveriyi kopyala (JSON)", + "exitViewer": "Görüntüleyiciden Çık", "zoomIn": "Yakınlaştır", "zoomOut": "Uzaklaştır", "rotateCounterClockwise": "Saat yönünün tersine döndür", @@ -17,8 +17,8 @@ "showOptionsPanel": "Yan Paneli Göster", "modelSelect": "Model Seçimi", "reset": "Resetle", - "uploadImage": "İmaj Yükle", - "previousImage": "Önceki İmaj", + "uploadImage": "Görsel Yükle", + "previousImage": "Önceki Görsel", "menu": "Menü", "about": "Hakkında", "mode": "Kip", @@ -48,18 +48,18 @@ "langSimplifiedChinese": "Çince (Basit)", "langUkranian": "Ukraynaca", "langSpanish": "İspanyolca", - "txt2img": "Yazıdan İmaj", - "img2img": "İmajdan İmaj", + "txt2img": "Yazıdan Görsel", + "img2img": "Görselden Görsel", "linear": "Doğrusal", - "nodes": "İş Akış Düzenleyici", + "nodes": "İş Akışı Düzenleyici", "postprocessing": "Rötuş", "postProcessing": "Rötuş", "postProcessDesc2": "Daha gelişmiş iş akışlarına olanak sağlayacak özel bir arayüz yakında yayınlanacaktır.", - "postProcessDesc3": "Invoke AI Komut Satırı Arayüzü, Embiggen dahil birçok yeni özellik sunmaktadır.", + "postProcessDesc3": "Invoke AI Komut Satırı Arayüzü, içlerinde Embiggen da bulunan birçok özellik sunmaktadır.", "langKorean": "Korece", - "unifiedCanvas": "Akıllı Tuval", - "nodesDesc": "İmaj oluşturmak için hazırladığımız çizge tabanlı sistem şu an geliştirme aşamasındadır. Bu harika özellik hakkındaki gelişmeler için bizi takip etmeye devam edin.", - "postProcessDesc1": "Invoke AI birçok rötuş (post-process) aracı sağlar. İmaj büyütme ve yüz iyileştirme halihazırda WebUI üzerinden kullanılabilir. Bunlara Yazıdan İmaj ve İmajdan İmaj sekmelerindeki Gelişmiş Ayarlar menüsünden ulaşabilirsiniz. İsterseniz mevcut görüntü ekranının üzerindeki veya görüntüleyicideki imajı doğrudan üstteki tuşlar yardımıyla düzenleyebilirsiniz.", + "unifiedCanvas": "Tuval", + "nodesDesc": "Görsel oluşturmaya yardımcı çizge tabanlı sistem şimdilik geliştirme aşamasındadır. Bu süper özellik hakkındaki gelişmeler için kulağınız bizde olsun.", + "postProcessDesc1": "Invoke AI birçok rötuş (post-process) aracı sağlar. Görsel büyütme ve yüz iyileştirme WebUI üzerinden kullanıma uygun durumdadır. Bunlara Yazıdan Görsel ve Görselden Görsel sekmelerindeki Gelişmiş Ayarlar menüsünden ulaşabilirsiniz. Ayrıca var olan görseli üzerindeki düğmeler yardımıyla düzenleyebilirsiniz.", "batch": "Toplu İş Yöneticisi", "accept": "Onayla", "cancel": "Vazgeç", @@ -94,7 +94,7 @@ "save": "Kaydet", "statusMergingModels": "Modeller Birleştiriliyor", "statusGenerating": "Oluşturuluyor", - "statusGenerationComplete": "Oluşturma Tamamlandı", + "statusGenerationComplete": "Oluşturma Bitti", "statusGeneratingOutpainting": "Dışboyama Oluşturuluyor", "statusLoadingModel": "Model Yükleniyor", "random": "Rastgele", @@ -111,20 +111,20 @@ "statusRestoringFacesGFPGAN": "Yüzler İyileştiriliyor (GFPGAN)", "template": "Şablon", "saveAs": "Farklı Kaydet", - "statusProcessingComplete": "İşlem Tamamlandı", - "statusSavingImage": "İmaj Kaydediliyor", + "statusProcessingComplete": "İşlem Bitti", + "statusSavingImage": "Görsel Kaydediliyor", "somethingWentWrong": "Bir sorun oluştu", "statusConvertingModel": "Model Dönüştürülüyor", "statusDisconnected": "Bağlantı Kesildi", "statusError": "Hata", - "statusGeneratingImageToImage": "İmajdan İmaj Oluşturuluyor", + "statusGeneratingImageToImage": "Görselden Görsel Oluşturuluyor", "statusGeneratingInpainting": "İçboyama Oluşturuluyor", "statusRestoringFaces": "Yüzler İyileştiriliyor", "statusUpscaling": "Büyütme", "statusUpscalingESRGAN": "Büyütme (ESRGAN)", "training": "Eğitim", - "statusGeneratingTextToImage": "Yazıdan İmaj Oluşturuluyor", - "imagePrompt": "Resim İstemi", + "statusGeneratingTextToImage": "Yazıdan Görsel Oluşturuluyor", + "imagePrompt": "Görsel İstemi", "unknown": "Bilinmeyen", "green": "Yeşil", "red": "Kırmızı", @@ -137,8 +137,8 @@ "error": "Hata", "generate": "Oluştur", "free": "Serbest", - "imageFailedToLoad": "İmaj Yüklenemedi", - "safetensors": "Safetensor", + "imageFailedToLoad": "Görsel Yüklenemedi", + "safetensors": "Safetensors", "upload": "Yükle", "nextPage": "Sonraki Sayfa", "prevPage": "Önceki Sayfa", @@ -147,16 +147,22 @@ "direction": "Yön", "darkMode": "Koyu Tema", "unsaved": "Kaydedilmemiş", - "unknownError": "Bilinmeyen Hata" + "unknownError": "Bilinmeyen Hata", + "installed": "Yüklü", + "data": "Veri", + "input": "Giriş", + "copy": "Kopyala", + "created": "Yaratma", + "updated": "Güncelleme" }, "accordions": { "generation": { "title": "Oluşturma", "modelTab": "Model", - "conceptsTab": "Konseptler" + "conceptsTab": "Kavramlar" }, "image": { - "title": "İmaj" + "title": "Görsel" }, "advanced": { "title": "Gelişmiş" @@ -167,7 +173,7 @@ "infillTab": "Doldurma" }, "control": { - "ipTab": "Resim İstemleri" + "ipTab": "Görsel İstemleri" } }, "boards": { @@ -179,25 +185,25 @@ "myBoard": "Panom", "selectBoard": "Bir Pano Seç", "addBoard": "Pano Ekle", - "deleteBoardAndImages": "Panoyu ve İmajları Sil", + "deleteBoardAndImages": "Panoyu ve Görselleri Sil", "deleteBoardOnly": "Sadece Panoyu Sil", "deletedBoardsCannotbeRestored": "Silinen panolar geri getirilemez", "menuItemAutoAdd": "Bu panoya otomatik olarak ekle", "move": "Taşı", - "movingImagesToBoard_one": "{{count}} imajı şu panoya taşı:", - "movingImagesToBoard_other": "{{count}} imajı şu panoya taşı:", + "movingImagesToBoard_one": "{{count}} görseli şu panoya taşı:", + "movingImagesToBoard_other": "{{count}} görseli şu panoya taşı:", "noMatching": "Eşleşen pano yok", "searchBoard": "Pano Ara...", - "topMessage": "Bu pano, şu özelliklerde kullanılan imajlar içeriyor:", + "topMessage": "Bu pano, şuralarda kullanılan görseller içeriyor:", "downloadBoard": "Panoyu İndir", "uncategorized": "Kategorisiz", "changeBoard": "Panoyu Değiştir", - "bottomMessage": "Bu panoyu ve imajları silmek, bunları kullanan özelliklerin resetlemesine neden olacaktır." + "bottomMessage": "Bu panoyu ve görselleri silmek, bunları kullanan özelliklerin resetlemesine neden olacaktır." }, "controlnet": { "balanced": "Dengeli", - "contentShuffle": "İçerik Karma", - "contentShuffleDescription": "İmajın içeriğini karıştırır", + "contentShuffle": "İçerik Karıştırma", + "contentShuffleDescription": "Görselin içeriğini karıştırır", "depthZoe": "Derinlik (Zoe)", "depthZoeDescription": "Zoe kullanarak derinlik haritası oluşturma", "resizeMode": "Boyutlandırma Kipi", @@ -216,9 +222,9 @@ "noneDescription": "Hiçbir işlem uygulanmamış", "selectModel": "Model seçin", "showAdvanced": "Gelişmiş Ayarları Göster", - "controlNetT2IMutexDesc": "$t(common.controlNet) ve $t(common.t2iAdapter)'nün beraber kullanımı henüz desteklenmiyor.", + "controlNetT2IMutexDesc": "$t(common.controlNet) ve $t(common.t2iAdapter)'nün birlikte kullanımı şimdilik desteklenmiyor.", "canny": "Canny", - "colorMapDescription": "İmajdan bir renk haritası oluşturur", + "colorMapDescription": "Görselden renk haritası oluşturur", "handAndFace": "El ve Yüz", "processor": "İşlemci", "prompt": "İstem", @@ -233,26 +239,26 @@ "cannyDescription": "Canny kenar algılama", "fill": "Doldur", "highThreshold": "Üst Eşik", - "imageResolution": "İmaj Çözünürlüğü", + "imageResolution": "Görsel Çözünürlüğü", "colorMapTileSize": "Karo Boyutu", - "importImageFromCanvas": "Tuvalden İmajı içe Aktar", + "importImageFromCanvas": "Tuvaldeki Görseli Al", "importMaskFromCanvas": "Tuvalden Maskeyi İçe Aktar", "lowThreshold": "Alt Eşik", "base": "Taban", - "depthAnythingDescription": "Depth Anything tekniği ile derinlik haritası oluşturma" + "depthAnythingDescription": "Depth Anything yöntemi ile derinlik haritası oluşturma" }, "queue": { "queuedCount": "{{pending}} Sırada", "resumeSucceeded": "İşlem Sürdürüldü", "openQueue": "Sırayı Göster", - "cancelSucceeded": "Öğeden Vazgeçildi", - "cancelFailed": "Öğeden Vazgeçmede Sorun", + "cancelSucceeded": "İş Geri Çekildi", + "cancelFailed": "İşi Geri Çekmede Sorun", "prune": "Arındır", "pruneTooltip": "{{item_count}} Bitmiş İşi Sil", "resumeFailed": "İşlemi Sürdürmede Sorun", "pauseFailed": "İşlemi Duraklatmada Sorun", "cancelBatchSucceeded": "Toplu İşten Vazgeçildi", - "pruneSucceeded": "{{item_count}} Bitmiş İş Sıradan Kaldırıldı", + "pruneSucceeded": "{{item_count}} Bitmiş İş Sıradan Silindi", "in_progress": "İşleniyor", "completed": "Bitti", "canceled": "Vazgeçildi", @@ -269,34 +275,36 @@ "resume": "Sürdür", "queueTotal": "Toplam {{total}}", "queueEmpty": "Sıra Boş", - "clearQueueAlertDialog": "Sırayı boşaltma tuşu halihazırdaki işlemi durdurur ve sırayı tamamen boşaltır.", - "current": "Şu Anki", + "clearQueueAlertDialog": "Sırayı boşaltma düğmesi geçerli işlemi durdurur ve sırayı boşaltır.", + "current": "Şimdiki", "time": "Süre", "pause": "Duraklat", "pauseTooltip": "İşlemi Duraklat", "pruneFailed": "Sırayı Arındırmada Sorun", - "clearTooltip": "Vazgeç ve Tüm Öğeleri Sil", + "clearTooltip": "Vazgeç ve Tüm İşleri Sil", "clear": "Boşalt", "cancelBatchFailed": "Toplu İşten Vazgeçmede Sorun", "next": "Sonraki", "status": "Durum", "failed": "Başarısız", - "item": "Öğe", + "item": "İş", "enqueueing": "Toplu İş Sıraya Alınıyor", "pauseSucceeded": "İşlem Duraklatıldı", "cancel": "Vazgeç", - "cancelTooltip": "Şu Anki Öğeden Vazgeç", + "cancelTooltip": "Bu İşi Geri Çek", "clearSucceeded": "Sıra Boşaltıldı", "clearFailed": "Sırayı Boşaltmada Sorun", "cancelBatch": "Toplu İşten Vazgeç", - "cancelItem": "Öğeden Vazgeç", + "cancelItem": "İşi Geri Çek", "total": "Toplam", "pending": "Sırada", "completedIn": "'de bitirildi", "batch": "Toplu İş", "session": "Oturum", "batchQueued": "Toplu İş Sıraya Alındı", - "notReady": "Sıraya Alınamadı" + "notReady": "Sıraya Alınamadı", + "batchFieldValues": "Toplu İş Değişkenleri", + "queueMaxExceeded": "Sıra sınırı {{max_queue_size}} aşıldı, {{skip}} atlanıyor" }, "invocationCache": { "cacheSize": "Önbellek Boyutu", @@ -307,8 +315,8 @@ "enable": "Aç" }, "gallery": { - "deleteImageBin": "Silinen imajlar işletim sisteminin çöp kutusuna gönderilir.", - "deleteImagePermanent": "Silinen imajlar geri getirilemez.", + "deleteImageBin": "Silinen görseller işletim sisteminin çöp kutusuna gönderilir.", + "deleteImagePermanent": "Silinen görseller geri getirilemez.", "assets": "Özkaynaklar", "autoAssignBoardOnClick": "Tıklanan Panoya Otomatik Atama", "loading": "Yükleniyor", @@ -316,28 +324,32 @@ "download": "İndir", "deleteSelection": "Seçileni Sil", "preparingDownloadFailed": "İndirme Hazırlanırken Sorun", - "problemDeletingImages": "İmaj Silmede Sorun", - "featuresWillReset": "Bu imajı silerseniz, o özellikler resetlenecektir.", + "problemDeletingImages": "Görsel Silmede Sorun", + "featuresWillReset": "Bu görseli silerseniz, o özellikler resetlenecektir.", "galleryImageResetSize": "Boyutu Resetle", - "noImageSelected": "İmaj Seçili Değil", + "noImageSelected": "Görsel Seçilmedi", "unstarImage": "Yıldızı Kaldır", "uploads": "Yüklemeler", - "problemDeletingImagesDesc": "Bir ya da daha çok imaj silinemedi", + "problemDeletingImagesDesc": "Bir ya da daha çok görsel silinemedi", "gallerySettings": "Galeri Ayarları", - "image": "imaj", - "galleryImageSize": "İmaj Boyutu", - "allImagesLoaded": "Bütün İmajlar Yüklendi", + "image": "görsel", + "galleryImageSize": "Görsel Boyutu", + "allImagesLoaded": "Tüm Görseller Yüklendi", "copy": "Kopyala", - "noImagesInGallery": "Gösterilecek İmaj Yok", - "autoSwitchNewImages": "Yeni İmajı Biter Bitmez Gör", + "noImagesInGallery": "Gösterilecek Görsel Yok", + "autoSwitchNewImages": "Yeni Görseli Biter Bitmez Gör", "maintainAspectRatio": "En-Boy Oranını Koru", - "currentlyInUse": "Bu imaj şu an bu bölümlerde kullanımda:", - "deleteImage": "İmajı Sil", + "currentlyInUse": "Bu görsel şurada kullanımda:", + "deleteImage": "Görseli Sil", "loadMore": "Daha Getir", - "setCurrentImage": "Çalışma İmajı Yap", + "setCurrentImage": "Çalışma Görseli Yap", "unableToLoad": "Galeri Yüklenemedi", "downloadSelection": "Seçileni İndir", - "preparingDownload": "İndirmeye Hazırlanıyor" + "preparingDownload": "İndirmeye Hazırlanıyor", + "singleColumnLayout": "Tek Sütun Düzen", + "generations": "Çıktılar", + "showUploads": "Yüklenenleri Göster", + "showGenerations": "Çıktıları Göster" }, "hrf": { "hrf": "Yüksek Çözünürlük Kürü", @@ -350,17 +362,321 @@ "method": "Yüksek Çözünürlük Kürü Yöntemi" }, "upscaleMethod": "Büyütme Yöntemi", - "enableHrfTooltip": "Daha düşük bir başlangıç çözünürlüğüyle oluşturup ana çözünürlüğe büyütür ve İmajdan İmaj yapar." + "enableHrfTooltip": "Daha düşük bir çözünürlükle oluşturmaya başlar, ana çözünürlüğe büyütür ve Görselden Görsel'i çalıştırır." }, "hotkeys": { "noHotkeysFound": "Kısayol Tuşu Bulanamadı", "searchHotkeys": "Kısayol Tuşlarında Ara", - "clearSearch": "Aramayı Sil" + "clearSearch": "Aramayı Sil", + "colorPicker": { + "title": "Renk Seçici", + "desc": "Tuvalde renk seçiciye geçer" + }, + "consoleToggle": { + "title": "Konsolu Aç-Kapat", + "desc": "Konsolu aç-kapat" + }, + "hideMask": { + "desc": "Maskeyi gizle-göster", + "title": "Maskeyi Gizle" + }, + "focusPrompt": { + "title": "İsteme Odaklan", + "desc": "Görsel istemi alanına odaklanır" + }, + "keyboardShortcuts": "Kısayol Tuşları", + "nextImage": { + "title": "Sonraki Görsel", + "desc": "Galerideki sonraki görseli göster" + }, + "maximizeWorkSpace": { + "desc": "Panelleri kapat ve çalışma alanını genişlet", + "title": "Çalışma Alanını Genişlet" + }, + "pinOptions": { + "desc": "Ayar panelini iğnele", + "title": "Ayarları İğnele" + }, + "nodesHotkeys": "Çizgeler", + "quickToggleMove": { + "desc": "Geçici olarak Kayma Aracına geçer", + "title": "Geçici Kayma" + }, + "showHideBoundingBox": { + "title": "Sınırlayıcı Kutuyu Gizle/Göster", + "desc": "Sınırlayıcı kutunun görünürlüğünü değiştir" + }, + "showInfo": { + "desc": "Seçili görselin üstverisini göster", + "title": "Bilgileri Göster" + }, + "nextStagingImage": { + "desc": "Sonraki Görsel Parçayı Göster", + "title": "Sonraki Görsel Parça" + }, + "acceptStagingImage": { + "desc": "Geçiçi Görsel Parçasını Onayla", + "title": "Geçiçi Görsel Parçasını Onayla" + }, + "changeTabs": { + "desc": "Çalışma alanını değiştir", + "title": "Sekmeyi değiştir" + }, + "closePanels": { + "title": "Panelleri Kapat", + "desc": "Açık panelleri kapat" + }, + "decreaseBrushOpacity": { + "title": "Fırça Saydamlığını Artır", + "desc": "Tuval fırçasının saydamlığını artırır" + }, + "clearMask": { + "title": "Maskeyi Sil", + "desc": "Tüm maskeyi sil" + }, + "decreaseGalleryThumbSize": { + "desc": "Galerideki küçük görsel boyutunu düşürür", + "title": "Küçük Görsel Boyutunu Düşür" + }, + "deleteImage": { + "desc": "Seçili görseli sil", + "title": "Görseli Sil" + }, + "invoke": { + "desc": "Görsel Oluştur", + "title": "Invoke" + }, + "increaseGalleryThumbSize": { + "title": "Küçük Görsel Boyutunu Artır", + "desc": "Galerideki küçük görsel boyutunu artırır" + }, + "setParameters": { + "title": "Değişkenleri Kullan", + "desc": "Seçili görselin tüm değişkenlerini kullan" + }, + "setPrompt": { + "desc": "Seçili görselin istemini kullan", + "title": "İstemi Kullan" + }, + "toggleLayer": { + "desc": "Maske/Taban katmanları arasında geçiş yapar", + "title": "Katmanı Gizle-Göster" + }, + "upscale": { + "title": "Büyüt", + "desc": "Seçili görseli büyüt" + }, + "setSeed": { + "title": "Tohumu Kullan", + "desc": "Seçili görselin tohumunu kullan" + }, + "appHotkeys": "Uygulama", + "cancel": { + "desc": "Geçerli İşi Sil", + "title": "Vazgeç" + }, + "sendToImageToImage": { + "title": "Görselden Görsel'e Gönder", + "desc": "Seçili görseli Görselden Görsel'e gönder" + }, + "fillBoundingBox": { + "title": "Sınırlayıcı Kutuyu Doldur", + "desc": "Sınırlayıcı kutuyu fırçadaki renkle doldurur" + }, + "moveTool": { + "desc": "Tuvalde kaymayı sağlar", + "title": "Kayma Aracı" + }, + "redoStroke": { + "desc": "Fırça vuruşunu yinele", + "title": "Vuruşu Yinele" + }, + "increaseBrushOpacity": { + "title": "Fırçanın Saydamlığını Düşür", + "desc": "Tuval fırçasının saydamlığını düşürür" + }, + "selectEraser": { + "desc": "Tuval silgisini kullan", + "title": "Silgiyi Kullan" + }, + "toggleOptions": { + "desc": "Ayarlar panelini aç-kapat", + "title": "Ayarları Aç-Kapat" + }, + "copyToClipboard": { + "desc": "Tuval içeriğini kopyala", + "title": "Kopyala" + }, + "galleryHotkeys": "Galeri", + "generalHotkeys": "Genel", + "mergeVisible": { + "desc": "Tuvalin görünür tüm katmanlarını birleştir", + "title": "Katmanları Birleştir" + }, + "toggleGallery": { + "title": "Galeriyi Aç-Kapat", + "desc": "Galeri panelini aç-kapat" + }, + "downloadImage": { + "title": "Görseli İndir", + "desc": "Tuval içeriğini indir" + }, + "previousStagingImage": { + "title": "Önceki Görsel Parça", + "desc": "Önceki Görsel Parçayı Göster" + }, + "increaseBrushSize": { + "title": "Fırça Boyutunu Artır", + "desc": "Tuval fırçasının/silgisinin boyutunu artırır" + }, + "previousImage": { + "desc": "Galerideki önceki görseli göster", + "title": "Önceki Görsel" + }, + "toggleOptionsAndGallery": { + "title": "Ayarları ve Galeriyi Aç-Kapat", + "desc": "Ayarlar ve galeri panellerini aç-kapat" + }, + "toggleSnap": { + "desc": "Kılavuza Uydur", + "title": "Kılavuza Uydur" + }, + "resetView": { + "desc": "Tuval Görüşünü Resetle", + "title": "Görüşü Resetle" + }, + "cancelAndClear": { + "desc": "Geçerli işi geri çek ve sıradaki tüm işleri sil", + "title": "Vazgeç ve Sil" + }, + "decreaseBrushSize": { + "title": "Fırça Boyutunu Düşür", + "desc": "Tuval fırçasının/silgisinin boyutunu düşürür" + }, + "resetOptionsAndGallery": { + "desc": "Ayarlar ve galeri panellerini resetler", + "title": "Ayarları ve Galeriyi Resetle" + }, + "remixImage": { + "desc": "Seçili görselin tohumu hariç tüm değişkenlerini kullan", + "title": "Benzerini Türet" + }, + "undoStroke": { + "title": "Vuruşu Geri Al", + "desc": "Fırça vuruşunu geri al" + }, + "saveToGallery": { + "title": "Galeriye Gönder", + "desc": "Tuval içeriğini galeriye gönder" + }, + "unifiedCanvasHotkeys": "Tuval", + "addNodes": { + "desc": "Çizge ekleme menüsünü açar", + "title": "Çizge Ekle" + }, + "eraseBoundingBox": { + "desc": "Sınırlayıcı kutunun içini boşaltır", + "title": "Sınırlayıcı Kutuyu Boşalt" + }, + "selectBrush": { + "desc": "Tuval fırçasını kullan", + "title": "Fırçayı Kullan" + } }, "embedding": { "incompatibleModel": "Uyumsuz ana model:" }, "unifiedCanvas": { - "accept": "Onayla" + "accept": "Onayla", + "emptyTempImagesFolderMessage": "Geçici görsel klasörünü boşaltmak Tuvali resetler. Yineleme ve geri alma geçmişi, görsel parçası bölümü ve tuval taban katmanı da dolayısıla resetlenir.", + "clearCanvasHistoryMessage": "Tuval geçmişini silmek tuvale dokunmaz, ancak yineleme ve geri alma geçmişini geri dönülemez bir biçimde siler." + }, + "nodes": { + "unableToValidateWorkflow": "İş Akışı Doğrulanamadı", + "workflowContact": "İletişim", + "loadWorkflow": "İş Akışı Yükle", + "workflowNotes": "Notlar", + "workflow": "İş Akışı", + "notesDescription": "İş akışınız hakkında not düşün", + "workflowTags": "Etiketler", + "workflowDescription": "Kısa Tanım", + "workflowValidation": "İş Akışı Doğrulama Sorunu", + "workflowVersion": "Sürüm", + "newWorkflow": "Yeni İş Akışı", + "currentImageDescription": "İşlemdeki görseli Çizge Düzenleyicide gösterir", + "workflowAuthor": "Yaratıcı", + "workflowName": "Ad", + "workflowSettings": "İş Akışı Düzenleyici Ayarları", + "currentImage": "İşlemdeki Görsel", + "noWorkflow": "İş Akışı Yok", + "newWorkflowDesc": "Yeni iş akışı?", + "problemReadingWorkflow": "Görselden iş akışı çağrılamadı", + "downloadWorkflow": "İş Akışını İndir (JSON)", + "unableToMigrateWorkflow": "İş Akışı Aktarılamadı", + "unknownErrorValidatingWorkflow": "İş akışını doğrulamada bilinmeyen bir sorun", + "unableToGetWorkflowVersion": "İş akışı sürümüne ulaşılamadı", + "unrecognizedWorkflowVersion": "Tanınmayan iş akışı sürümü {{version}}", + "newWorkflowDesc2": "Geçerli iş akışında kaydedilmemiş değişiklikler var.", + "unableToLoadWorkflow": "İş Akışı Yüklenemedi" + }, + "workflows": { + "searchWorkflows": "İş Akışlarında Ara", + "workflowName": "İş Akışı Adı", + "problemSavingWorkflow": "İş Akışını Kaydetmede Sorun", + "saveWorkflow": "İş Akışını Kaydet", + "uploadWorkflow": "Dosyadan Yükle", + "newWorkflowCreated": "Yeni İş Akışı Yaratıldı", + "problemLoading": "İş Akışlarını Yüklemede Sorun", + "loading": "İş Akışları Yükleniyor", + "noDescription": "Tanımsız", + "workflowIsOpen": "İş Akışı Açık", + "clearWorkflowSearchFilter": "İş Akışı Aramasını Resetle", + "workflowEditorMenu": "İş Akışı Düzenleyici Menüsü", + "downloadWorkflow": "İndir", + "saveWorkflowAs": "İş Akışını Farklı Kaydet", + "savingWorkflow": "İş Akışı Kaydediliyor...", + "userWorkflows": "İş Akışlarım", + "defaultWorkflows": "Varsayılan İş Akışları", + "workflows": "İş Akışları", + "workflowLibrary": "Depo", + "deleteWorkflow": "İş Akışını Sil", + "unnamedWorkflow": "Adsız İş Akışı", + "noWorkflows": "İş Akışı Yok", + "workflowSaved": "İş Akışı Kaydedildi" + }, + "toast": { + "problemDownloadingCanvasDesc": "Taban katman indirilemedi", + "problemSavingMaskDesc": "Maske kaydedilemedi", + "problemSavingCanvasDesc": "Taban katman kaydedilemedi", + "problemRetrievingWorkflow": "İş Akışını Getirmede Sorun", + "workflowDeleted": "İş Akışı Silindi", + "loadedWithWarnings": "İş Akışı Yüklendi Ancak Uyarılar Var", + "problemImportingMaskDesc": "Maske aktarılamadı", + "problemMergingCanvasDesc": "Taban katman aktarılamadı", + "problemCopyingCanvasDesc": "Taban katman aktarılamadı", + "workflowLoaded": "İş Akışı Yüklendi", + "problemDeletingWorkflow": "İş Akışını Silmede Sorun" + }, + "metadata": { + "generationMode": "", + "workflow": "" + }, + "parameters": { + "invoke": { + "noPrompts": "İstem oluşturulmadı" + } + }, + "modelManager": { + "baseModel": "Ana Model" + }, + "dynamicPrompts": { + "loading": "Devimsel İstemler Oluşturuluyor...", + "combinatorial": "Birleşimsel Oluşturma" + }, + "models": { + "incompatibleBaseModel": "Uyumsuz ana model" + }, + "settings": { + "generation": "Oluşturma" } } From 74ae4d7774dd07afc2caf188b1eb77413dfb7a6a Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Wed, 31 Jan 2024 10:01:57 +0100 Subject: [PATCH 09/22] translationBot(ui): update translation files Updated by "Remove blank strings" hook in Weblate. Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/invokeai/web-ui/ Translation: InvokeAI/Web UI --- invokeai/frontend/web/public/locales/tr.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/invokeai/frontend/web/public/locales/tr.json b/invokeai/frontend/web/public/locales/tr.json index 7bb9985e77..fae92daa3a 100644 --- a/invokeai/frontend/web/public/locales/tr.json +++ b/invokeai/frontend/web/public/locales/tr.json @@ -657,10 +657,6 @@ "workflowLoaded": "İş Akışı Yüklendi", "problemDeletingWorkflow": "İş Akışını Silmede Sorun" }, - "metadata": { - "generationMode": "", - "workflow": "" - }, "parameters": { "invoke": { "noPrompts": "İstem oluşturulmadı" From f48a2c5fd254f34c097fb35be55247371d6a815d Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Feb 2024 00:02:39 +1100 Subject: [PATCH 10/22] fix(ui): workflow settings styling Got borked in the redesign. --- .../TopRightPanel/WorkflowEditorSettings.tsx | 80 ++++++++++++------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx index 152594f42a..cfb6c99043 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx @@ -1,7 +1,9 @@ +import type { FormLabelProps } from '@invoke-ai/ui-library'; import { Divider, Flex, FormControl, + FormControlGroup, FormHelperText, FormLabel, Heading, @@ -30,6 +32,8 @@ import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { SelectionMode } from 'reactflow'; +const formLabelProps: FormLabelProps = { flexGrow: 1 }; + const selector = createMemoizedSelector(selectNodesSlice, (nodes) => { const { shouldAnimateEdges, shouldValidateGraph, shouldSnapToGrid, shouldColorEdges, selectionMode } = nodes; return { @@ -100,37 +104,51 @@ const WorkflowEditorSettings = ({ children }: Props) => { {t('parameters.general')} - - {t('nodes.animatedEdges')} - - {t('nodes.animatedEdgesHelp')} - - - - {t('nodes.snapToGrid')} - - {t('nodes.snapToGridHelp')} - - - - {t('nodes.colorCodeEdges')} - - {t('nodes.colorCodeEdgesHelp')} - - - - {t('nodes.fullyContainNodes')} - - {t('nodes.fullyContainNodesHelp')} - - - {t('common.advanced')} - - - {t('nodes.validateConnections')} - - {t('nodes.validateConnectionsHelp')} - + + + + {t('nodes.animatedEdges')} + + + {t('nodes.animatedEdgesHelp')} + + + + + {t('nodes.snapToGrid')} + + + {t('nodes.snapToGridHelp')} + + + + + {t('nodes.colorCodeEdges')} + + + {t('nodes.colorCodeEdgesHelp')} + + + + + {t('nodes.fullyContainNodes')} + + + {t('nodes.fullyContainNodesHelp')} + + + + {t('common.advanced')} + + + + {t('nodes.validateConnections')} + + + {t('nodes.validateConnectionsHelp')} + + + From 14efc95707937264eadf052a5827aba06e0181e3 Mon Sep 17 00:00:00 2001 From: Brandon Rising Date: Tue, 30 Jan 2024 20:35:01 -0500 Subject: [PATCH 11/22] Allow passing of a civit api key --- invokeai/backend/install/model_install_backend.py | 7 ++++++- invokeai/backend/util/util.py | 3 +-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/invokeai/backend/install/model_install_backend.py b/invokeai/backend/install/model_install_backend.py index 2ffc1e6ff4..d412d0226f 100644 --- a/invokeai/backend/install/model_install_backend.py +++ b/invokeai/backend/install/model_install_backend.py @@ -104,12 +104,14 @@ class ModelInstall(object): prediction_type_helper: Optional[Callable[[Path], SchedulerPredictionType]] = None, model_manager: Optional[ModelManager] = None, access_token: Optional[str] = None, + civit_api_key: Optional[str] = None, ): self.config = config self.mgr = model_manager or ModelManager(config.model_conf_path) self.datasets = OmegaConf.load(Dataset_path) self.prediction_helper = prediction_type_helper self.access_token = access_token or HfFolder.get_token() + self.civit_api_key = civit_api_key or os.environ.get("CIVIT_API_KEY") self.reverse_paths = self._reverse_paths(self.datasets) def all_models(self) -> Dict[str, ModelLoadInfo]: @@ -326,7 +328,10 @@ class ModelInstall(object): def _install_url(self, url: str) -> AddModelResult: with TemporaryDirectory(dir=self.config.models_path) as staging: - location = download_with_resume(url, Path(staging)) + CIVITAI_RE = r".*civitai.com.*" + civit_url = re.match(CIVITAI_RE, url, re.IGNORECASE) + print(civit_url) + location = download_with_resume(url, Path(staging), access_token=self.civit_api_key if civit_url else None) if not location: logger.error(f"Unable to download {url}. Skipping.") info = ModelProbe().heuristic_probe(location, self.prediction_helper) diff --git a/invokeai/backend/util/util.py b/invokeai/backend/util/util.py index 4612b42cb9..85a4488915 100644 --- a/invokeai/backend/util/util.py +++ b/invokeai/backend/util/util.py @@ -286,9 +286,8 @@ def download_with_resume(url: str, dest: Path, access_token: str = None) -> Path open_mode = "wb" exist_size = 0 - resp = requests.get(url, header, stream=True) + resp = requests.get(url, headers=header, stream=True, allow_redirects=True) content_length = int(resp.headers.get("content-length", 0)) - if dest.is_dir(): try: file_name = re.search('filename="(.+)"', resp.headers.get("Content-Disposition")).group(1) From 088e3420e6883398f9bafac5fa1a0a1e687d7598 Mon Sep 17 00:00:00 2001 From: Brandon Rising Date: Tue, 30 Jan 2024 20:46:42 -0500 Subject: [PATCH 12/22] Allow passing of civit api key via config --- invokeai/app/services/config/config_default.py | 2 ++ invokeai/backend/install/model_install_backend.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/invokeai/app/services/config/config_default.py b/invokeai/app/services/config/config_default.py index 3af906bd04..c13900cfee 100644 --- a/invokeai/app/services/config/config_default.py +++ b/invokeai/app/services/config/config_default.py @@ -293,6 +293,8 @@ class InvokeAIAppConfig(InvokeAISettings): lora_dir : Optional[Path] = Field(default=None, description='Path to a directory of LoRA/LyCORIS models to be imported on startup.', json_schema_extra=Categories.Paths) embedding_dir : Optional[Path] = Field(default=None, description='Path to a directory of Textual Inversion embeddings to be imported on startup.', json_schema_extra=Categories.Paths) controlnet_dir : Optional[Path] = Field(default=None, description='Path to a directory of ControlNet embeddings to be imported on startup.', json_schema_extra=Categories.Paths) + + civit_api_key : Optional[str] = Field(default=os.environ.get("CIVIT_API_KEY"), description="API key for Civit", json_schema_extra=Categories.Other) # this is not referred to in the source code and can be removed entirely #free_gpu_mem : Optional[bool] = Field(default=None, description="If true, purge model from GPU after each generation.", json_schema_extra=Categories.MemoryPerformance) diff --git a/invokeai/backend/install/model_install_backend.py b/invokeai/backend/install/model_install_backend.py index d412d0226f..8f4fdece31 100644 --- a/invokeai/backend/install/model_install_backend.py +++ b/invokeai/backend/install/model_install_backend.py @@ -111,7 +111,7 @@ class ModelInstall(object): self.datasets = OmegaConf.load(Dataset_path) self.prediction_helper = prediction_type_helper self.access_token = access_token or HfFolder.get_token() - self.civit_api_key = civit_api_key or os.environ.get("CIVIT_API_KEY") + self.civit_api_key = civit_api_key or config.civit_api_key self.reverse_paths = self._reverse_paths(self.datasets) def all_models(self) -> Dict[str, ModelLoadInfo]: From 5d773dc94c9ed7acc21cca3d7ee5b4d6490e8950 Mon Sep 17 00:00:00 2001 From: Brandon Rising Date: Tue, 30 Jan 2024 20:47:36 -0500 Subject: [PATCH 13/22] Remove debug line --- invokeai/backend/install/model_install_backend.py | 1 - 1 file changed, 1 deletion(-) diff --git a/invokeai/backend/install/model_install_backend.py b/invokeai/backend/install/model_install_backend.py index 8f4fdece31..5d7c39beb9 100644 --- a/invokeai/backend/install/model_install_backend.py +++ b/invokeai/backend/install/model_install_backend.py @@ -330,7 +330,6 @@ class ModelInstall(object): with TemporaryDirectory(dir=self.config.models_path) as staging: CIVITAI_RE = r".*civitai.com.*" civit_url = re.match(CIVITAI_RE, url, re.IGNORECASE) - print(civit_url) location = download_with_resume(url, Path(staging), access_token=self.civit_api_key if civit_url else None) if not location: logger.error(f"Unable to download {url}. Skipping.") From 2c5ef92979f92717b860f1e9156d95e97a8aa0c3 Mon Sep 17 00:00:00 2001 From: Brandon Rising Date: Tue, 30 Jan 2024 21:05:15 -0500 Subject: [PATCH 14/22] Move location of config property, comment for explanation of use --- invokeai/app/services/config/config_default.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/invokeai/app/services/config/config_default.py b/invokeai/app/services/config/config_default.py index c13900cfee..510e60d7bc 100644 --- a/invokeai/app/services/config/config_default.py +++ b/invokeai/app/services/config/config_default.py @@ -284,6 +284,9 @@ class InvokeAIAppConfig(InvokeAISettings): deny_nodes : Optional[List[str]] = Field(default=None, description="List of nodes to deny. Omit to deny none.", json_schema_extra=Categories.Nodes) node_cache_size : int = Field(default=512, description="How many cached nodes to keep in memory", json_schema_extra=Categories.Nodes) + # MODEL IMPORT + civit_api_key : Optional[str] = Field(default=os.environ.get("CIVIT_API_KEY"), description="API key for Civit", json_schema_extra=Categories.Other) + # DEPRECATED FIELDS - STILL HERE IN ORDER TO OBTAN VALUES FROM PRE-3.1 CONFIG FILES always_use_cpu : bool = Field(default=False, description="If true, use the CPU for rendering even if a GPU is available.", json_schema_extra=Categories.MemoryPerformance) max_cache_size : Optional[float] = Field(default=None, gt=0, description="Maximum memory amount used by model cache for rapid switching", json_schema_extra=Categories.MemoryPerformance) @@ -294,7 +297,6 @@ class InvokeAIAppConfig(InvokeAISettings): embedding_dir : Optional[Path] = Field(default=None, description='Path to a directory of Textual Inversion embeddings to be imported on startup.', json_schema_extra=Categories.Paths) controlnet_dir : Optional[Path] = Field(default=None, description='Path to a directory of ControlNet embeddings to be imported on startup.', json_schema_extra=Categories.Paths) - civit_api_key : Optional[str] = Field(default=os.environ.get("CIVIT_API_KEY"), description="API key for Civit", json_schema_extra=Categories.Other) # this is not referred to in the source code and can be removed entirely #free_gpu_mem : Optional[bool] = Field(default=None, description="If true, purge model from GPU after each generation.", json_schema_extra=Categories.MemoryPerformance) From a769f93be0218c1c4030c9c2a21748acf864c01b Mon Sep 17 00:00:00 2001 From: Brandon Rising Date: Tue, 30 Jan 2024 21:06:17 -0500 Subject: [PATCH 15/22] Remove unnecessary change --- invokeai/backend/util/util.py | 1 + 1 file changed, 1 insertion(+) diff --git a/invokeai/backend/util/util.py b/invokeai/backend/util/util.py index 85a4488915..13751e2770 100644 --- a/invokeai/backend/util/util.py +++ b/invokeai/backend/util/util.py @@ -288,6 +288,7 @@ def download_with_resume(url: str, dest: Path, access_token: str = None) -> Path resp = requests.get(url, headers=header, stream=True, allow_redirects=True) content_length = int(resp.headers.get("content-length", 0)) + if dest.is_dir(): try: file_name = re.search('filename="(.+)"', resp.headers.get("Content-Disposition")).group(1) From 522ff4a042007e1710645d3ad2cc6010f7704104 Mon Sep 17 00:00:00 2001 From: Brandon Rising Date: Tue, 30 Jan 2024 22:25:23 -0500 Subject: [PATCH 16/22] civit -> civitai --- invokeai/app/services/config/config_default.py | 2 +- invokeai/backend/install/model_install_backend.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/invokeai/app/services/config/config_default.py b/invokeai/app/services/config/config_default.py index 510e60d7bc..65af5bc1c4 100644 --- a/invokeai/app/services/config/config_default.py +++ b/invokeai/app/services/config/config_default.py @@ -285,7 +285,7 @@ class InvokeAIAppConfig(InvokeAISettings): node_cache_size : int = Field(default=512, description="How many cached nodes to keep in memory", json_schema_extra=Categories.Nodes) # MODEL IMPORT - civit_api_key : Optional[str] = Field(default=os.environ.get("CIVIT_API_KEY"), description="API key for Civit", json_schema_extra=Categories.Other) + civitai_api_key : Optional[str] = Field(default=os.environ.get("CIVITAI_API_KEY"), description="API key for CivitAI", json_schema_extra=Categories.Other) # DEPRECATED FIELDS - STILL HERE IN ORDER TO OBTAN VALUES FROM PRE-3.1 CONFIG FILES always_use_cpu : bool = Field(default=False, description="If true, use the CPU for rendering even if a GPU is available.", json_schema_extra=Categories.MemoryPerformance) diff --git a/invokeai/backend/install/model_install_backend.py b/invokeai/backend/install/model_install_backend.py index 5d7c39beb9..601f83fc6f 100644 --- a/invokeai/backend/install/model_install_backend.py +++ b/invokeai/backend/install/model_install_backend.py @@ -104,14 +104,14 @@ class ModelInstall(object): prediction_type_helper: Optional[Callable[[Path], SchedulerPredictionType]] = None, model_manager: Optional[ModelManager] = None, access_token: Optional[str] = None, - civit_api_key: Optional[str] = None, + civitai_api_key: Optional[str] = None, ): self.config = config self.mgr = model_manager or ModelManager(config.model_conf_path) self.datasets = OmegaConf.load(Dataset_path) self.prediction_helper = prediction_type_helper self.access_token = access_token or HfFolder.get_token() - self.civit_api_key = civit_api_key or config.civit_api_key + self.civitai_api_key = civitai_api_key or config.civitai_api_key self.reverse_paths = self._reverse_paths(self.datasets) def all_models(self) -> Dict[str, ModelLoadInfo]: @@ -330,7 +330,7 @@ class ModelInstall(object): with TemporaryDirectory(dir=self.config.models_path) as staging: CIVITAI_RE = r".*civitai.com.*" civit_url = re.match(CIVITAI_RE, url, re.IGNORECASE) - location = download_with_resume(url, Path(staging), access_token=self.civit_api_key if civit_url else None) + location = download_with_resume(url, Path(staging), access_token=self.civitai_api_key if civit_url else None) if not location: logger.error(f"Unable to download {url}. Skipping.") info = ModelProbe().heuristic_probe(location, self.prediction_helper) From a0996b1c0aaf27d39dfc60b1bd5910e2ff3c6159 Mon Sep 17 00:00:00 2001 From: Brandon Rising Date: Tue, 30 Jan 2024 22:32:40 -0500 Subject: [PATCH 17/22] Fix ruff styling --- invokeai/backend/install/model_install_backend.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/invokeai/backend/install/model_install_backend.py b/invokeai/backend/install/model_install_backend.py index 601f83fc6f..fdbe714f62 100644 --- a/invokeai/backend/install/model_install_backend.py +++ b/invokeai/backend/install/model_install_backend.py @@ -330,7 +330,9 @@ class ModelInstall(object): with TemporaryDirectory(dir=self.config.models_path) as staging: CIVITAI_RE = r".*civitai.com.*" civit_url = re.match(CIVITAI_RE, url, re.IGNORECASE) - location = download_with_resume(url, Path(staging), access_token=self.civitai_api_key if civit_url else None) + location = download_with_resume( + url, Path(staging), access_token=self.civitai_api_key if civit_url else None + ) if not location: logger.error(f"Unable to download {url}. Skipping.") info = ModelProbe().heuristic_probe(location, self.prediction_helper) From f68f8898c0f053629cf28256b82f43d50421add9 Mon Sep 17 00:00:00 2001 From: Mary Hipp Rogers Date: Wed, 31 Jan 2024 08:32:31 -0500 Subject: [PATCH 18/22] Workflow navigation & save-as (#5607) * redo top panel of workflow editor * add checkbox option to save to project, integrate save-as flow into first time saving workflow * remove log * remove workflowLibrary as a feature that can be disabled * lint * feat(ui): make SaveWorkflowAsDialog a singleton Fixes an issue where the workflow name would erroneously be an empty string (which it should show the current workflow name). Also makes it easier to interact with this component. - Extract the dialog state to a hook - Render the dialog once in `` - Use the hook in the various buttons that should open the dialog - Fix a few wonkily named components (pre-existing issue) * fix(ui): when saving a never-before-saved workflow, do not append " (copy)" to the name * fix(ui): do not obscure workflow library button with add node popover This component is kinda janky :/ the popover content somehow renders invisibly over the button. I think it's related to the `. Need to redo this in the future, but for now, making the popover render lazily fixes this. --------- Co-authored-by: Mary Hipp Co-authored-by: psychedelicious <4822129+psychedelicious@users.noreply.github.com> --- invokeai/frontend/web/public/locales/en.json | 5 + .../frontend/web/src/app/types/invokeai.ts | 4 +- .../features/nodes/components/NodeEditor.tsx | 2 + .../flow/AddNodePopover/AddNodePopover.tsx | 1 + .../flow/panels/TopPanel/ClearFlowButton.tsx | 64 +++++++++++ .../panels/TopPanel/SaveWorkflowButton.tsx | 42 ++++++++ .../flow/panels/TopPanel/TopPanel.tsx | 19 ++-- .../panels/TopPanel/UpdateNodesButton.tsx | 12 ++- .../flow/panels/TopPanel/WorkflowName.tsx | 17 +-- .../panels/TopRightPanel/TopRightPanel.tsx | 5 +- .../src/features/nodes/store/workflowSlice.ts | 8 +- .../SaveWorkflowAsDialog.tsx | 102 ++++++++++++++++++ .../useSaveWorkflowAsDialog.ts | 52 +++++++++ .../components/WorkflowLibraryButton.tsx | 12 ++- .../SaveWorkflowAsMenuItem.tsx | 53 ++------- .../SaveWorkflowMenuItem.tsx | 30 +++++- .../WorkflowLibraryMenu.tsx | 13 +-- .../workflowLibrary/hooks/useSaveWorkflow.ts | 3 +- .../hooks/useSaveWorkflowAs.ts | 16 ++- 19 files changed, 362 insertions(+), 98 deletions(-) create mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/ClearFlowButton.tsx create mode 100644 invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton.tsx create mode 100644 invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog.tsx create mode 100644 invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowAsDialog/useSaveWorkflowAsDialog.ts diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 57b69199df..3df56c10ac 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -1014,6 +1014,9 @@ "newWorkflow": "New Workflow", "newWorkflowDesc": "Create a new workflow?", "newWorkflowDesc2": "Your current workflow has unsaved changes.", + "clearWorkflow": "Clear Workflow", + "clearWorkflowDesc": "Clear this workflow and start a new one?", + "clearWorkflowDesc2": "Your current workflow has unsaved changes.", "scheduler": "Scheduler", "schedulerDescription": "TODO", "sDXLMainModelField": "SDXL Model", @@ -1698,6 +1701,7 @@ "downloadWorkflow": "Save to File", "saveWorkflow": "Save Workflow", "saveWorkflowAs": "Save Workflow As", + "saveWorkflowToProject": "Save Workflow to Project", "savingWorkflow": "Saving Workflow...", "problemSavingWorkflow": "Problem Saving Workflow", "workflowSaved": "Workflow Saved", @@ -1712,6 +1716,7 @@ "clearWorkflowSearchFilter": "Clear Workflow Search Filter", "workflowName": "Workflow Name", "newWorkflowCreated": "New Workflow Created", + "workflowCleared": "Workflow Cleared", "workflowEditorMenu": "Workflow Editor Menu", "workflowIsOpen": "Workflow is Open" }, diff --git a/invokeai/frontend/web/src/app/types/invokeai.ts b/invokeai/frontend/web/src/app/types/invokeai.ts index faee66a9a9..d511812cb4 100644 --- a/invokeai/frontend/web/src/app/types/invokeai.ts +++ b/invokeai/frontend/web/src/app/types/invokeai.ts @@ -23,9 +23,7 @@ export type AppFeature = | 'resumeQueue' | 'prependQueue' | 'invocationCache' - | 'bulkDownload' - | 'workflowLibrary'; - + | 'bulkDownload'; /** * A disable-able Stable Diffusion feature */ diff --git a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx index 501d263c38..bb801b6f39 100644 --- a/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/NodeEditor.tsx @@ -4,6 +4,7 @@ import { Flex } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel'; +import { SaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog'; import type { AnimationProps } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion'; import type { CSSProperties } from 'react'; @@ -59,6 +60,7 @@ const NodeEditor = () => { + )} diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx index faa4c0b054..b24b52c6ab 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx @@ -179,6 +179,7 @@ const AddNodePopover = () => { closeOnBlur={true} returnFocusOnClose={true} initialFocusRef={inputRef} + isLazy > diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/ClearFlowButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/ClearFlowButton.tsx new file mode 100644 index 0000000000..3ea6c54c2c --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/ClearFlowButton.tsx @@ -0,0 +1,64 @@ +import { ConfirmationAlertDialog, Flex, IconButton, Text, useDisclosure } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiTrashSimpleFill } from 'react-icons/pi'; + +import { addToast } from '../../../../../system/store/systemSlice'; +import { makeToast } from '../../../../../system/util/makeToast'; +import { nodeEditorReset } from '../../../../store/nodesSlice'; + +const ClearFlowButton = () => { + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + const { isOpen, onOpen, onClose } = useDisclosure(); + const isTouched = useAppSelector((s) => s.workflow.isTouched); + + const handleNewWorkflow = useCallback(() => { + dispatch(nodeEditorReset()); + + dispatch( + addToast( + makeToast({ + title: t('workflows.workflowCleared'), + status: 'success', + }) + ) + ); + + onClose(); + }, [dispatch, onClose, t]); + + const onClick = useCallback(() => { + if (!isTouched) { + handleNewWorkflow(); + return; + } + onOpen(); + }, [handleNewWorkflow, isTouched, onOpen]); + + return ( + <> + } + onClick={onClick} + pointerEvents="auto" + /> + + + {t('nodes.clearWorkflowDesc')} + {t('nodes.clearWorkflowDesc2')} + + + + ); +}; + +export default memo(ClearFlowButton); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton.tsx new file mode 100644 index 0000000000..2d0abf3af5 --- /dev/null +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton.tsx @@ -0,0 +1,42 @@ +import { IconButton } from '@invoke-ai/ui-library'; +import { useAppSelector } from 'app/store/storeHooks'; +import { useSaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/useSaveWorkflowAsDialog'; +import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { PiFloppyDiskBold } from 'react-icons/pi'; + +import { isWorkflowWithID, useSaveLibraryWorkflow } from '../../../../../workflowLibrary/hooks/useSaveWorkflow'; +import { $builtWorkflow } from '../../../../hooks/useWorkflowWatcher'; + +const SaveWorkflowButton = () => { + const { t } = useTranslation(); + const isTouched = useAppSelector((s) => s.workflow.isTouched); + const { onOpen } = useSaveWorkflowAsDialog(); + const { saveWorkflow } = useSaveLibraryWorkflow(); + + const handleClickSave = useCallback(async () => { + const builtWorkflow = $builtWorkflow.get(); + if (!builtWorkflow) { + return; + } + + if (isWorkflowWithID(builtWorkflow)) { + saveWorkflow(); + } else { + onOpen(); + } + }, [onOpen, saveWorkflow]); + + return ( + } + isDisabled={!isTouched} + onClick={handleClickSave} + pointerEvents="auto" + /> + ); +}; + +export default memo(SaveWorkflowButton); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx index 442b0d33b8..c87af124bf 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx @@ -1,23 +1,28 @@ import { Flex, Spacer } from '@invoke-ai/ui-library'; import AddNodeButton from 'features/nodes/components/flow/panels/TopPanel/AddNodeButton'; +import ClearFlowButton from 'features/nodes/components/flow/panels/TopPanel/ClearFlowButton'; +import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton'; import UpdateNodesButton from 'features/nodes/components/flow/panels/TopPanel/UpdateNodesButton'; import WorkflowName from 'features/nodes/components/flow/panels/TopPanel/WorkflowName'; -import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton'; import WorkflowLibraryMenu from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu'; import { memo } from 'react'; const TopCenterPanel = () => { - const isWorkflowLibraryEnabled = useFeatureStatus('workflowLibrary').isFeatureEnabled; - return ( - - - + + + + + + + + - {isWorkflowLibraryEnabled && } + + ); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/UpdateNodesButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/UpdateNodesButton.tsx index 300eb19396..d356eaa4e1 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/UpdateNodesButton.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/UpdateNodesButton.tsx @@ -1,4 +1,4 @@ -import { Button } from '@invoke-ai/ui-library'; +import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { useGetNodesNeedUpdate } from 'features/nodes/hooks/useGetNodesNeedUpdate'; import { updateAllNodesRequested } from 'features/nodes/store/actions'; @@ -19,9 +19,13 @@ const UpdateNodesButton = () => { } return ( - + } + onClick={handleClickUpdateNodes} + pointerEvents="auto" + /> ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/WorkflowName.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/WorkflowName.tsx index 5573e89270..527147c67d 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/WorkflowName.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/WorkflowName.tsx @@ -1,26 +1,13 @@ import { Text } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; -import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; -import { memo, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; +import { memo } from 'react'; const TopCenterPanel = () => { - const { t } = useTranslation(); const name = useAppSelector((s) => s.workflow.name); - const isTouched = useAppSelector((s) => s.workflow.isTouched); - const isWorkflowLibraryEnabled = useFeatureStatus('workflowLibrary').isFeatureEnabled; - - const displayName = useMemo(() => { - let _displayName = name || t('workflows.unnamedWorkflow'); - if (isTouched && isWorkflowLibraryEnabled) { - _displayName += ` (${t('common.unsaved')})`; - } - return _displayName; - }, [t, name, isTouched, isWorkflowLibraryEnabled]); return ( - {displayName} + {name} ); }; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx index f3c903bf4a..be939f35bd 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/TopRightPanel.tsx @@ -1,15 +1,12 @@ import { Flex } from '@invoke-ai/ui-library'; -import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import WorkflowLibraryButton from 'features/workflowLibrary/components/WorkflowLibraryButton'; import WorkflowLibraryMenu from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu'; import { memo } from 'react'; const TopRightPanel = () => { - const isWorkflowLibraryEnabled = useFeatureStatus('workflowLibrary').isFeatureEnabled; - return ( - {isWorkflowLibraryEnabled && } + ); diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts index 5f6859c623..f6ffa20f13 100644 --- a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts @@ -5,7 +5,7 @@ import { workflowLoaded } from 'features/nodes/store/actions'; import { isAnyNodeOrEdgeMutation, nodeEditorReset, nodesDeleted } from 'features/nodes/store/nodesSlice'; import type { WorkflowsState as WorkflowState } from 'features/nodes/store/types'; import type { FieldIdentifier } from 'features/nodes/types/field'; -import type { WorkflowV2 } from 'features/nodes/types/workflow'; +import type { WorkflowCategory, WorkflowV2 } from 'features/nodes/types/workflow'; import { cloneDeep, isEqual, uniqBy } from 'lodash-es'; export const blankWorkflow: Omit = { @@ -46,6 +46,11 @@ const workflowSlice = createSlice({ state.name = action.payload; state.isTouched = true; }, + workflowCategoryChanged: (state, action: PayloadAction) => { + if (action.payload) { + state.meta.category = action.payload; + } + }, workflowDescriptionChanged: (state, action: PayloadAction) => { state.description = action.payload; state.isTouched = true; @@ -102,6 +107,7 @@ export const { workflowExposedFieldAdded, workflowExposedFieldRemoved, workflowNameChanged, + workflowCategoryChanged, workflowDescriptionChanged, workflowTagsChanged, workflowAuthorChanged, diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog.tsx new file mode 100644 index 0000000000..74a8916475 --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog.tsx @@ -0,0 +1,102 @@ +import { + AlertDialog, + AlertDialogBody, + AlertDialogContent, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogOverlay, + Button, + Checkbox, + Flex, + FormControl, + FormLabel, + Input, +} from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; +import { useSaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/useSaveWorkflowAsDialog'; +import { t } from 'i18next'; +import type { ChangeEvent } from 'react'; +import { useCallback, useRef } from 'react'; + +import { $workflowCategories } from '../../../../app/store/nanostores/workflowCategories'; +import { useSaveWorkflowAs } from '../../hooks/useSaveWorkflowAs'; + +export const SaveWorkflowAsDialog = () => { + const { isOpen, onClose, workflowName, setWorkflowName, shouldSaveToProject, setShouldSaveToProject } = + useSaveWorkflowAsDialog(); + + const workflowCategories = useStore($workflowCategories); + + const { saveWorkflowAs } = useSaveWorkflowAs(); + + const cancelRef = useRef(null); + const inputRef = useRef(null); + + const onChange = useCallback( + (e: ChangeEvent) => { + setWorkflowName(e.target.value); + }, + [setWorkflowName] + ); + + const onChangeCheckbox = useCallback( + (e: ChangeEvent) => { + setShouldSaveToProject(e.target.checked); + }, + [setShouldSaveToProject] + ); + + const clearAndClose = useCallback(() => { + onClose(); + }, [onClose]); + + const onSave = useCallback(async () => { + const category = shouldSaveToProject ? 'project' : 'user'; + await saveWorkflowAs({ + name: workflowName, + category, + onSuccess: clearAndClose, + onError: clearAndClose, + }); + }, [workflowName, saveWorkflowAs, shouldSaveToProject, clearAndClose]); + + return ( + + + + + {t('workflows.saveWorkflowAs')} + + + + + {t('workflows.workflowName')} + + + {workflowCategories.includes('project') && ( + + {t('workflows.saveWorkflowToProject')} + + )} + + + + + + + + + + + + ); +}; diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowAsDialog/useSaveWorkflowAsDialog.ts b/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowAsDialog/useSaveWorkflowAsDialog.ts new file mode 100644 index 0000000000..78f3e3f537 --- /dev/null +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/SaveWorkflowAsDialog/useSaveWorkflowAsDialog.ts @@ -0,0 +1,52 @@ +import { useStore } from '@nanostores/react'; +import { createSelector } from '@reduxjs/toolkit'; +import { useAppSelector } from 'app/store/storeHooks'; +import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice'; +import { getWorkflowCopyName } from 'features/workflowLibrary/util/getWorkflowCopyName'; +import { atom } from 'nanostores'; +import { useCallback } from 'react'; + +const $isOpen = atom(false); +const $workflowName = atom(''); +const $shouldSaveToProject = atom(false); + +const selectNewWorkflowName = createSelector(selectWorkflowSlice, ({ name, id }): string => { + // If the workflow has no ID, it's a new workflow that has never been saved to the server. The dialog should use + // whatever the user has entered in the workflow name field. + if (!id) { + return name; + } + // Else, the workflow is already saved to the server. The dialog should use the workflow's name with " (copy)" + // appended to it. + if (name.length) { + return getWorkflowCopyName(name); + } + // Else, we have a workflow that has been saved to the server, but has no name. This should never happen, but if + // it does, we just return an empty string and let the dialog use the default name. + return ''; +}); + +export const useSaveWorkflowAsDialog = () => { + const newWorkflowName = useAppSelector(selectNewWorkflowName); + + const isOpen = useStore($isOpen); + const onOpen = useCallback(() => { + $workflowName.set(newWorkflowName); + $isOpen.set(true); + }, [newWorkflowName]); + const onClose = useCallback(() => { + $isOpen.set(false); + $workflowName.set(''); + $shouldSaveToProject.set(false); + }, []); + + const workflowName = useStore($workflowName); + const setWorkflowName = useCallback((workflowName: string) => $workflowName.set(workflowName), []); + + const shouldSaveToProject = useStore($shouldSaveToProject); + const setShouldSaveToProject = useCallback((shouldSaveToProject: boolean) => { + $shouldSaveToProject.set(shouldSaveToProject); + }, []); + + return { workflowName, setWorkflowName, shouldSaveToProject, setShouldSaveToProject, isOpen, onOpen, onClose }; +}; diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx index f128505dbe..33c3cee2bb 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryButton.tsx @@ -1,4 +1,4 @@ -import { Button, useDisclosure } from '@invoke-ai/ui-library'; +import { IconButton, useDisclosure } from '@invoke-ai/ui-library'; import { WorkflowLibraryModalContext } from 'features/workflowLibrary/context/WorkflowLibraryModalContext'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,9 +12,13 @@ const WorkflowLibraryButton = () => { return ( - + } + onClick={disclosure.onOpen} + pointerEvents="auto" + /> ); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem.tsx index 55add16267..bd8a909ace 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem.tsx @@ -1,53 +1,18 @@ -import { ConfirmationAlertDialog, FormControl, FormLabel, Input, MenuItem, useDisclosure } from '@invoke-ai/ui-library'; -import { useAppSelector } from 'app/store/storeHooks'; -import { useSaveWorkflowAs } from 'features/workflowLibrary/hooks/useSaveWorkflowAs'; -import { getWorkflowCopyName } from 'features/workflowLibrary/util/getWorkflowCopyName'; -import type { ChangeEvent } from 'react'; -import { memo, useCallback, useRef, useState } from 'react'; +import { MenuItem } from '@invoke-ai/ui-library'; +import { useSaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/useSaveWorkflowAsDialog'; +import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiCopyBold } from 'react-icons/pi'; -const SaveWorkflowAsButton = () => { - const currentName = useAppSelector((s) => s.workflow.name); +const SaveWorkflowAsMenuItem = () => { const { t } = useTranslation(); - const { saveWorkflowAs } = useSaveWorkflowAs(); - const [name, setName] = useState(getWorkflowCopyName(currentName)); - const { isOpen, onOpen, onClose } = useDisclosure(); - const inputRef = useRef(null); - - const onOpenCallback = useCallback(() => { - setName(getWorkflowCopyName(currentName)); - onOpen(); - inputRef.current?.focus(); - }, [currentName, onOpen]); - - const onSave = useCallback(async () => { - saveWorkflowAs({ name, onSuccess: onClose, onError: onClose }); - }, [name, onClose, saveWorkflowAs]); - - const onChange = useCallback((e: ChangeEvent) => { - setName(e.target.value); - }, []); + const { onOpen } = useSaveWorkflowAsDialog(); return ( - <> - } onClick={onOpenCallback}> - {t('workflows.saveWorkflowAs')} - - - - - {t('workflows.workflowName')} - - - - + } onClick={onOpen}> + {t('workflows.saveWorkflowAs')} + ); }; -export default memo(SaveWorkflowAsButton); +export default memo(SaveWorkflowAsMenuItem); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx index 1e2866cb77..3fc7cee257 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx @@ -1,17 +1,37 @@ import { MenuItem } from '@invoke-ai/ui-library'; -import { useSaveLibraryWorkflow } from 'features/workflowLibrary/hooks/useSaveWorkflow'; -import { memo } from 'react'; +import { useSaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/useSaveWorkflowAsDialog'; +import { isWorkflowWithID, useSaveLibraryWorkflow } from 'features/workflowLibrary/hooks/useSaveWorkflow'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiFloppyDiskBold } from 'react-icons/pi'; -const SaveLibraryWorkflowMenuItem = () => { +import { useAppSelector } from '../../../../app/store/storeHooks'; +import { $builtWorkflow } from '../../../nodes/hooks/useWorkflowWatcher'; + +const SaveWorkflowMenuItem = () => { const { t } = useTranslation(); const { saveWorkflow } = useSaveLibraryWorkflow(); + const { onOpen } = useSaveWorkflowAsDialog(); + const isTouched = useAppSelector((s) => s.workflow.isTouched); + + const handleClickSave = useCallback(async () => { + const builtWorkflow = $builtWorkflow.get(); + if (!builtWorkflow) { + return; + } + + if (isWorkflowWithID(builtWorkflow)) { + saveWorkflow(); + } else { + onOpen(); + } + }, [onOpen, saveWorkflow]); + return ( - } onClick={saveWorkflow}> + } onClick={handleClickSave}> {t('workflows.saveWorkflow')} ); }; -export default memo(SaveLibraryWorkflowMenuItem); +export default memo(SaveWorkflowMenuItem); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu.tsx index 158eb73edc..73d0249d3d 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu.tsx @@ -7,7 +7,6 @@ import { useDisclosure, useGlobalMenuClose, } from '@invoke-ai/ui-library'; -import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import DownloadWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/DownloadWorkflowMenuItem'; import NewWorkflowMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/NewWorkflowMenuItem'; import SaveWorkflowAsMenuItem from 'features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowAsMenuItem'; @@ -22,9 +21,6 @@ const WorkflowLibraryMenu = () => { const { t } = useTranslation(); const { isOpen, onOpen, onClose } = useDisclosure(); useGlobalMenuClose(onClose); - - const isWorkflowLibraryEnabled = useFeatureStatus('workflowLibrary').isFeatureEnabled; - return ( { pointerEvents="auto" /> - {isWorkflowLibraryEnabled && } - {isWorkflowLibraryEnabled && } - - + + + + + diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts index 15d0fc650e..78d7071c20 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflow.ts @@ -17,7 +17,8 @@ type UseSaveLibraryWorkflowReturn = { type UseSaveLibraryWorkflow = () => UseSaveLibraryWorkflowReturn; -const isWorkflowWithID = (workflow: WorkflowV2): workflow is O.Required => Boolean(workflow.id); +export const isWorkflowWithID = (workflow: WorkflowV2): workflow is O.Required => + Boolean(workflow.id); export const useSaveLibraryWorkflow: UseSaveLibraryWorkflow = () => { const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts index c24961b918..2bd7aabbe4 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts +++ b/invokeai/frontend/web/src/features/workflowLibrary/hooks/useSaveWorkflowAs.ts @@ -2,13 +2,21 @@ import type { ToastId } from '@invoke-ai/ui-library'; import { useToast } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; import { $builtWorkflow } from 'features/nodes/hooks/useWorkflowWatcher'; -import { workflowIDChanged, workflowNameChanged, workflowSaved } from 'features/nodes/store/workflowSlice'; +import { + workflowCategoryChanged, + workflowIDChanged, + workflowNameChanged, + workflowSaved, +} from 'features/nodes/store/workflowSlice'; import { useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useCreateWorkflowMutation, workflowsApi } from 'services/api/endpoints/workflows'; +import type { WorkflowCategory } from '../../nodes/types/workflow'; + type SaveWorkflowAsArg = { name: string; + category: WorkflowCategory; onSuccess?: () => void; onError?: () => void; }; @@ -28,7 +36,7 @@ export const useSaveWorkflowAs: UseSaveWorkflowAs = () => { const toast = useToast(); const toastRef = useRef(); const saveWorkflowAs = useCallback( - async ({ name: newName, onSuccess, onError }: SaveWorkflowAsArg) => { + async ({ name: newName, category, onSuccess, onError }: SaveWorkflowAsArg) => { const workflow = $builtWorkflow.get(); if (!workflow) { return; @@ -42,10 +50,14 @@ export const useSaveWorkflowAs: UseSaveWorkflowAs = () => { try { workflow.id = undefined; workflow.name = newName; + workflow.meta.category = category; + const data = await createWorkflow(workflow).unwrap(); dispatch(workflowIDChanged(data.workflow.id)); dispatch(workflowNameChanged(data.workflow.name)); + dispatch(workflowCategoryChanged(data.workflow.meta.category)); dispatch(workflowSaved()); + onSuccess && onSuccess(); toast.update(toastRef.current, { title: t('workflows.workflowSaved'), From 25291a2e016626ddf6a0a9af1e4600589eba7520 Mon Sep 17 00:00:00 2001 From: Mary Hipp Date: Wed, 31 Jan 2024 11:48:11 -0500 Subject: [PATCH 19/22] select first image if no selectedImageName --- .../listenerMiddleware/listeners/boardIdSelected.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts index c93381f0f2..ebcb96d116 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardIdSelected.ts @@ -36,10 +36,11 @@ export const addBoardIdSelectedListener = () => { const { data: boardImagesData } = imagesApi.endpoints.listImages.select(queryArgs)(getState()); if (boardImagesData && boardIdSelected.match(action) && action.payload.selectedImageName) { - const firstImage = imagesSelectors.selectAll(boardImagesData)[0]; const selectedImage = imagesSelectors.selectById(boardImagesData, action.payload.selectedImageName); - - dispatch(imageSelected(selectedImage || firstImage || null)); + dispatch(imageSelected(selectedImage || null)); + } else if (boardImagesData) { + const firstImage = imagesSelectors.selectAll(boardImagesData)[0]; + dispatch(imageSelected(firstImage || null)); } else { // board has no images - deselect dispatch(imageSelected(null)); From b24e8dd829bcbe8ffdf75b360dece8e9b7e572a0 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Wed, 31 Jan 2024 21:31:38 +1100 Subject: [PATCH 20/22] feat(stats): refactor InvocationStatsService to output stats as dataclasses This allows the stats to be written to disk as JSON and analyzed. - Add dataclasses to hold stats. - Move stats pretty-print logic to `__str__` of the new `InvocationStatsSummary` class. - Add `get_stats` and `dump_stats` methods to `InvocationStatsServiceBase`. - `InvocationStatsService` now throws if stats are requested for a session it doesn't know about. This avoids needing to do a lot of messy null checks. - Update `DefaultInvocationProcessor` to use the new stats methods and suppresses the new errors. --- .../invocation_processor_default.py | 34 +++-- .../invocation_stats/invocation_stats_base.py | 28 +++- .../invocation_stats_common.py | 135 +++++++++++++++--- .../invocation_stats_default.py | 89 +++++++++--- 4 files changed, 233 insertions(+), 53 deletions(-) diff --git a/invokeai/app/services/invocation_processor/invocation_processor_default.py b/invokeai/app/services/invocation_processor/invocation_processor_default.py index 1cfb0c1822..4951b51121 100644 --- a/invokeai/app/services/invocation_processor/invocation_processor_default.py +++ b/invokeai/app/services/invocation_processor/invocation_processor_default.py @@ -1,11 +1,15 @@ import time import traceback +from contextlib import suppress from threading import BoundedSemaphore, Event, Thread from typing import Optional import invokeai.backend.util.logging as logger from invokeai.app.invocations.baseinvocation import InvocationContext from invokeai.app.services.invocation_queue.invocation_queue_common import InvocationQueueItem +from invokeai.app.services.invocation_stats.invocation_stats_common import ( + GESStatsNotFoundError, +) from invokeai.app.util.profiler import Profiler from ..invoker import Invoker @@ -152,7 +156,8 @@ class DefaultInvocationProcessor(InvocationProcessorABC): pass except CanceledException: - self.__invoker.services.performance_statistics.reset_stats(graph_execution_state.id) + with suppress(GESStatsNotFoundError): + self.__invoker.services.performance_statistics.reset_stats(graph_execution_state.id) pass except Exception as e: @@ -177,7 +182,8 @@ class DefaultInvocationProcessor(InvocationProcessorABC): error_type=e.__class__.__name__, error=error, ) - self.__invoker.services.performance_statistics.reset_stats(graph_execution_state.id) + 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 @@ -209,15 +215,21 @@ class DefaultInvocationProcessor(InvocationProcessorABC): error=traceback.format_exc(), ) elif is_complete: - 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: - profiler.stop() + 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: pass # Log something? KeyboardInterrupt is probably not going to be seen by the processor diff --git a/invokeai/app/services/invocation_stats/invocation_stats_base.py b/invokeai/app/services/invocation_stats/invocation_stats_base.py index 6e5b6a9f69..22624a6579 100644 --- a/invokeai/app/services/invocation_stats/invocation_stats_base.py +++ b/invokeai/app/services/invocation_stats/invocation_stats_base.py @@ -30,8 +30,10 @@ writes to the system log is stored in InvocationServices.performance_statistics. from abc import ABC, abstractmethod from contextlib import AbstractContextManager +from pathlib import Path from invokeai.app.invocations.baseinvocation import BaseInvocation +from invokeai.app.services.invocation_stats.invocation_stats_common import InvocationStatsSummary class InvocationStatsServiceBase(ABC): @@ -61,8 +63,9 @@ class InvocationStatsServiceBase(ABC): @abstractmethod def reset_stats(self, graph_execution_state_id: str): """ - Reset all statistics for the indicated graph - :param graph_execution_state_id + Reset all statistics for the indicated graph. + :param graph_execution_state_id: The id of the session whose stats to reset. + :raises GESStatsNotFoundError: if the graph isn't tracked in the stats. """ pass @@ -70,5 +73,26 @@ class InvocationStatsServiceBase(ABC): def log_stats(self, graph_execution_state_id: str): """ Write out the accumulated statistics to the log or somewhere else. + :param graph_execution_state_id: The id of the session whose stats to log. + :raises GESStatsNotFoundError: if the graph isn't tracked in the stats. + """ + pass + + @abstractmethod + def get_stats(self, graph_execution_state_id: str) -> InvocationStatsSummary: + """ + Gets the accumulated statistics for the indicated graph. + :param graph_execution_state_id: The id of the session whose stats to get. + :raises GESStatsNotFoundError: if the graph isn't tracked in the stats. + """ + pass + + @abstractmethod + def dump_stats(self, graph_execution_state_id: str, output_path: Path) -> None: + """ + Write out the accumulated statistics to the indicated path as JSON. + :param graph_execution_state_id: The id of the session whose stats to dump. + :param output_path: The file to write the stats to. + :raises GESStatsNotFoundError: if the graph isn't tracked in the stats. """ pass diff --git a/invokeai/app/services/invocation_stats/invocation_stats_common.py b/invokeai/app/services/invocation_stats/invocation_stats_common.py index 543edc076a..f4c906a58f 100644 --- a/invokeai/app/services/invocation_stats/invocation_stats_common.py +++ b/invokeai/app/services/invocation_stats/invocation_stats_common.py @@ -1,5 +1,91 @@ from collections import defaultdict -from dataclasses import dataclass +from dataclasses import asdict, dataclass +from typing import Any, Optional + + +class GESStatsNotFoundError(Exception): + """Raised when execution stats are not found for a given Graph Execution State.""" + + +@dataclass +class NodeExecutionStatsSummary: + """The stats for a specific type of node.""" + + node_type: str + num_calls: int + time_used_seconds: float + peak_vram_gb: float + + +@dataclass +class ModelCacheStatsSummary: + """The stats for the model cache.""" + + high_water_mark_gb: float + cache_size_gb: float + total_usage_gb: float + cache_hits: int + cache_misses: int + models_cached: int + models_cleared: int + + +@dataclass +class GraphExecutionStatsSummary: + """The stats for the graph execution state.""" + + graph_execution_state_id: str + execution_time_seconds: float + # `wall_time_seconds`, `ram_usage_gb` and `ram_change_gb` are derived from the node execution stats. + # In some situations, there are no node stats, so these values are optional. + wall_time_seconds: Optional[float] + ram_usage_gb: Optional[float] + ram_change_gb: Optional[float] + + +@dataclass +class InvocationStatsSummary: + """ + The accumulated stats for a graph execution. + Its `__str__` method returns a human-readable stats summary. + """ + + vram_usage_gb: Optional[float] + graph_stats: GraphExecutionStatsSummary + model_cache_stats: ModelCacheStatsSummary + node_stats: list[NodeExecutionStatsSummary] + + def __str__(self) -> str: + _str = "" + _str = f"Graph stats: {self.graph_stats.graph_execution_state_id}\n" + _str += f"{'Node':>30} {'Calls':>7} {'Seconds':>9} {'VRAM Used':>10}\n" + + for summary in self.node_stats: + _str += f"{summary.node_type:>30} {summary.num_calls:>7} {summary.time_used_seconds:>8.3f}s {summary.peak_vram_gb:>9.3f}G\n" + + _str += f"TOTAL GRAPH EXECUTION TIME: {self.graph_stats.execution_time_seconds:7.3f}s\n" + + if self.graph_stats.wall_time_seconds is not None: + _str += f"TOTAL GRAPH WALL TIME: {self.graph_stats.wall_time_seconds:7.3f}s\n" + + if self.graph_stats.ram_usage_gb is not None and self.graph_stats.ram_change_gb is not None: + _str += f"RAM used by InvokeAI process: {self.graph_stats.ram_usage_gb:4.2f}G ({self.graph_stats.ram_change_gb:+5.3f}G)\n" + + _str += f"RAM used to load models: {self.model_cache_stats.total_usage_gb:4.2f}G\n" + if self.vram_usage_gb: + _str += f"VRAM in use: {self.vram_usage_gb:4.3f}G\n" + _str += "RAM cache statistics:\n" + _str += f" Model cache hits: {self.model_cache_stats.cache_hits}\n" + _str += f" Model cache misses: {self.model_cache_stats.cache_misses}\n" + _str += f" Models cached: {self.model_cache_stats.models_cached}\n" + _str += f" Models cleared from cache: {self.model_cache_stats.models_cleared}\n" + _str += f" Cache high water mark: {self.model_cache_stats.high_water_mark_gb:4.2f}/{self.model_cache_stats.cache_size_gb:4.2f}G\n" + + return _str + + def as_dict(self) -> dict[str, Any]: + """Returns the stats as a dictionary.""" + return asdict(self) @dataclass @@ -55,12 +141,33 @@ class GraphExecutionStats: return last_node - def get_pretty_log(self, graph_execution_state_id: str) -> str: - log = f"Graph stats: {graph_execution_state_id}\n" - log += f"{'Node':>30} {'Calls':>7}{'Seconds':>9} {'VRAM Used':>10}\n" + def get_graph_stats_summary(self, graph_execution_state_id: str) -> GraphExecutionStatsSummary: + """Get a summary of the graph stats.""" + first_node = self.get_first_node_stats() + last_node = self.get_last_node_stats() - # Log stats aggregated by node type. + wall_time_seconds: Optional[float] = None + ram_usage_gb: Optional[float] = None + ram_change_gb: Optional[float] = None + + if last_node and first_node: + wall_time_seconds = last_node.end_time - first_node.start_time + ram_usage_gb = last_node.end_ram_gb + ram_change_gb = last_node.end_ram_gb - first_node.start_ram_gb + + return GraphExecutionStatsSummary( + graph_execution_state_id=graph_execution_state_id, + execution_time_seconds=self.get_total_run_time(), + wall_time_seconds=wall_time_seconds, + ram_usage_gb=ram_usage_gb, + ram_change_gb=ram_change_gb, + ) + + def get_node_stats_summaries(self) -> list[NodeExecutionStatsSummary]: + """Get a summary of the node stats.""" + summaries: list[NodeExecutionStatsSummary] = [] node_stats_by_type: dict[str, list[NodeExecutionStats]] = defaultdict(list) + for node_stats in self._node_stats_list: node_stats_by_type[node_stats.invocation_type].append(node_stats) @@ -68,17 +175,9 @@ class GraphExecutionStats: num_calls = len(node_type_stats_list) time_used = sum([n.total_time() for n in node_type_stats_list]) peak_vram = max([n.peak_vram_gb for n in node_type_stats_list]) - log += f"{node_type:>30} {num_calls:>4} {time_used:7.3f}s {peak_vram:4.3f}G\n" + summary = NodeExecutionStatsSummary( + node_type=node_type, num_calls=num_calls, time_used_seconds=time_used, peak_vram_gb=peak_vram + ) + summaries.append(summary) - # Log stats for the entire graph. - log += f"TOTAL GRAPH EXECUTION TIME: {self.get_total_run_time():7.3f}s\n" - - first_node = self.get_first_node_stats() - last_node = self.get_last_node_stats() - if first_node is not None and last_node is not None: - total_wall_time = last_node.end_time - first_node.start_time - ram_change = last_node.end_ram_gb - first_node.start_ram_gb - log += f"TOTAL GRAPH WALL TIME: {total_wall_time:7.3f}s\n" - log += f"RAM used by InvokeAI process: {last_node.end_ram_gb:4.2f}G ({ram_change:+5.3f}G)\n" - - return log + return summaries diff --git a/invokeai/app/services/invocation_stats/invocation_stats_default.py b/invokeai/app/services/invocation_stats/invocation_stats_default.py index 93f396c2b9..a2652dabc3 100644 --- a/invokeai/app/services/invocation_stats/invocation_stats_default.py +++ b/invokeai/app/services/invocation_stats/invocation_stats_default.py @@ -1,5 +1,7 @@ +import json import time from contextlib import contextmanager +from pathlib import Path import psutil import torch @@ -10,7 +12,15 @@ from invokeai.app.services.invoker import Invoker from invokeai.backend.model_management.model_cache import CacheStats from .invocation_stats_base import InvocationStatsServiceBase -from .invocation_stats_common import GraphExecutionStats, NodeExecutionStats +from .invocation_stats_common import ( + GESStatsNotFoundError, + GraphExecutionStats, + GraphExecutionStatsSummary, + InvocationStatsSummary, + ModelCacheStatsSummary, + NodeExecutionStats, + NodeExecutionStatsSummary, +) # Size of 1GB in bytes. GB = 2**30 @@ -95,31 +105,66 @@ class InvocationStatsService(InvocationStatsServiceBase): del self._stats[graph_execution_state_id] del self._cache_stats[graph_execution_state_id] except KeyError as e: - logger.warning(f"Attempted to clear statistics for unknown graph {graph_execution_state_id}: {e}.") + msg = f"Attempted to clear statistics for unknown graph {graph_execution_state_id}: {e}." + logger.warning(msg) + raise GESStatsNotFoundError(msg) - def log_stats(self, graph_execution_state_id: str): + def get_stats(self, graph_execution_state_id: str) -> InvocationStatsSummary: + graph_stats_summary = self._get_graph_summary(graph_execution_state_id) + node_stats_summaries = self._get_node_summaries(graph_execution_state_id) + model_cache_stats_summary = self._get_model_cache_summary(graph_execution_state_id) + vram_usage_gb = torch.cuda.memory_allocated() / GB if torch.cuda.is_available() else None + + return InvocationStatsSummary( + graph_stats=graph_stats_summary, + model_cache_stats=model_cache_stats_summary, + node_stats=node_stats_summaries, + vram_usage_gb=vram_usage_gb, + ) + + def log_stats(self, graph_execution_state_id: str) -> None: + stats = self.get_stats(graph_execution_state_id) + logger.info(str(stats)) + + def dump_stats(self, graph_execution_state_id: str, output_path: Path) -> None: + stats = self.get_stats(graph_execution_state_id) + with open(output_path, "w") as f: + f.write(json.dumps(stats.as_dict(), indent=2)) + + def _get_model_cache_summary(self, graph_execution_state_id: str) -> ModelCacheStatsSummary: try: - graph_stats = self._stats[graph_execution_state_id] cache_stats = self._cache_stats[graph_execution_state_id] except KeyError as e: - logger.warning(f"Attempted to log statistics for unknown graph {graph_execution_state_id}: {e}.") - return + msg = f"Attempted to get model cache statistics for unknown graph {graph_execution_state_id}: {e}." + logger.warning(msg) + raise GESStatsNotFoundError(msg) - log = graph_stats.get_pretty_log(graph_execution_state_id) + return ModelCacheStatsSummary( + cache_hits=cache_stats.hits, + cache_misses=cache_stats.misses, + high_water_mark_gb=cache_stats.high_watermark / GB, + cache_size_gb=cache_stats.cache_size / GB, + total_usage_gb=sum(list(cache_stats.loaded_model_sizes.values())) / GB, + models_cached=cache_stats.in_cache, + models_cleared=cache_stats.cleared, + ) - hwm = cache_stats.high_watermark / GB - tot = cache_stats.cache_size / GB - loaded = sum(list(cache_stats.loaded_model_sizes.values())) / GB - log += f"RAM used to load models: {loaded:4.2f}G\n" - if torch.cuda.is_available(): - log += f"VRAM in use: {(torch.cuda.memory_allocated() / GB):4.3f}G\n" - log += "RAM cache statistics:\n" - log += f" Model cache hits: {cache_stats.hits}\n" - log += f" Model cache misses: {cache_stats.misses}\n" - log += f" Models cached: {cache_stats.in_cache}\n" - log += f" Models cleared from cache: {cache_stats.cleared}\n" - log += f" Cache high water mark: {hwm:4.2f}/{tot:4.2f}G\n" - logger.info(log) + def _get_graph_summary(self, graph_execution_state_id: str) -> GraphExecutionStatsSummary: + 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.warning(msg) + raise GESStatsNotFoundError(msg) - del self._stats[graph_execution_state_id] - del self._cache_stats[graph_execution_state_id] + return graph_stats.get_graph_stats_summary(graph_execution_state_id) + + def _get_node_summaries(self, graph_execution_state_id: str) -> list[NodeExecutionStatsSummary]: + 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.warning(msg) + raise GESStatsNotFoundError(msg) + + return graph_stats.get_node_stats_summaries() From 9f6b9d4d23fff6cf64bb1a16a748e0eaaabd9ad1 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Feb 2024 08:18:57 +1100 Subject: [PATCH 21/22] fix(stats): preserve stack when raising GESStatsNotFoundError --- .../services/invocation_stats/invocation_stats_default.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/invokeai/app/services/invocation_stats/invocation_stats_default.py b/invokeai/app/services/invocation_stats/invocation_stats_default.py index a2652dabc3..b7b221f411 100644 --- a/invokeai/app/services/invocation_stats/invocation_stats_default.py +++ b/invokeai/app/services/invocation_stats/invocation_stats_default.py @@ -107,7 +107,7 @@ class InvocationStatsService(InvocationStatsServiceBase): except KeyError as e: msg = f"Attempted to clear statistics for unknown graph {graph_execution_state_id}: {e}." logger.warning(msg) - raise GESStatsNotFoundError(msg) + raise GESStatsNotFoundError(msg) from e def get_stats(self, graph_execution_state_id: str) -> InvocationStatsSummary: graph_stats_summary = self._get_graph_summary(graph_execution_state_id) @@ -137,7 +137,7 @@ class InvocationStatsService(InvocationStatsServiceBase): except KeyError as e: msg = f"Attempted to get model cache statistics for unknown graph {graph_execution_state_id}: {e}." logger.warning(msg) - raise GESStatsNotFoundError(msg) + raise GESStatsNotFoundError(msg) from e return ModelCacheStatsSummary( cache_hits=cache_stats.hits, @@ -155,7 +155,7 @@ class InvocationStatsService(InvocationStatsServiceBase): except KeyError as e: msg = f"Attempted to get graph statistics for unknown graph {graph_execution_state_id}: {e}." logger.warning(msg) - raise GESStatsNotFoundError(msg) + raise GESStatsNotFoundError(msg) from e return graph_stats.get_graph_stats_summary(graph_execution_state_id) @@ -165,6 +165,6 @@ class InvocationStatsService(InvocationStatsServiceBase): except KeyError as e: msg = f"Attempted to get node statistics for unknown graph {graph_execution_state_id}: {e}." logger.warning(msg) - raise GESStatsNotFoundError(msg) + raise GESStatsNotFoundError(msg) from e return graph_stats.get_node_stats_summaries() From 4410ecf62c4206d2ea4432e16bb7b01fbb32aeb9 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Thu, 1 Feb 2024 08:19:38 +1100 Subject: [PATCH 22/22] fix(stats): log errors at error level They were erroneously at warning before. --- .../services/invocation_stats/invocation_stats_default.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/invokeai/app/services/invocation_stats/invocation_stats_default.py b/invokeai/app/services/invocation_stats/invocation_stats_default.py index b7b221f411..7f3ef3023d 100644 --- a/invokeai/app/services/invocation_stats/invocation_stats_default.py +++ b/invokeai/app/services/invocation_stats/invocation_stats_default.py @@ -106,7 +106,7 @@ class InvocationStatsService(InvocationStatsServiceBase): 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.warning(msg) + logger.error(msg) raise GESStatsNotFoundError(msg) from e def get_stats(self, graph_execution_state_id: str) -> InvocationStatsSummary: @@ -136,7 +136,7 @@ class InvocationStatsService(InvocationStatsServiceBase): 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.warning(msg) + logger.error(msg) raise GESStatsNotFoundError(msg) from e return ModelCacheStatsSummary( @@ -154,7 +154,7 @@ class InvocationStatsService(InvocationStatsServiceBase): 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.warning(msg) + logger.error(msg) raise GESStatsNotFoundError(msg) from e return graph_stats.get_graph_stats_summary(graph_execution_state_id) @@ -164,7 +164,7 @@ class InvocationStatsService(InvocationStatsServiceBase): 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.warning(msg) + logger.error(msg) raise GESStatsNotFoundError(msg) from e return graph_stats.get_node_stats_summaries()