Skip to content
Open
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
10 changes: 10 additions & 0 deletions docs/src/content/docs/concepts/models.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,14 @@ In this situation, you may need to provide some additional information to identi
Add `:v2` to the repo ID and use that when installing the model: `monster-labs/control_v1p_sd15_qrcode_monster:v2`
:::

## Exporting and Importing Model Settings

Each model in the Model Manager has **Export Settings** and **Import Settings** buttons in the model header. These let you save a model's configuration to a JSON file and apply it to another installation, which is useful for backing up your tweaks, sharing a curated configuration with other users, or restoring a model after a reinstall.

The exported file contains the model's `name`, `description`, `source URL`, the cover image (encoded as a base64 data URL), and any settings that apply to the model type — typically `default_settings`, `trigger_phrases`, and `cpu_only`. When importing, every field present in the file is applied to the selected model, overwriting its current values. Fields the target model type does not support are skipped and reported in a warning toast.

:::caution
Importing replaces the target model's metadata, settings, and thumbnail with whatever is in the file. Make sure you're importing into the correct model.
:::

[set up in the config file]: ../../configuration/invokeai-yaml
Original file line number Diff line number Diff line change
@@ -1,51 +1,33 @@
import { IconButton } from '@invoke-ai/ui-library';
import { toast } from 'features/toast/toast';
import { memo, useCallback, useMemo } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { PiDownloadSimpleBold } from 'react-icons/pi';
import type { AnyModelConfigWithExternal } from 'services/api/types';

import { buildExportData, fetchImageAsDataUrl, sanitizeFilename } from './modelSettingsIO';

type Props = {
modelConfig: AnyModelConfigWithExternal;
};

const buildExportData = (modelConfig: AnyModelConfigWithExternal): Record<string, unknown> => {
const data: Record<string, unknown> = {};

if (
'default_settings' in modelConfig &&
modelConfig.default_settings !== undefined &&
modelConfig.default_settings !== null
) {
data.default_settings = modelConfig.default_settings;
}

if (
'trigger_phrases' in modelConfig &&
modelConfig.trigger_phrases !== undefined &&
modelConfig.trigger_phrases !== null
) {
data.trigger_phrases = modelConfig.trigger_phrases;
}

if ('cpu_only' in modelConfig && modelConfig.cpu_only !== null) {
data.cpu_only = modelConfig.cpu_only;
}

return data;
};

