Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# These are supported funding model platforms
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository

github: invoke-ai
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ Get started with contributing by reading our [contribution documentation][contri

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.

## Sponsors

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.

<!-- Sponsor logos can be added below, or automated with a GitHub Action
such as `JamesIves/github-sponsors-readme-action` to keep them in sync. -->

[![Sponsor Invoke](https://img.shields.io/badge/Sponsor-Invoke-ea4aaa?logo=githubsponsors&logoColor=white)][sponsor link]

## Thanks

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.
Expand All @@ -112,6 +121,7 @@ Original portions of the software are Copyright © 2024 by respective contributo
[github issues]: https://github.com/invoke-ai/InvokeAI/issues
[docs home]: https://invoke.ai
[installation docs]: https://invoke.ai/start-here/installation/
[sponsor link]: https://github.com/sponsors/invoke-ai
[#dev-chat]: https://discord.com/channels/1020123559063990373/1049495067846524939
[contributing docs]: https://invoke.ai/contributing/
[CI checks on main badge]: https://flat.badgen.net/github/checks/invoke-ai/InvokeAI/main?label=CI%20status%20on%20main&cache=900&icon=github
Expand Down
16 changes: 13 additions & 3 deletions invokeai/app/api/extract_metadata_from_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ def extract_metadata_from_image(
stringified_metadata: str | None = None

# Use the metadata override if provided, else attempt to extract it from the image file.
metadata_raw = invokeai_metadata_override or pil_image.info.get("invokeai_metadata", None)
metadata_raw = (
invokeai_metadata_override
if invokeai_metadata_override is not None
else pil_image.info.get("invokeai_metadata", None)
)

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

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

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

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

# The fallback value for graph is None.
stringified_graph: str | None = None
Expand Down
1 change: 1 addition & 0 deletions invokeai/app/invocations/z_image_denoise.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,7 @@ def _run_diffusion(self, context: InvocationContext) -> torch.Tensor:
transformer=transformer,
regional_attn_mask=regional_extension.regional_attn_mask,
img_seq_len=img_seq_len,
positive_cap_feats=pos_prompt_embeds,
)
)

Expand Down
296 changes: 151 additions & 145 deletions invokeai/backend/z_image/z_image_transformer_patch.py

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions invokeai/frontend/webv2/src/workbench/auth/capabilities.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, expect, it } from 'vitest';

import type { AuthSession } from './session';

import { getCapabilities } from './capabilities';

const createSession = (overrides: Partial<AuthSession>): AuthSession => ({
multiuserEnabled: false,
phase: 'ready',
sessionExpired: false,
setupRequired: false,
strictPasswordChecking: false,
user: null,
...overrides,
});

describe('getCapabilities', () => {
it('allows clearing intermediates in single-user mode', () => {
expect(getCapabilities(createSession({ multiuserEnabled: false })).canClearIntermediates).toBe(true);
});

it('allows clearing intermediates for multi-user admins only', () => {
expect(
getCapabilities(
createSession({
multiuserEnabled: true,
user: { is_admin: true } as AuthSession['user'],
})
).canClearIntermediates
).toBe(true);

expect(
getCapabilities(
createSession({
multiuserEnabled: true,
user: { is_admin: false } as AuthSession['user'],
})
).canClearIntermediates
).toBe(false);
});
});
2 changes: 2 additions & 0 deletions invokeai/frontend/webv2/src/workbench/auth/capabilities.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useAuthSession, type AuthSession } from './session';

export interface Capabilities {
canClearIntermediates: boolean;
canManageModels: boolean;
canManageNodes: boolean;
canManageUsers: boolean;
Expand All @@ -11,6 +12,7 @@ export const getCapabilities = (session: AuthSession): Capabilities => {
const isAdmin = isSingleUser || session.user?.is_admin === true;

return {
canClearIntermediates: isAdmin,
canManageModels: isAdmin,
canManageNodes: isAdmin,
canManageUsers: session.multiuserEnabled && session.user?.is_admin === true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';

const mocks = vi.hoisted(() => ({
apiFetchJson: vi.fn(),
}));

vi.mock('@workbench/backend/http', () => ({
apiFetchJson: mocks.apiFetchJson,
}));

describe('intermediates API helpers', () => {
beforeEach(() => {
mocks.apiFetchJson.mockReset();
});

it('reads the intermediate image count from the images endpoint', async () => {
mocks.apiFetchJson.mockResolvedValueOnce(7);
const { getIntermediatesCount } = await import('./intermediates');

await expect(getIntermediatesCount()).resolves.toBe(7);

expect(mocks.apiFetchJson).toHaveBeenCalledWith('/api/v1/images/intermediates');
});

it('clears intermediate images with DELETE and returns the cleared count', async () => {
mocks.apiFetchJson.mockResolvedValueOnce(3);
const { clearIntermediates } = await import('./intermediates');

await expect(clearIntermediates()).resolves.toBe(3);

expect(mocks.apiFetchJson).toHaveBeenCalledWith('/api/v1/images/intermediates', { method: 'DELETE' });
});
});

describe('clear intermediates UI state', () => {
it('requires permission, a loaded nonzero count, and no active queue work', async () => {
const { getClearIntermediatesState } = await import('./intermediates');

expect(
getClearIntermediatesState({ canClearIntermediates: false, hasActiveQueueWork: false, intermediatesCount: 4 })
).toEqual({ disabled: true, reason: 'Only administrators can clear intermediates.' });

expect(
getClearIntermediatesState({ canClearIntermediates: true, hasActiveQueueWork: true, intermediatesCount: 4 })
).toEqual({ disabled: true, reason: 'Wait for pending or running queue work to finish.' });

expect(
getClearIntermediatesState({ canClearIntermediates: true, hasActiveQueueWork: false, intermediatesCount: 0 })
).toEqual({ disabled: true, reason: 'There are no intermediates to clear.' });

expect(
getClearIntermediatesState({ canClearIntermediates: true, hasActiveQueueWork: false, intermediatesCount: null })
).toEqual({ disabled: true, reason: 'Loading intermediate count.' });

expect(
getClearIntermediatesState({ canClearIntermediates: true, hasActiveQueueWork: false, intermediatesCount: 4 })
).toEqual({ disabled: false });
});

it('describes the destructive clear action before confirmation', async () => {
const { getClearIntermediatesConfirmation } = await import('./intermediates');

expect(getClearIntermediatesConfirmation(5)).toEqual({
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.',
confirmLabel: 'Clear intermediates',
title: 'Clear intermediate images?',
});
});
});
54 changes: 54 additions & 0 deletions invokeai/frontend/webv2/src/workbench/images/intermediates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { apiFetchJson } from '@workbench/backend/http';

const INTERMEDIATES_URL = '/api/v1/images/intermediates';

export interface ClearIntermediatesStateInput {
canClearIntermediates: boolean;
hasActiveQueueWork: boolean;
intermediatesCount: number | null;
}

export interface ClearIntermediatesState {
disabled: boolean;
reason?: string;
}

export interface ClearIntermediatesConfirmation {
body: string;
confirmLabel: string;
title: string;
}

export const getIntermediatesCount = (): Promise<number> => apiFetchJson<number>(INTERMEDIATES_URL);

export const clearIntermediates = (): Promise<number> => apiFetchJson<number>(INTERMEDIATES_URL, { method: 'DELETE' });

export const getClearIntermediatesState = ({
canClearIntermediates,
hasActiveQueueWork,
intermediatesCount,
}: ClearIntermediatesStateInput): ClearIntermediatesState => {
if (!canClearIntermediates) {
return { disabled: true, reason: 'Only administrators can clear intermediates.' };
}

if (hasActiveQueueWork) {
return { disabled: true, reason: 'Wait for pending or running queue work to finish.' };
}

if (intermediatesCount === null) {
return { disabled: true, reason: 'Loading intermediate count.' };
}

if (intermediatesCount === 0) {
return { disabled: true, reason: 'There are no intermediates to clear.' };
}

return { disabled: false };
};

export const getClearIntermediatesConfirmation = (count: number): ClearIntermediatesConfirmation => ({
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.`,
confirmLabel: 'Clear intermediates',
title: 'Clear intermediate images?',
});
Loading
Loading