Skip to content

Commit 4339bc2

Browse files
authored
Merge pull request #3 from invoke-ai/feat/clear-intermediates
Feat/clear intermediates
2 parents a6b35ea + 2ce412d commit 4339bc2

14 files changed

Lines changed: 582 additions & 148 deletions

.github/FUNDING.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# These are supported funding model platforms
2+
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository
3+
4+
github: invoke-ai

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,15 @@ Get started with contributing by reading our [contribution documentation][contri
100100

101101
We hope you enjoy using Invoke as much as we enjoy creating it, and we hope you will elect to become part of our community.
102102

103+
## Sponsors
104+
105+
Invoke's open-source development is powered by our sponsors. If Invoke is valuable to you or your business, please consider [sponsoring us][sponsor link] — it directly funds maintenance, new features, and community support.
106+
107+
<!-- Sponsor logos can be added below, or automated with a GitHub Action
108+
such as `JamesIves/github-sponsors-readme-action` to keep them in sync. -->
109+
110+
[![Sponsor Invoke](https://img.shields.io/badge/Sponsor-Invoke-ea4aaa?logo=githubsponsors&logoColor=white)][sponsor link]
111+
103112
## Thanks
104113

105114
Invoke is a combined effort of [passionate and talented people from across the world][contributors]. We thank them for their time, hard work and effort.
@@ -112,6 +121,7 @@ Original portions of the software are Copyright © 2024 by respective contributo
112121
[github issues]: https://github.com/invoke-ai/InvokeAI/issues
113122
[docs home]: https://invoke.ai
114123
[installation docs]: https://invoke.ai/start-here/installation/
124+
[sponsor link]: https://github.com/sponsors/invoke-ai
115125
[#dev-chat]: https://discord.com/channels/1020123559063990373/1049495067846524939
116126
[contributing docs]: https://invoke.ai/contributing/
117127
[CI checks on main badge]: https://flat.badgen.net/github/checks/invoke-ai/InvokeAI/main?label=CI%20status%20on%20main&cache=900&icon=github

invokeai/app/api/extract_metadata_from_image.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,11 @@ def extract_metadata_from_image(
4848
stringified_metadata: str | None = None
4949

5050
# Use the metadata override if provided, else attempt to extract it from the image file.
51-
metadata_raw = invokeai_metadata_override or pil_image.info.get("invokeai_metadata", None)
51+
metadata_raw = (
52+
invokeai_metadata_override
53+
if invokeai_metadata_override is not None
54+
else pil_image.info.get("invokeai_metadata", None)
55+
)
5256

5357
# If the metadata is present in the image file, we will attempt to parse it as JSON. When we create images,
5458
# we always store metadata as a stringified JSON dict. So, we expect it to be a string here.
@@ -66,7 +70,11 @@ def extract_metadata_from_image(
6670

6771
# We expect the workflow, if embedded in the image, to be a JSON-stringified WorkflowWithoutID. We will store it
6872
# as a string.
69-
workflow_raw: str | None = invokeai_workflow_override or pil_image.info.get("invokeai_workflow", None)
73+
workflow_raw: str | None = (
74+
invokeai_workflow_override
75+
if invokeai_workflow_override is not None
76+
else pil_image.info.get("invokeai_workflow", None)
77+
)
7078

7179
# The fallback value for workflow is None.
7280
stringified_workflow: str | None = None
@@ -85,7 +93,9 @@ def extract_metadata_from_image(
8593

8694
# We expect the workflow, if embedded in the image, to be a JSON-stringified Graph. We will store it as a
8795
# string.
88-
graph_raw: str | None = invokeai_graph_override or pil_image.info.get("invokeai_graph", None)
96+
graph_raw: str | None = (
97+
invokeai_graph_override if invokeai_graph_override is not None else pil_image.info.get("invokeai_graph", None)
98+
)
8999

90100
# The fallback value for graph is None.
91101
stringified_graph: str | None = None

invokeai/app/invocations/z_image_denoise.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,7 @@ def _run_diffusion(self, context: InvocationContext) -> torch.Tensor:
558558
transformer=transformer,
559559
regional_attn_mask=regional_extension.regional_attn_mask,
560560
img_seq_len=img_seq_len,
561+
positive_cap_feats=pos_prompt_embeds,
561562
)
562563
)
563564

invokeai/backend/z_image/z_image_transformer_patch.py

Lines changed: 151 additions & 145 deletions
Large diffs are not rendered by default.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import type { AuthSession } from './session';
4+
5+
import { getCapabilities } from './capabilities';
6+
7+
const createSession = (overrides: Partial<AuthSession>): AuthSession => ({
8+
multiuserEnabled: false,
9+
phase: 'ready',
10+
sessionExpired: false,
11+
setupRequired: false,
12+
strictPasswordChecking: false,
13+
user: null,
14+
...overrides,
15+
});
16+
17+
describe('getCapabilities', () => {
18+
it('allows clearing intermediates in single-user mode', () => {
19+
expect(getCapabilities(createSession({ multiuserEnabled: false })).canClearIntermediates).toBe(true);
20+
});
21+
22+
it('allows clearing intermediates for multi-user admins only', () => {
23+
expect(
24+
getCapabilities(
25+
createSession({
26+
multiuserEnabled: true,
27+
user: { is_admin: true } as AuthSession['user'],
28+
})
29+
).canClearIntermediates
30+
).toBe(true);
31+
32+
expect(
33+
getCapabilities(
34+
createSession({
35+
multiuserEnabled: true,
36+
user: { is_admin: false } as AuthSession['user'],
37+
})
38+
).canClearIntermediates
39+
).toBe(false);
40+
});
41+
});

invokeai/frontend/webv2/src/workbench/auth/capabilities.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useAuthSession, type AuthSession } from './session';
22

33
export interface Capabilities {
4+
canClearIntermediates: boolean;
45
canManageModels: boolean;
56
canManageNodes: boolean;
67
canManageUsers: boolean;
@@ -11,6 +12,7 @@ export const getCapabilities = (session: AuthSession): Capabilities => {
1112
const isAdmin = isSingleUser || session.user?.is_admin === true;
1213

1314
return {
15+
canClearIntermediates: isAdmin,
1416
canManageModels: isAdmin,
1517
canManageNodes: isAdmin,
1618
canManageUsers: session.multiuserEnabled && session.user?.is_admin === true,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
3+
const mocks = vi.hoisted(() => ({
4+
apiFetchJson: vi.fn(),
5+
}));
6+
7+
vi.mock('@workbench/backend/http', () => ({
8+
apiFetchJson: mocks.apiFetchJson,
9+
}));
10+
11+
describe('intermediates API helpers', () => {
12+
beforeEach(() => {
13+
mocks.apiFetchJson.mockReset();
14+
});
15+
16+
it('reads the intermediate image count from the images endpoint', async () => {
17+
mocks.apiFetchJson.mockResolvedValueOnce(7);
18+
const { getIntermediatesCount } = await import('./intermediates');
19+
20+
await expect(getIntermediatesCount()).resolves.toBe(7);
21+
22+
expect(mocks.apiFetchJson).toHaveBeenCalledWith('/api/v1/images/intermediates');
23+
});
24+
25+
it('clears intermediate images with DELETE and returns the cleared count', async () => {
26+
mocks.apiFetchJson.mockResolvedValueOnce(3);
27+
const { clearIntermediates } = await import('./intermediates');
28+
29+
await expect(clearIntermediates()).resolves.toBe(3);
30+
31+
expect(mocks.apiFetchJson).toHaveBeenCalledWith('/api/v1/images/intermediates', { method: 'DELETE' });
32+
});
33+
});
34+
35+
describe('clear intermediates UI state', () => {
36+
it('requires permission, a loaded nonzero count, and no active queue work', async () => {
37+
const { getClearIntermediatesState } = await import('./intermediates');
38+
39+
expect(
40+
getClearIntermediatesState({ canClearIntermediates: false, hasActiveQueueWork: false, intermediatesCount: 4 })
41+
).toEqual({ disabled: true, reason: 'Only administrators can clear intermediates.' });
42+
43+
expect(
44+
getClearIntermediatesState({ canClearIntermediates: true, hasActiveQueueWork: true, intermediatesCount: 4 })
45+
).toEqual({ disabled: true, reason: 'Wait for pending or running queue work to finish.' });
46+
47+
expect(
48+
getClearIntermediatesState({ canClearIntermediates: true, hasActiveQueueWork: false, intermediatesCount: 0 })
49+
).toEqual({ disabled: true, reason: 'There are no intermediates to clear.' });
50+
51+
expect(
52+
getClearIntermediatesState({ canClearIntermediates: true, hasActiveQueueWork: false, intermediatesCount: null })
53+
).toEqual({ disabled: true, reason: 'Loading intermediate count.' });
54+
55+
expect(
56+
getClearIntermediatesState({ canClearIntermediates: true, hasActiveQueueWork: false, intermediatesCount: 4 })
57+
).toEqual({ disabled: false });
58+
});
59+
60+
it('describes the destructive clear action before confirmation', async () => {
61+
const { getClearIntermediatesConfirmation } = await import('./intermediates');
62+
63+
expect(getClearIntermediatesConfirmation(5)).toEqual({
64+
body: 'This permanently deletes 5 temporary intermediate images and clears canvas layers or staged images that may reference them. Final gallery images are not removed.',
65+
confirmLabel: 'Clear intermediates',
66+
title: 'Clear intermediate images?',
67+
});
68+
});
69+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { apiFetchJson } from '@workbench/backend/http';
2+
3+
const INTERMEDIATES_URL = '/api/v1/images/intermediates';
4+
5+
export interface ClearIntermediatesStateInput {
6+
canClearIntermediates: boolean;
7+
hasActiveQueueWork: boolean;
8+
intermediatesCount: number | null;
9+
}
10+
11+
export interface ClearIntermediatesState {
12+
disabled: boolean;
13+
reason?: string;
14+
}
15+
16+
export interface ClearIntermediatesConfirmation {
17+
body: string;
18+
confirmLabel: string;
19+
title: string;
20+
}
21+
22+
export const getIntermediatesCount = (): Promise<number> => apiFetchJson<number>(INTERMEDIATES_URL);
23+
24+
export const clearIntermediates = (): Promise<number> => apiFetchJson<number>(INTERMEDIATES_URL, { method: 'DELETE' });
25+
26+
export const getClearIntermediatesState = ({
27+
canClearIntermediates,
28+
hasActiveQueueWork,
29+
intermediatesCount,
30+
}: ClearIntermediatesStateInput): ClearIntermediatesState => {
31+
if (!canClearIntermediates) {
32+
return { disabled: true, reason: 'Only administrators can clear intermediates.' };
33+
}
34+
35+
if (hasActiveQueueWork) {
36+
return { disabled: true, reason: 'Wait for pending or running queue work to finish.' };
37+
}
38+
39+
if (intermediatesCount === null) {
40+
return { disabled: true, reason: 'Loading intermediate count.' };
41+
}
42+
43+
if (intermediatesCount === 0) {
44+
return { disabled: true, reason: 'There are no intermediates to clear.' };
45+
}
46+
47+
return { disabled: false };
48+
};
49+
50+
export const getClearIntermediatesConfirmation = (count: number): ClearIntermediatesConfirmation => ({
51+
body: `This permanently deletes ${count} temporary intermediate image${count === 1 ? '' : 's'} and clears canvas layers or staged images that may reference them. Final gallery images are not removed.`,
52+
confirmLabel: 'Clear intermediates',
53+
title: 'Clear intermediate images?',
54+
});

0 commit comments

Comments
 (0)