const sanitizeFilename = (name: string): string => {
return name.replace(/[<>:"/\\|?*]/g, '_');
};

export const ModelSettingsExportButton = memo(({ modelConfig }: Props) => {
const { t } = useTranslation();

const hasExportableData = useMemo(() => Object.keys(buildExportData(modelConfig)).length > 0, [modelConfig]);

const handleExport = useCallback(() => {
const handleExport = useCallback(async () => {
const data = buildExportData(modelConfig);

if (
'cover_image' in modelConfig &&
typeof modelConfig.cover_image === 'string' &&
modelConfig.cover_image.length > 0
) {
const dataUrl = await fetchImageAsDataUrl(modelConfig.cover_image);
if (dataUrl) {
data.cover_image = dataUrl;
}
}

const json = JSON.stringify(data, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
Expand Down Expand Up @@ -73,7 +55,6 @@ export const ModelSettingsExportButton = memo(({ modelConfig }: Props) => {
aria-label={t('modelManager.exportSettings')}
tooltip={t('modelManager.exportSettings')}
onClick={handleExport}
isDisabled={!hasExportableData}
/>
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,10 @@ import type { ChangeEvent } from 'react';
import { memo, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { PiUploadSimpleBold } from 'react-icons/pi';
import { useUpdateModelMutation } from 'services/api/endpoints/models';
import { useUpdateModelImageMutation, useUpdateModelMutation } from 'services/api/endpoints/models';
import type { AnyModelConfigWithExternal } from 'services/api/types';

const validateImportData = (data: unknown): data is Record<string, unknown> => {
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
return false;
}

const obj = data as Record<string, unknown>;

if ('trigger_phrases' in obj && obj.trigger_phrases !== undefined) {
if (!Array.isArray(obj.trigger_phrases) || !obj.trigger_phrases.every((p) => typeof p === 'string')) {
return false;
}
}

if ('default_settings' in obj && obj.default_settings !== undefined) {
if (
typeof obj.default_settings !== 'object' ||
obj.default_settings === null ||
Array.isArray(obj.default_settings)
) {
return false;
}
}

if ('cpu_only' in obj && obj.cpu_only !== undefined) {
if (typeof obj.cpu_only !== 'boolean') {
return false;
}
}

return true;
};
import { dataUrlToFile, isImageDataUrl, validateImportData } from './modelSettingsIO';

type Props = {
modelConfig: AnyModelConfigWithExternal;
Expand All @@ -47,13 +17,21 @@ export const ModelSettingsImportButton = memo(({ modelConfig }: Props) => {
const { t } = useTranslation();
const fileInputRef = useRef<HTMLInputElement>(null);
const [updateModel] = useUpdateModelMutation();
const [updateModelImage] = useUpdateModelImageMutation();

const applySettings = useCallback(
async (data: Record<string, unknown>) => {
const body: Record<string, unknown> = {};
const skippedFields: string[] = [];

const importableFields = ['default_settings', 'trigger_phrases', 'cpu_only'] as const;
const importableFields = [
'name',
'description',
'source_url',
'default_settings',
'trigger_phrases',
'cpu_only',
] as const;

for (const field of importableFields) {
if (!(field in data) || data[field] === undefined || data[field] === null) {
Expand All @@ -66,7 +44,12 @@ export const ModelSettingsImportButton = memo(({ modelConfig }: Props) => {
}
}

if (Object.keys(body).length === 0) {
const coverImageDataUrl =
'cover_image' in data && typeof data.cover_image === 'string' && isImageDataUrl(data.cover_image)
? data.cover_image
: null;

if (Object.keys(body).length === 0 && !coverImageDataUrl) {
if (skippedFields.length > 0) {
toast({
id: 'SETTINGS_IMPORT_INCOMPATIBLE',
Expand All @@ -77,35 +60,62 @@ export const ModelSettingsImportButton = memo(({ modelConfig }: Props) => {
return;
}

await updateModel({
key: modelConfig.key,
body,
})
.unwrap()
.then(() => {
if (skippedFields.length > 0) {
toast({
id: 'SETTINGS_IMPORTED',
title: t('modelManager.settingsImportedPartial', { fields: skippedFields.join(', ') }),
status: 'warning',
});
} else {
toast({
id: 'SETTINGS_IMPORTED',
title: t('modelManager.settingsImported'),
status: 'success',
});
}
})
.catch((_error) => {
let appliedAnything = false;
if (Object.keys(body).length > 0) {
try {
await updateModel({
key: modelConfig.key,
body,
}).unwrap();
appliedAnything = true;
} catch {
toast({
id: 'SETTINGS_IMPORT_FAILED',
title: t('modelManager.settingsImportFailed'),
status: 'error',
});
return;
}
}

if (coverImageDataUrl) {
const imageFile = dataUrlToFile(coverImageDataUrl, `${modelConfig.key}.png`);
if (!imageFile) {
skippedFields.push('cover_image');
} else {
try {
await updateModelImage({ key: modelConfig.key, image: imageFile }).unwrap();
appliedAnything = true;
} catch {
skippedFields.push('cover_image');
}
}
}

if (!appliedAnything) {
toast({
id: 'SETTINGS_IMPORT_FAILED',
title: t('modelManager.settingsImportFailed'),
status: 'error',
});
return;
}

if (skippedFields.length > 0) {
toast({
id: 'SETTINGS_IMPORTED',
title: t('modelManager.settingsImportedPartial', { fields: skippedFields.join(', ') }),
status: 'warning',
});
} else {
toast({
id: 'SETTINGS_IMPORTED',
title: t('modelManager.settingsImported'),
status: 'success',
});
}
},
[modelConfig, updateModel, t]
[modelConfig, updateModel, updateModelImage, t]
);

const handleFileChange = useCallback(
Expand Down
Loading
Loading