Skip to content

Commit 474d85e

Browse files
feat: add bulk reidentify action for models (invoke-ai#8951) (invoke-ai#8952)
* feat: add bulk reidentify action for models (invoke-ai#8951) Add a "Reidentify Models" bulk action to the model manager, allowing users to re-probe multiple models at once instead of one by one. - Backend: POST /api/v2/models/i/bulk_reidentify endpoint with partial failure handling (returns succeeded/failed lists) - Frontend: bulk reidentify mutation, confirmation modal with warning about custom settings reset, toast notifications for all outcomes - i18n: new translation keys for bulk reidentify UI strings * fix typgen * Fix bulk reidentify failing for models without trigger_phrases The bulk reidentify endpoint was directly assigning trigger_phrases without checking if the config type supports it, causing an AttributeError for ControlNet models. Added the same hasattr guard used by the individual reidentify endpoint. Also restored the missing path preservation that the individual endpoint has.
1 parent ed268b1 commit 474d85e

7 files changed

Lines changed: 343 additions & 2 deletions

File tree

invokeai/app/api/routers/model_manager.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,19 @@ class BulkDeleteModelsResponse(BaseModel):
516516
failed: List[dict] = Field(description="List of failed deletions with error messages")
517517

518518

519+
class BulkReidentifyModelsRequest(BaseModel):
520+
"""Request body for bulk model reidentification."""
521+
522+
keys: List[str] = Field(description="List of model keys to reidentify")
523+
524+
525+
class BulkReidentifyModelsResponse(BaseModel):
526+
"""Response body for bulk model reidentification."""
527+
528+
succeeded: List[str] = Field(description="List of successfully reidentified model keys")
529+
failed: List[dict] = Field(description="List of failed reidentifications with error messages")
530+
531+
519532
@model_manager_router.post(
520533
"/i/bulk_delete",
521534
operation_id="bulk_delete_models",
@@ -557,6 +570,67 @@ async def bulk_delete_models(
557570
return BulkDeleteModelsResponse(deleted=deleted, failed=failed)
558571

559572

573+
@model_manager_router.post(
574+
"/i/bulk_reidentify",
575+
operation_id="bulk_reidentify_models",
576+
responses={
577+
200: {"description": "Models reidentified (possibly with some failures)"},
578+
},
579+
status_code=200,
580+
)
581+
async def bulk_reidentify_models(
582+
current_admin: AdminUserOrDefault,
583+
request: BulkReidentifyModelsRequest = Body(description="List of model keys to reidentify"),
584+
) -> BulkReidentifyModelsResponse:
585+
"""
586+
Reidentify multiple models by re-probing their weights files.
587+
588+
Returns a list of successfully reidentified keys and failed reidentifications with error messages.
589+
"""
590+
logger = ApiDependencies.invoker.services.logger
591+
store = ApiDependencies.invoker.services.model_manager.store
592+
models_path = ApiDependencies.invoker.services.configuration.models_path
593+
594+
succeeded = []
595+
failed = []
596+
597+
for key in request.keys:
598+
try:
599+
config = store.get_model(key)
600+
if pathlib.Path(config.path).is_relative_to(models_path):
601+
model_path = pathlib.Path(config.path)
602+
else:
603+
model_path = models_path / config.path
604+
mod = ModelOnDisk(model_path)
605+
result = ModelConfigFactory.from_model_on_disk(mod)
606+
if result.config is None:
607+
raise InvalidModelException("Unable to identify model format")
608+
609+
# Retain user-editable fields from the original config
610+
result.config.path = config.path
611+
result.config.key = config.key
612+
result.config.name = config.name
613+
result.config.description = config.description
614+
result.config.cover_image = config.cover_image
615+
if hasattr(config, "trigger_phrases") and hasattr(result.config, "trigger_phrases"):
616+
result.config.trigger_phrases = config.trigger_phrases
617+
result.config.source = config.source
618+
result.config.source_type = config.source_type
619+
620+
store.replace_model(config.key, result.config)
621+
succeeded.append(key)
622+
logger.info(f"Reidentified model: {key}")
623+
except UnknownModelException as e:
624+
logger.error(f"Failed to reidentify model {key}: {str(e)}")
625+
failed.append({"key": key, "error": str(e)})
626+
except Exception as e:
627+
logger.error(f"Failed to reidentify model {key}: {str(e)}")
628+
failed.append({"key": key, "error": str(e)})
629+
630+
logger.info(f"Bulk reidentify completed: {len(succeeded)} succeeded, {len(failed)} failed")
631+
return BulkReidentifyModelsResponse(succeeded=succeeded, failed=failed)
632+
633+
560634
@model_manager_router.delete(
561635
"/i/{key}/image",
562636
operation_id="delete_model_image",

invokeai/frontend/web/public/locales/en.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,6 +1012,15 @@
10121012
"reidentifySuccess": "Model reidentified successfully",
10131013
"reidentifyUnknown": "Unable to identify model",
10141014
"reidentifyError": "Error reidentifying model",
1015+
"reidentifyModels": "Reidentify Models",
1016+
"reidentifyModelsConfirm": "Are you sure you want to reidentify {{count}} model(s)? This will re-probe their weights files to determine the correct format and settings.",
1017+
"reidentifyWarning": "This will reset any custom settings you may have applied to these models.",
1018+
"modelsReidentified": "Successfully reidentified {{count}} model(s)",
1019+
"modelsReidentifyFailed": "Failed to reidentify models",
1020+
"someModelsFailedToReidentify": "{{count}} model(s) could not be reidentified",
1021+
"modelsReidentifiedPartial": "Partially completed",
1022+
"someModelsReidentified": "{{succeeded}} reidentified, {{failed}} failed",
1023+
"modelsReidentifyError": "Error reidentifying models",
10151024
"updatePath": "Update Path",
10161025
"updatePathTooltip": "Update the file path for this model if you have moved the model files to a new location.",
10171026
"updatePathDescription": "Enter the new path to the model file or directory. Use this if you have manually moved the model files on disk.",
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import {
2+
AlertDialog,
3+
AlertDialogBody,
4+
AlertDialogContent,
5+
AlertDialogFooter,
6+
AlertDialogHeader,
7+
AlertDialogOverlay,
8+
Button,
9+
Flex,
10+
Text,
11+
} from '@invoke-ai/ui-library';
12+
import { memo, useRef } from 'react';
13+
import { useTranslation } from 'react-i18next';
14+
15+
type BulkReidentifyModelsModalProps = {
16+
isOpen: boolean;
17+
onClose: () => void;
18+
onConfirm: () => void;
19+
modelCount: number;
20+
isReidentifying?: boolean;
21+
};
22+
23+
export const BulkReidentifyModelsModal = memo(
24+
({ isOpen, onClose, onConfirm, modelCount, isReidentifying = false }: BulkReidentifyModelsModalProps) => {
25+
const { t } = useTranslation();
26+
const cancelRef = useRef<HTMLButtonElement>(null);
27+
28+
return (
29+
<AlertDialog isOpen={isOpen} onClose={onClose} leastDestructiveRef={cancelRef} isCentered>
30+
<AlertDialogOverlay>
31+
<AlertDialogContent>
32+
<AlertDialogHeader fontSize="lg" fontWeight="bold">
33+
{t('modelManager.reidentifyModels', {
34+
count: modelCount,
35+
defaultValue: 'Reidentify Models',
36+
})}
37+
</AlertDialogHeader>
38+
39+
<AlertDialogBody>
40+
<Flex flexDir="column" gap={3}>
41+
<Text>
42+
{t('modelManager.reidentifyModelsConfirm', {
43+
count: modelCount,
44+
defaultValue: `Are you sure you want to reidentify ${modelCount} model(s)? This will re-probe their weights files to determine the correct format and settings.`,
45+
})}
46+
</Text>
47+
<Text fontWeight="semibold" color="warning.400">
48+
{t('modelManager.reidentifyWarning', {
49+
defaultValue: 'This will reset any custom settings you may have applied to these models.',
50+
})}
51+
</Text>
52+
</Flex>
53+
</AlertDialogBody>
54+
55+
<AlertDialogFooter>
56+
<Button ref={cancelRef} onClick={onClose} isDisabled={isReidentifying}>
57+
{t('common.cancel')}
58+
</Button>
59+
<Button colorScheme="warning" onClick={onConfirm} ml={3} isLoading={isReidentifying}>
60+
{t('modelManager.reidentify')}
61+
</Button>
62+
</AlertDialogFooter>
63+
</AlertDialogContent>
64+
</AlertDialogOverlay>
65+
</AlertDialog>
66+
);
67+
}
68+
);
69+
70+
BulkReidentifyModelsModal.displayName = 'BulkReidentifyModelsModal';

invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,22 @@ import { serializeError } from 'serialize-error';
1818
import {
1919
modelConfigsAdapterSelectors,
2020
useBulkDeleteModelsMutation,
21+
useBulkReidentifyModelsMutation,
2122
useGetMissingModelsQuery,
2223
useGetModelConfigsQuery,
2324
} from 'services/api/endpoints/models';
2425
import type { AnyModelConfig } from 'services/api/types';
2526

2627
import { BulkDeleteModelsModal } from './BulkDeleteModelsModal';
28+
import { BulkReidentifyModelsModal } from './BulkReidentifyModelsModal';
2729
import { FetchingModelsLoader } from './FetchingModelsLoader';
2830
import { MissingModelsProvider } from './MissingModelsContext';
2931
import { ModelListWrapper } from './ModelListWrapper';
3032

3133
const log = logger('models');
3234

3335
export const [useBulkDeleteModal] = buildUseDisclosure(false);
36+
export const [useBulkReidentifyModal] = buildUseDisclosure(false);
3437

3538
const ModelList = () => {
3639
const dispatch = useAppDispatch();
@@ -40,11 +43,14 @@ const ModelList = () => {
4043
const { t } = useTranslation();
4144
const toast = useToast();
4245
const { isOpen, close } = useBulkDeleteModal();
46+
const { isOpen: isReidentifyOpen, close: closeReidentify } = useBulkReidentifyModal();
4347
const [isDeleting, setIsDeleting] = useState(false);
48+
const [isReidentifying, setIsReidentifying] = useState(false);
4449

4550
const { data: allModelsData, isLoading: isLoadingAll } = useGetModelConfigsQuery();
4651
const { data: missingModelsData, isLoading: isLoadingMissing } = useGetMissingModelsQuery();
4752
const [bulkDeleteModels] = useBulkDeleteModelsMutation();
53+
const [bulkReidentifyModels] = useBulkReidentifyModelsMutation();
4854

4955
const data = filteredModelType === 'missing' ? missingModelsData : allModelsData;
5056
const isLoading = filteredModelType === 'missing' ? isLoadingMissing : isLoadingAll;
@@ -148,6 +154,67 @@ const ModelList = () => {
148154
}
149155
}, [bulkDeleteModels, selectedModelKeys, dispatch, close, toast, t]);
150156

157+
const handleConfirmBulkReidentify = useCallback(async () => {
158+
setIsReidentifying(true);
159+
try {
160+
const result = await bulkReidentifyModels({ keys: selectedModelKeys }).unwrap();
161+
162+
// Clear selection and close modal
163+
dispatch(clearModelSelection());
164+
dispatch(setSelectedModelKey(null));
165+
closeReidentify();
166+
167+
if (result.failed.length === 0) {
168+
toast({
169+
id: 'BULK_REIDENTIFY_SUCCESS',
170+
title: t('modelManager.modelsReidentified', {
171+
count: result.succeeded.length,
172+
defaultValue: `Successfully reidentified ${result.succeeded.length} model(s)`,
173+
}),
174+
status: 'success',
175+
});
176+
} else if (result.succeeded.length === 0) {
177+
toast({
178+
id: 'BULK_REIDENTIFY_FAILED',
179+
title: t('modelManager.modelsReidentifyFailed', {
180+
defaultValue: 'Failed to reidentify models',
181+
}),
182+
description: t('modelManager.someModelsFailedToReidentify', {
183+
count: result.failed.length,
184+
defaultValue: `${result.failed.length} model(s) could not be reidentified`,
185+
}),
186+
status: 'error',
187+
});
188+
} else {
189+
toast({
190+
id: 'BULK_REIDENTIFY_PARTIAL',
191+
title: t('modelManager.modelsReidentifiedPartial', {
192+
defaultValue: 'Partially completed',
193+
}),
194+
description: t('modelManager.someModelsReidentified', {
195+
succeeded: result.succeeded.length,
196+
failed: result.failed.length,
197+
defaultValue: `${result.succeeded.length} reidentified, ${result.failed.length} failed`,
198+
}),
199+
status: 'warning',
200+
});
201+
}
202+
203+
log.info(`Bulk reidentify completed: ${result.succeeded.length} succeeded, ${result.failed.length} failed`);
204+
} catch (err) {
205+
log.error({ error: serializeError(err as Error) }, 'Bulk reidentify error');
206+
toast({
207+
id: 'BULK_REIDENTIFY_ERROR',
208+
title: t('modelManager.modelsReidentifyError', {
209+
defaultValue: 'Error reidentifying models',
210+
}),
211+
status: 'error',
212+
});
213+
} finally {
214+
setIsReidentifying(false);
215+
}
216+
}, [bulkReidentifyModels, selectedModelKeys, dispatch, closeReidentify, toast, t]);
217+
151218
return (
152219
<MissingModelsProvider>
153220
<Flex flexDirection="column" w="full" h="full">
@@ -173,6 +240,13 @@ const ModelList = () => {
173240
modelCount={selectedModelKeys.length}
174241
isDeleting={isDeleting}
175242
/>
243+
<BulkReidentifyModelsModal
244+
isOpen={isReidentifyOpen}
245+
onClose={closeReidentify}
246+
onConfirm={handleConfirmBulkReidentify}
247+
modelCount={selectedModelKeys.length}
248+
isReidentifying={isReidentifying}
249+
/>
176250
</MissingModelsProvider>
177251
);
178252
};

invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListBulkActions.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ import {
1111
} from 'features/modelManagerV2/store/modelManagerV2Slice';
1212
import { t } from 'i18next';
1313
import { memo, useCallback, useMemo } from 'react';
14-
import { PiCaretDownBold, PiTrashSimpleBold } from 'react-icons/pi';
14+
import { PiCaretDownBold, PiSparkleFill, PiTrashSimpleBold } from 'react-icons/pi';
1515
import {
1616
modelConfigsAdapterSelectors,
1717
useGetMissingModelsQuery,
1818
useGetModelConfigsQuery,
1919
} from 'services/api/endpoints/models';
2020
import type { AnyModelConfig } from 'services/api/types';
2121

22-
import { useBulkDeleteModal } from './ModelList';
22+
import { useBulkDeleteModal, useBulkReidentifyModal } from './ModelList';
2323

2424
const ModelListBulkActionsSx: SystemStyleObject = {
2525
alignItems: 'center',
@@ -40,11 +40,16 @@ export const ModelListBulkActions = memo(({ sx }: ModelListBulkActionsProps) =>
4040
const { data: allModelsData } = useGetModelConfigsQuery();
4141
const { data: missingModelsData } = useGetMissingModelsQuery();
4242
const bulkDeleteModal = useBulkDeleteModal();
43+
const bulkReidentifyModal = useBulkReidentifyModal();
4344

4445
const handleBulkDelete = useCallback(() => {
4546
bulkDeleteModal.open();
4647
}, [bulkDeleteModal]);
4748

49+
const handleBulkReidentify = useCallback(() => {
50+
bulkReidentifyModal.open();
51+
}, [bulkReidentifyModal]);
52+
4853
// Calculate displayed (filtered) model keys
4954
const displayedModelKeys = useMemo(() => {
5055
// Use missing models data when the filter is 'missing'
@@ -125,6 +130,12 @@ export const ModelListBulkActions = memo(({ sx }: ModelListBulkActionsProps) =>
125130
{t('modelManager.actions')}
126131
</MenuButton>
127132
<MenuList>
133+
<MenuItem icon={<PiSparkleFill />} onClick={handleBulkReidentify}>
134+
{t('modelManager.reidentifyModels', {
135+
count: selectionCount,
136+
defaultValue: 'Reidentify Models',
137+
})}
138+
</MenuItem>
128139
<MenuItem icon={<PiTrashSimpleBold />} onClick={handleBulkDelete} color="error.300">
129140
{t('modelManager.deleteModels', { count: selectionCount })}
130141
</MenuItem>

invokeai/frontend/web/src/services/api/endpoints/models.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ type BulkDeleteModelsResponse = {
5252
failed: string[];
5353
};
5454

55+
type BulkReidentifyModelsArg = {
56+
keys: string[];
57+
};
58+
type BulkReidentifyModelsResponse = {
59+
succeeded: string[];
60+
failed: string[];
61+
};
62+
5563
type ConvertMainModelResponse =
5664
paths['/api/v2/models/convert/{key}']['put']['responses']['200']['content']['application/json'];
5765

@@ -431,6 +439,16 @@ export const modelsApi = api.injectEndpoints({
431439
}
432440
},
433441
}),
442+
bulkReidentifyModels: build.mutation<BulkReidentifyModelsResponse, BulkReidentifyModelsArg>({
443+
query: ({ keys }) => {
444+
return {
445+
url: buildModelsUrl('i/bulk_reidentify'),
446+
method: 'POST',
447+
body: { keys },
448+
};
449+
},
450+
invalidatesTags: [{ type: 'ModelConfig', id: LIST_TAG }],
451+
}),
434452
getOrphanedModels: build.query<GetOrphanedModelsResponse, void>({
435453
query: () => ({
436454
url: buildModelsUrl('sync/orphaned'),
@@ -475,6 +493,7 @@ export const {
475493
useResetHFTokenMutation,
476494
useEmptyModelCacheMutation,
477495
useReidentifyModelMutation,
496+
useBulkReidentifyModelsMutation,
478497
useGetOrphanedModelsQuery,
479498
useDeleteOrphanedModelsMutation,
480499
} = modelsApi;

0 commit comments

Comments
 (0)