mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-20 10:07:54 -05:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c451f52ea3 | ||
|
|
8a2c78f2e1 | ||
|
|
bcc78bde9b | ||
|
|
054bb6fe0a | ||
|
|
4f4aa6d92e | ||
|
|
eac51ac6f5 | ||
|
|
9f349a7c0a | ||
|
|
918afa5b15 | ||
|
|
eb1113f95c | ||
|
|
4f4ba7b462 | ||
|
|
2298be0e6b | ||
|
|
63494dfca7 | ||
|
|
36a1d39454 | ||
|
|
a6f6d5c400 | ||
|
|
e85f221aca | ||
|
|
d4797e37dc | ||
|
|
3e7923d072 | ||
|
|
a85d69ce3d | ||
|
|
96db006c99 | ||
|
|
8ca57d03d8 | ||
|
|
6c404ce5f8 | ||
|
|
584e07182b | ||
|
|
f787e9acf6 | ||
|
|
5a24b89e54 |
@@ -26,13 +26,10 @@ from invokeai.app.services.style_preset_records.style_preset_records_common impo
|
||||
)
|
||||
|
||||
|
||||
class StylePresetUpdateFormData(BaseModel):
|
||||
class StylePresetFormData(BaseModel):
|
||||
name: str = Field(description="Preset name")
|
||||
positive_prompt: str = Field(description="Positive prompt")
|
||||
negative_prompt: str = Field(description="Negative prompt")
|
||||
|
||||
|
||||
class StylePresetCreateFormData(StylePresetUpdateFormData):
|
||||
type: PresetType = Field(description="Preset type")
|
||||
|
||||
|
||||
@@ -95,9 +92,10 @@ async def update_style_preset(
|
||||
|
||||
try:
|
||||
parsed_data = json.loads(data)
|
||||
validated_data = StylePresetUpdateFormData(**parsed_data)
|
||||
validated_data = StylePresetFormData(**parsed_data)
|
||||
|
||||
name = validated_data.name
|
||||
type = validated_data.type
|
||||
positive_prompt = validated_data.positive_prompt
|
||||
negative_prompt = validated_data.negative_prompt
|
||||
|
||||
@@ -105,7 +103,7 @@ async def update_style_preset(
|
||||
raise HTTPException(status_code=400, detail="Invalid preset data")
|
||||
|
||||
preset_data = PresetData(positive_prompt=positive_prompt, negative_prompt=negative_prompt)
|
||||
changes = StylePresetChanges(name=name, preset_data=preset_data)
|
||||
changes = StylePresetChanges(name=name, preset_data=preset_data, type=type)
|
||||
|
||||
style_preset_image = ApiDependencies.invoker.services.style_preset_image_files.get_url(style_preset_id)
|
||||
style_preset = ApiDependencies.invoker.services.style_preset_records.update(
|
||||
@@ -145,7 +143,7 @@ async def create_style_preset(
|
||||
|
||||
try:
|
||||
parsed_data = json.loads(data)
|
||||
validated_data = StylePresetCreateFormData(**parsed_data)
|
||||
validated_data = StylePresetFormData(**parsed_data)
|
||||
|
||||
name = validated_data.name
|
||||
type = validated_data.type
|
||||
|
||||
@@ -32,6 +32,7 @@ class PresetType(str, Enum, metaclass=MetaEnum):
|
||||
class StylePresetChanges(BaseModel, extra="forbid"):
|
||||
name: Optional[str] = Field(default=None, description="The style preset's new name.")
|
||||
preset_data: Optional[PresetData] = Field(default=None, description="The updated data for style preset.")
|
||||
type: Optional[PresetType] = Field(description="The updated type of the style preset")
|
||||
|
||||
|
||||
class StylePresetWithoutId(BaseModel):
|
||||
|
||||
@@ -1675,6 +1675,7 @@
|
||||
"layers_other": "Layers"
|
||||
},
|
||||
"upscaling": {
|
||||
"upscale": "Upscale",
|
||||
"creativity": "Creativity",
|
||||
"exceedsMaxSize": "Upscale settings exceed max size limit",
|
||||
"exceedsMaxSizeDetails": "Max upscale limit is {{maxUpscaleDimension}}x{{maxUpscaleDimension}} pixels. Please try a smaller image or decrease your scale selection.",
|
||||
@@ -1723,6 +1724,7 @@
|
||||
"positivePrompt": "Positive Prompt",
|
||||
"preview": "Preview",
|
||||
"private": "Private",
|
||||
"promptTemplateCleared": "Prompt Template Cleared",
|
||||
"searchByName": "Search by name",
|
||||
"shared": "Shared",
|
||||
"sharedTemplates": "Shared Templates",
|
||||
|
||||
@@ -929,7 +929,7 @@
|
||||
"missingInvocationTemplate": "Modello di invocazione mancante",
|
||||
"missingFieldTemplate": "Modello di campo mancante",
|
||||
"singleFieldType": "{{name}} (Singola)",
|
||||
"imageAccessError": "Impossibile trovare l'immagine {{image_name}}, ripristino delle impostazioni predefinite",
|
||||
"imageAccessError": "Impossibile trovare l'immagine {{image_name}}, ripristino ai valori predefiniti",
|
||||
"boardAccessError": "Impossibile trovare la bacheca {{board_id}}, ripristino ai valori predefiniti",
|
||||
"modelAccessError": "Impossibile trovare il modello {{key}}, ripristino ai valori predefiniti"
|
||||
},
|
||||
@@ -1782,7 +1782,13 @@
|
||||
"updatePromptTemplate": "Aggiorna il modello di prompt",
|
||||
"type": "Tipo",
|
||||
"promptTemplatesDesc2": "Utilizza la stringa segnaposto <Pre>{{placeholder}}</Pre> per specificare dove inserire il tuo prompt nel modello.",
|
||||
"importTemplates": "Importa modelli di prompt",
|
||||
"importTemplatesDesc": "Il formato deve essere un CSV con colonne 'name' e 'prompt' o 'positive_prompt' e 'negative_prompt' incluse, oppure un file JSON con chiavi 'name' e 'prompt' o 'positive_prompt' e 'negative_prompt"
|
||||
"importTemplates": "Importa modelli di prompt (CSV/JSON)",
|
||||
"exportDownloaded": "Esportazione completata",
|
||||
"exportFailed": "Impossibile generare e scaricare il file CSV",
|
||||
"exportPromptTemplates": "Esporta i miei modelli di prompt (CSV)",
|
||||
"positivePromptColumn": "'prompt' o 'positive_prompt'",
|
||||
"noTemplates": "Nessun modello",
|
||||
"acceptedColumnsKeys": "Colonne/chiavi accettate:",
|
||||
"templateActions": "Azioni modello"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +91,8 @@
|
||||
"enabled": "Включено",
|
||||
"disabled": "Отключено",
|
||||
"comparingDesc": "Сравнение двух изображений",
|
||||
"comparing": "Сравнение"
|
||||
"comparing": "Сравнение",
|
||||
"dontShowMeThese": "Не показывай мне это"
|
||||
},
|
||||
"gallery": {
|
||||
"galleryImageSize": "Размер изображений",
|
||||
@@ -153,7 +154,11 @@
|
||||
"showArchivedBoards": "Показать архивированные доски",
|
||||
"searchImages": "Поиск по метаданным",
|
||||
"displayBoardSearch": "Отобразить поиск досок",
|
||||
"displaySearch": "Отобразить поиск"
|
||||
"displaySearch": "Отобразить поиск",
|
||||
"exitBoardSearch": "Выйти из поиска досок",
|
||||
"go": "Перейти",
|
||||
"exitSearch": "Выйти из поиска",
|
||||
"jump": "Пыгнуть"
|
||||
},
|
||||
"hotkeys": {
|
||||
"keyboardShortcuts": "Горячие клавиши",
|
||||
@@ -376,6 +381,10 @@
|
||||
"toggleViewer": {
|
||||
"title": "Переключить просмотр изображений",
|
||||
"desc": "Переключение между средством просмотра изображений и рабочей областью для текущей вкладки."
|
||||
},
|
||||
"postProcess": {
|
||||
"desc": "Обработайте текущее изображение с помощью выбранной модели постобработки",
|
||||
"title": "Обработать изображение"
|
||||
}
|
||||
},
|
||||
"modelManager": {
|
||||
@@ -589,7 +598,10 @@
|
||||
"infillColorValue": "Цвет заливки",
|
||||
"globalSettings": "Глобальные настройки",
|
||||
"globalNegativePromptPlaceholder": "Глобальный негативный запрос",
|
||||
"globalPositivePromptPlaceholder": "Глобальный запрос"
|
||||
"globalPositivePromptPlaceholder": "Глобальный запрос",
|
||||
"postProcessing": "Постобработка (Shift + U)",
|
||||
"processImage": "Обработка изображения",
|
||||
"sendToUpscale": "Отправить на увеличение"
|
||||
},
|
||||
"settings": {
|
||||
"models": "Модели",
|
||||
@@ -623,7 +635,9 @@
|
||||
"intermediatesCleared_many": "Очищено {{count}} промежуточных",
|
||||
"clearIntermediatesDesc1": "Очистка промежуточных элементов приведет к сбросу состояния Canvas и ControlNet.",
|
||||
"intermediatesClearedFailed": "Проблема очистки промежуточных",
|
||||
"reloadingIn": "Перезагрузка через"
|
||||
"reloadingIn": "Перезагрузка через",
|
||||
"informationalPopoversDisabled": "Информационные всплывающие окна отключены",
|
||||
"informationalPopoversDisabledDesc": "Информационные всплывающие окна были отключены. Включите их в Настройках."
|
||||
},
|
||||
"toast": {
|
||||
"uploadFailed": "Загрузка не удалась",
|
||||
@@ -694,7 +708,9 @@
|
||||
"sessionRef": "Сессия: {{sessionId}}",
|
||||
"outOfMemoryError": "Ошибка нехватки памяти",
|
||||
"outOfMemoryErrorDesc": "Ваши текущие настройки генерации превышают возможности системы. Пожалуйста, измените настройки и повторите попытку.",
|
||||
"somethingWentWrong": "Что-то пошло не так"
|
||||
"somethingWentWrong": "Что-то пошло не так",
|
||||
"importFailed": "Импорт неудачен",
|
||||
"importSuccessful": "Импорт успешен"
|
||||
},
|
||||
"tooltip": {
|
||||
"feature": {
|
||||
@@ -1017,7 +1033,8 @@
|
||||
"composition": "Только композиция",
|
||||
"hed": "HED",
|
||||
"beginEndStepPercentShort": "Начало/конец %",
|
||||
"setControlImageDimensionsForce": "Скопируйте размер в Ш/В (игнорируйте модель)"
|
||||
"setControlImageDimensionsForce": "Скопируйте размер в Ш/В (игнорируйте модель)",
|
||||
"depthAnythingSmallV2": "Small V2"
|
||||
},
|
||||
"boards": {
|
||||
"autoAddBoard": "Авто добавление Доски",
|
||||
@@ -1042,7 +1059,7 @@
|
||||
"downloadBoard": "Скачать доску",
|
||||
"deleteBoard": "Удалить доску",
|
||||
"deleteBoardAndImages": "Удалить доску и изображения",
|
||||
"deletedBoardsCannotbeRestored": "Удаленные доски не подлежат восстановлению",
|
||||
"deletedBoardsCannotbeRestored": "Удаленные доски не могут быть восстановлены. Выбор «Удалить только доску» переведет изображения в состояние без категории.",
|
||||
"assetsWithCount_one": "{{count}} ассет",
|
||||
"assetsWithCount_few": "{{count}} ассета",
|
||||
"assetsWithCount_many": "{{count}} ассетов",
|
||||
@@ -1057,7 +1074,11 @@
|
||||
"boards": "Доски",
|
||||
"addPrivateBoard": "Добавить личную доску",
|
||||
"private": "Личные доски",
|
||||
"shared": "Общие доски"
|
||||
"shared": "Общие доски",
|
||||
"hideBoards": "Скрыть доски",
|
||||
"viewBoards": "Просмотреть доски",
|
||||
"noBoards": "Нет досок {{boardType}}",
|
||||
"deletedPrivateBoardsCannotbeRestored": "Удаленные доски не могут быть восстановлены. Выбор «Удалить только доску» переведет изображения в приватное состояние без категории для создателя изображения."
|
||||
},
|
||||
"dynamicPrompts": {
|
||||
"seedBehaviour": {
|
||||
@@ -1417,6 +1438,30 @@
|
||||
"paragraphs": [
|
||||
"Метод, с помощью которого применяется текущий IP-адаптер."
|
||||
]
|
||||
},
|
||||
"structure": {
|
||||
"paragraphs": [
|
||||
"Структура контролирует, насколько точно выходное изображение будет соответствовать макету оригинала. Низкая структура допускает значительные изменения, в то время как высокая структура строго сохраняет исходную композицию и макет."
|
||||
],
|
||||
"heading": "Структура"
|
||||
},
|
||||
"scale": {
|
||||
"paragraphs": [
|
||||
"Масштаб управляет размером выходного изображения и основывается на кратном разрешении входного изображения. Например, при увеличении в 2 раза изображения 1024x1024 на выходе получится 2048 x 2048."
|
||||
],
|
||||
"heading": "Масштаб"
|
||||
},
|
||||
"creativity": {
|
||||
"paragraphs": [
|
||||
"Креативность контролирует степень свободы, предоставляемой модели при добавлении деталей. При низкой креативности модель остается близкой к оригинальному изображению, в то время как высокая креативность позволяет вносить больше изменений. При использовании подсказки высокая креативность увеличивает влияние подсказки."
|
||||
],
|
||||
"heading": "Креативность"
|
||||
},
|
||||
"upscaleModel": {
|
||||
"heading": "Модель увеличения",
|
||||
"paragraphs": [
|
||||
"Модель увеличения масштаба масштабирует изображение до выходного размера перед добавлением деталей. Можно использовать любую поддерживаемую модель масштабирования, но некоторые из них специализированы для различных видов изображений, например фотографий или линейных рисунков."
|
||||
]
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
@@ -1693,7 +1738,78 @@
|
||||
"canvasTab": "$t(ui.tabs.canvas) $t(common.tab)",
|
||||
"queueTab": "$t(ui.tabs.queue) $t(common.tab)",
|
||||
"modelsTab": "$t(ui.tabs.models) $t(common.tab)",
|
||||
"queue": "Очередь"
|
||||
"queue": "Очередь",
|
||||
"upscaling": "Увеличение",
|
||||
"upscalingTab": "$t(ui.tabs.upscaling) $t(common.tab)"
|
||||
}
|
||||
},
|
||||
"upscaling": {
|
||||
"exceedsMaxSize": "Параметры масштабирования превышают максимальный размер",
|
||||
"exceedsMaxSizeDetails": "Максимальный предел масштабирования составляет {{maxUpscaleDimension}}x{{maxUpscaleDimension}} пикселей. Пожалуйста, попробуйте использовать меньшее изображение или уменьшите масштаб.",
|
||||
"structure": "Структура",
|
||||
"missingTileControlNetModel": "Не установлены подходящие модели ControlNet",
|
||||
"missingUpscaleInitialImage": "Отсутствует увеличиваемое изображение",
|
||||
"missingUpscaleModel": "Отсутствует увеличивающая модель",
|
||||
"creativity": "Креативность",
|
||||
"upscaleModel": "Модель увеличения",
|
||||
"scale": "Масштаб",
|
||||
"mainModelDesc": "Основная модель (архитектура SD1.5 или SDXL)",
|
||||
"upscaleModelDesc": "Модель увеличения (img2img)",
|
||||
"postProcessingModel": "Модель постобработки",
|
||||
"tileControlNetModelDesc": "Модель ControlNet для выбранной архитектуры основной модели",
|
||||
"missingModelsWarning": "Зайдите в <LinkComponent>Менеджер моделей</LinkComponent> чтоб установить необходимые модели:",
|
||||
"postProcessingMissingModelWarning": "Посетите <LinkComponent>Менеджер моделей</LinkComponent>, чтобы установить модель постобработки (img2img)."
|
||||
},
|
||||
"stylePresets": {
|
||||
"noMatchingTemplates": "Нет подходящих шаблонов",
|
||||
"promptTemplatesDesc1": "Шаблоны подсказок добавляют текст к подсказкам, которые вы пишете в окне подсказок.",
|
||||
"sharedTemplates": "Общие шаблоны",
|
||||
"templateDeleted": "Шаблон запроса удален",
|
||||
"toggleViewMode": "Переключить режим просмотра",
|
||||
"type": "Тип",
|
||||
"unableToDeleteTemplate": "Не получилось удалить шаблон запроса",
|
||||
"viewModeTooltip": "Вот как будет выглядеть ваш запрос с выбранным шаблоном. Чтобы его отредактировать, щелкните в любом месте текстового поля.",
|
||||
"viewList": "Просмотреть список шаблонов",
|
||||
"active": "Активно",
|
||||
"choosePromptTemplate": "Выберите шаблон запроса",
|
||||
"defaultTemplates": "Стандартные шаблоны",
|
||||
"deleteImage": "Удалить изображение",
|
||||
"deleteTemplate": "Удалить шаблон",
|
||||
"deleteTemplate2": "Вы уверены, что хотите удалить этот шаблон? Это нельзя отменить.",
|
||||
"editTemplate": "Редактировать шаблон",
|
||||
"exportPromptTemplates": "Экспорт моих шаблонов запроса (CSV)",
|
||||
"exportDownloaded": "Экспорт скачан",
|
||||
"exportFailed": "Невозможно сгенерировать и загрузить CSV",
|
||||
"flatten": "Объединить выбранный шаблон с текущим запросом",
|
||||
"acceptedColumnsKeys": "Принимаемые столбцы/ключи:",
|
||||
"positivePromptColumn": "'prompt' или 'positive_prompt'",
|
||||
"insertPlaceholder": "Вставить заполнитель",
|
||||
"name": "Имя",
|
||||
"negativePrompt": "Негативный запрос",
|
||||
"promptTemplatesDesc3": "Если вы не используете заполнитель, шаблон будет добавлен в конец запроса.",
|
||||
"positivePrompt": "Позитивный запрос",
|
||||
"preview": "Предпросмотр",
|
||||
"private": "Приватный",
|
||||
"templateActions": "Действия с шаблоном",
|
||||
"updatePromptTemplate": "Обновить шаблон запроса",
|
||||
"uploadImage": "Загрузить изображение",
|
||||
"useForTemplate": "Использовать для шаблона запроса",
|
||||
"clearTemplateSelection": "Очистить выбор шаблона",
|
||||
"copyTemplate": "Копировать шаблон",
|
||||
"createPromptTemplate": "Создать шаблон запроса",
|
||||
"importTemplates": "Импортировать шаблоны запроса (CSV/JSON)",
|
||||
"nameColumn": "'name'",
|
||||
"negativePromptColumn": "'negative_prompt'",
|
||||
"myTemplates": "Мои шаблоны",
|
||||
"noTemplates": "Нет шаблонов",
|
||||
"promptTemplatesDesc2": "Используйте строку-заполнитель <Pre>{{placeholder}}</Pre>, чтобы указать место, куда должен быть включен ваш запрос в шаблоне.",
|
||||
"searchByName": "Поиск по имени",
|
||||
"shared": "Общий"
|
||||
},
|
||||
"upsell": {
|
||||
"inviteTeammates": "Пригласите членов команды",
|
||||
"professional": "Профессионал",
|
||||
"professionalUpsell": "Доступно в профессиональной версии Invoke. Нажмите здесь или посетите invoke.com/pricing для получения более подробной информации.",
|
||||
"shareAccess": "Поделиться доступом"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt';
|
||||
import { getPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
|
||||
import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
|
||||
import { stylePresetsApi } from 'services/api/endpoints/stylePresets';
|
||||
import { utilitiesApi } from 'services/api/endpoints/utilities';
|
||||
import { socketConnected } from 'services/events/actions';
|
||||
|
||||
@@ -22,7 +23,10 @@ const matcher = isAnyOf(
|
||||
maxPromptsChanged,
|
||||
maxPromptsReset,
|
||||
socketConnected,
|
||||
activeStylePresetIdChanged
|
||||
activeStylePresetIdChanged,
|
||||
stylePresetsApi.endpoints.deleteStylePreset.matchFulfilled,
|
||||
stylePresetsApi.endpoints.updateStylePreset.matchFulfilled,
|
||||
stylePresetsApi.endpoints.listStylePresets.matchFulfilled
|
||||
);
|
||||
|
||||
export const addDynamicPromptsListener = (startAppListening: AppStartListening) => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { $authToken } from 'app/store/nanostores/authToken';
|
||||
*/
|
||||
|
||||
export const convertImageUrlToBlob = async (url: string) =>
|
||||
new Promise<Blob | null>((resolve) => {
|
||||
new Promise<Blob | null>((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas');
|
||||
@@ -17,17 +17,23 @@ export const convertImageUrlToBlob = async (url: string) =>
|
||||
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) {
|
||||
reject(new Error('Failed to get canvas context'));
|
||||
return;
|
||||
}
|
||||
context.drawImage(img, 0, 0);
|
||||
resolve(
|
||||
new Promise<Blob | null>((resolve) => {
|
||||
canvas.toBlob(function (blob) {
|
||||
resolve(blob);
|
||||
}, 'image/png');
|
||||
})
|
||||
);
|
||||
canvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
resolve(blob);
|
||||
} else {
|
||||
reject(new Error('Failed to convert image to blob'));
|
||||
}
|
||||
}, 'image/png');
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
reject(new Error('Image failed to load. The URL may be invalid or the object may not exist.'));
|
||||
};
|
||||
|
||||
img.crossOrigin = $authToken.get() ? 'use-credentials' : 'anonymous';
|
||||
img.src = url;
|
||||
});
|
||||
|
||||
@@ -66,7 +66,7 @@ export const Gallery = () => {
|
||||
<Flex flexDirection="column" alignItems="center" justifyContent="space-between" h="full" w="full" pt={1}>
|
||||
<Tabs index={galleryView === 'images' ? 0 : 1} variant="enclosed" display="flex" flexDir="column" w="full">
|
||||
<TabList gap={2} fontSize="sm" borderColor="base.800" alignItems="center" w="full">
|
||||
<Text fontSize="sm" fontWeight="semibold" noOfLines={1} px="2">
|
||||
<Text fontSize="sm" fontWeight="semibold" noOfLines={1} px="2" wordBreak="break-all">
|
||||
{boardName}
|
||||
</Text>
|
||||
<Spacer />
|
||||
|
||||
@@ -60,7 +60,6 @@ const ImageGalleryContent = () => {
|
||||
<GalleryHeader />
|
||||
<Flex alignItems="center" justifyContent="space-between" w="full">
|
||||
<Button
|
||||
w={112}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={handleToggleBoardPanel}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { handlers, parseAndRecallAllMetadata, parseAndRecallPrompts } from 'features/metadata/util/handlers';
|
||||
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
|
||||
import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
|
||||
export const useImageActions = (image_name?: string) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||
const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId);
|
||||
const { metadata, isLoading: isLoadingMetadata } = useDebouncedMetadata(image_name);
|
||||
const [hasMetadata, setHasMetadata] = useState(false);
|
||||
const [hasSeed, setHasSeed] = useState(false);
|
||||
@@ -46,14 +52,26 @@ export const useImageActions = (image_name?: string) => {
|
||||
parseMetadata();
|
||||
}, [metadata]);
|
||||
|
||||
const clearStylePreset = useCallback(() => {
|
||||
if (activeStylePresetId) {
|
||||
dispatch(activeStylePresetIdChanged(null));
|
||||
toast({
|
||||
status: 'info',
|
||||
title: t('stylePresets.promptTemplateCleared'),
|
||||
});
|
||||
}
|
||||
}, [dispatch, activeStylePresetId, t]);
|
||||
|
||||
const recallAll = useCallback(() => {
|
||||
parseAndRecallAllMetadata(metadata, activeTabName === 'generation');
|
||||
}, [activeTabName, metadata]);
|
||||
clearStylePreset();
|
||||
}, [activeTabName, metadata, clearStylePreset]);
|
||||
|
||||
const remix = useCallback(() => {
|
||||
// Recalls all metadata parameters except seed
|
||||
parseAndRecallAllMetadata(metadata, activeTabName === 'generation', ['seed']);
|
||||
}, [activeTabName, metadata]);
|
||||
clearStylePreset();
|
||||
}, [activeTabName, metadata, clearStylePreset]);
|
||||
|
||||
const recallSeed = useCallback(() => {
|
||||
handlers.seed.parse(metadata).then((seed) => {
|
||||
@@ -63,7 +81,8 @@ export const useImageActions = (image_name?: string) => {
|
||||
|
||||
const recallPrompts = useCallback(() => {
|
||||
parseAndRecallPrompts(metadata);
|
||||
}, [metadata]);
|
||||
clearStylePreset();
|
||||
}, [metadata, clearStylePreset]);
|
||||
|
||||
const createAsPreset = useCallback(async () => {
|
||||
if (image_name && metadata && imageDTO) {
|
||||
|
||||
@@ -48,7 +48,12 @@ export const UpscaleSettingsAccordion = memo(() => {
|
||||
});
|
||||
|
||||
return (
|
||||
<StandaloneAccordion label="Upscale" badges={badges} isOpen={isOpenAccordion} onToggle={onToggleAccordion}>
|
||||
<StandaloneAccordion
|
||||
label={t('upscaling.upscale')}
|
||||
badges={badges}
|
||||
isOpen={isOpenAccordion}
|
||||
onToggle={onToggleAccordion}
|
||||
>
|
||||
<Flex pt={4} px={4} w="full" h="full" flexDir="column" data-testid="upscale-settings-accordion">
|
||||
<Flex flexDir="column" gap={4}>
|
||||
<Flex gap={4}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Badge, Flex, IconButton, Text, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { Badge, Flex, IconButton, Spacer, Text, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { negativePromptChanged, positivePromptChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||
import { usePresetModifiedPrompts } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
|
||||
@@ -69,45 +69,40 @@ export const ActiveStylePreset = () => {
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Flex justifyContent="space-between" w="full" alignItems="center">
|
||||
<Flex gap={2} alignItems="center">
|
||||
<StylePresetImage imageWidth={25} presetImageUrl={activeStylePreset.image} />
|
||||
<Flex flexDir="column">
|
||||
<Badge colorScheme="invokeBlue" variant="subtle">
|
||||
{activeStylePreset.name}
|
||||
</Badge>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex gap={1}>
|
||||
<Tooltip label={t('stylePresets.toggleViewMode')}>
|
||||
<IconButton
|
||||
onClick={handleToggleViewMode}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-label={t('stylePresets.toggleViewMode')}
|
||||
colorScheme={viewMode ? 'invokeBlue' : 'base'}
|
||||
icon={<PiEyeBold />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('stylePresets.flatten')}>
|
||||
<IconButton
|
||||
onClick={handleFlattenPrompts}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-label={t('stylePresets.flatten')}
|
||||
icon={<PiStackSimpleBold />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('stylePresets.clearTemplateSelection')}>
|
||||
<IconButton
|
||||
onClick={handleClearActiveStylePreset}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-label={t('stylePresets.clearTemplateSelection')}
|
||||
icon={<PiXBold />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
<Flex w="full" alignItems="center" gap={2} minW={0}>
|
||||
<StylePresetImage imageWidth={25} presetImageUrl={activeStylePreset.image} />
|
||||
<Badge colorScheme="invokeBlue" variant="subtle" justifySelf="flex-start">
|
||||
{activeStylePreset.name}
|
||||
</Badge>
|
||||
<Spacer />
|
||||
<Tooltip label={t('stylePresets.toggleViewMode')}>
|
||||
<IconButton
|
||||
onClick={handleToggleViewMode}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-label={t('stylePresets.toggleViewMode')}
|
||||
colorScheme={viewMode ? 'invokeBlue' : 'base'}
|
||||
icon={<PiEyeBold />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('stylePresets.flatten')}>
|
||||
<IconButton
|
||||
onClick={handleFlattenPrompts}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-label={t('stylePresets.flatten')}
|
||||
icon={<PiStackSimpleBold />}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip label={t('stylePresets.clearTemplateSelection')}>
|
||||
<IconButton
|
||||
onClick={handleClearActiveStylePreset}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-label={t('stylePresets.clearTemplateSelection')}
|
||||
icon={<PiXBold />}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,8 +30,8 @@ export const StylePresetForm = ({
|
||||
updatingStylePresetId: string | null;
|
||||
formData: StylePresetFormData | null;
|
||||
}) => {
|
||||
const [createStylePreset] = useCreateStylePresetMutation();
|
||||
const [updateStylePreset] = useUpdateStylePresetMutation();
|
||||
const [createStylePreset, { isLoading: isCreating }] = useCreateStylePresetMutation();
|
||||
const [updateStylePreset, { isLoading: isUpdating }] = useUpdateStylePresetMutation();
|
||||
const { t } = useTranslation();
|
||||
const allowPrivateStylePresets = useAppSelector((s) => s.config.allowPrivateStylePresets);
|
||||
|
||||
@@ -93,8 +93,8 @@ export const StylePresetForm = ({
|
||||
</FormControl>
|
||||
</Flex>
|
||||
|
||||
<StylePresetPromptField label="Positive Prompt" control={control} name="positivePrompt" />
|
||||
<StylePresetPromptField label="Negative Prompt" control={control} name="negativePrompt" />
|
||||
<StylePresetPromptField label={t('stylePresets.positivePrompt')} control={control} name="positivePrompt" />
|
||||
<StylePresetPromptField label={t('stylePresets.negativePrompt')} control={control} name="negativePrompt" />
|
||||
<Box>
|
||||
<Text variant="subtext">{t('stylePresets.promptTemplatesDesc1')}</Text>
|
||||
<Text variant="subtext">
|
||||
@@ -109,7 +109,11 @@ export const StylePresetForm = ({
|
||||
|
||||
<Flex justifyContent="space-between" alignItems="flex-end" gap={10}>
|
||||
{allowPrivateStylePresets ? <StylePresetTypeField control={control} name="type" /> : <Spacer />}
|
||||
<Button onClick={handleSubmit(handleClickSave)} isDisabled={!formState.isValid}>
|
||||
<Button
|
||||
onClick={handleSubmit(handleClickSave)}
|
||||
isDisabled={!formState.isValid}
|
||||
isLoading={isCreating || isUpdating}
|
||||
>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
@@ -48,9 +48,13 @@ export const StylePresetModal = () => {
|
||||
} else {
|
||||
let file = null;
|
||||
if (data.imageUrl) {
|
||||
const blob = await convertImageUrlToBlob(data.imageUrl);
|
||||
if (blob) {
|
||||
file = new File([blob], 'style_preset.png', { type: 'image/png' });
|
||||
try {
|
||||
const blob = await convertImageUrlToBlob(data.imageUrl);
|
||||
if (blob) {
|
||||
file = new File([blob], 'style_preset.png', { type: 'image/png' });
|
||||
}
|
||||
} catch (error) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
setFormData({
|
||||
|
||||
@@ -21,6 +21,7 @@ const StylePresetImage = ({ presetImageUrl, imageWidth }: { presetImageUrl: stri
|
||||
/>
|
||||
)
|
||||
}
|
||||
p={2}
|
||||
>
|
||||
<Image
|
||||
src={presetImageUrl || ''}
|
||||
|
||||
@@ -77,7 +77,7 @@ export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithI
|
||||
|
||||
const handleDeletePreset = useCallback(async () => {
|
||||
try {
|
||||
await deleteStylePreset(preset.id);
|
||||
await deleteStylePreset(preset.id).unwrap();
|
||||
toast({
|
||||
status: 'success',
|
||||
title: t('stylePresets.templateDeleted'),
|
||||
@@ -174,7 +174,8 @@ export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithI
|
||||
onClose={onClose}
|
||||
title={t('stylePresets.deleteTemplate')}
|
||||
acceptCallback={handleDeletePreset}
|
||||
acceptButtonText="Delete"
|
||||
acceptButtonText={t('common.delete')}
|
||||
cancelButtonText={t('common.cancel')}
|
||||
>
|
||||
<p>{t('stylePresets.deleteTemplate2')}</p>
|
||||
</ConfirmationAlertDialog>
|
||||
|
||||
@@ -29,14 +29,14 @@ export const StylePresetMenuTrigger = () => {
|
||||
py={2}
|
||||
px={3}
|
||||
borderRadius="base"
|
||||
gap={1}
|
||||
gap={2}
|
||||
role="button"
|
||||
_hover={_hover}
|
||||
transitionProperty="background-color"
|
||||
transitionDuration="normal"
|
||||
w="full"
|
||||
>
|
||||
<ActiveStylePreset />
|
||||
|
||||
<IconButton aria-label={t('stylePresets.viewList')} variant="ghost" icon={<PiCaretDownBold />} size="sm" />
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig } from 'app/store/store';
|
||||
import { stylePresetsApi } from 'services/api/endpoints/stylePresets';
|
||||
|
||||
import type { StylePresetState } from './types';
|
||||
|
||||
@@ -24,6 +25,26 @@ export const stylePresetSlice = createSlice({
|
||||
state.viewMode = action.payload;
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder.addMatcher(stylePresetsApi.endpoints.deleteStylePreset.matchFulfilled, (state, action) => {
|
||||
if (state.activeStylePresetId === null) {
|
||||
return;
|
||||
}
|
||||
const deletedId = action.meta.arg.originalArgs;
|
||||
if (state.activeStylePresetId === deletedId) {
|
||||
state.activeStylePresetId = null;
|
||||
}
|
||||
});
|
||||
builder.addMatcher(stylePresetsApi.endpoints.listStylePresets.matchFulfilled, (state, action) => {
|
||||
if (state.activeStylePresetId === null) {
|
||||
return;
|
||||
}
|
||||
const ids = action.payload.map((preset) => preset.id);
|
||||
if (!ids.includes(state.activeStylePresetId)) {
|
||||
state.activeStylePresetId = null;
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { activeStylePresetIdChanged, searchTermChanged, viewModeChanged } = stylePresetSlice.actions;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { getViewModeChunks } from 'features/stylePresets/util/getViewModeChunks';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
describe('getViewModeChunks', () => {
|
||||
it('should return empty strings when presetPrompt is not provided', () => {
|
||||
const currentPrompt = 'current prompt';
|
||||
const presetPrompt = undefined;
|
||||
const result = getViewModeChunks(currentPrompt, presetPrompt);
|
||||
expect(result).toEqual(['', currentPrompt, '']);
|
||||
});
|
||||
|
||||
it('should return empty strings when presetPrompt is empty', () => {
|
||||
const currentPrompt = 'current prompt';
|
||||
const presetPrompt = '';
|
||||
const result = getViewModeChunks(currentPrompt, presetPrompt);
|
||||
expect(result).toEqual(['', currentPrompt, '']);
|
||||
});
|
||||
|
||||
it('should append presetPrompt to currentPrompt when presetPrompt does not contain PRESET_PLACEHOLDER', () => {
|
||||
const currentPrompt = 'current prompt';
|
||||
const presetPrompt = 'preset prompt';
|
||||
const result = getViewModeChunks(currentPrompt, presetPrompt);
|
||||
expect(result).toEqual(['', `${currentPrompt} `, presetPrompt]);
|
||||
});
|
||||
|
||||
it('should split presetPrompt into 3 parts when presetPrompt contains PRESET_PLACEHOLDER', () => {
|
||||
const currentPrompt = 'current prompt';
|
||||
const presetPrompt = 'before {prompt} after';
|
||||
const result = getViewModeChunks(currentPrompt, presetPrompt);
|
||||
expect(result).toEqual(['before ', currentPrompt, ' after']);
|
||||
});
|
||||
|
||||
it('should split presetPrompt into 3 parts when presetPrompt contains multiple PRESET_PLACEHOLDER', () => {
|
||||
const currentPrompt = 'current prompt';
|
||||
const presetPrompt = 'before {prompt} middle {prompt} after';
|
||||
const result = getViewModeChunks(currentPrompt, presetPrompt);
|
||||
expect(result).toEqual(['before ', currentPrompt, ' middle {prompt} after']);
|
||||
});
|
||||
|
||||
it('should handle the PRESET_PLACEHOLDER being at the start of the presetPrompt', () => {
|
||||
const currentPrompt = 'current prompt';
|
||||
const presetPrompt = '{prompt} after';
|
||||
const result = getViewModeChunks(currentPrompt, presetPrompt);
|
||||
expect(result).toEqual(['', currentPrompt, ' after']);
|
||||
});
|
||||
|
||||
it('should handle the PRESET_PLACEHOLDER being at the end of the presetPrompt', () => {
|
||||
const currentPrompt = 'current prompt';
|
||||
const presetPrompt = 'before {prompt}';
|
||||
const result = getViewModeChunks(currentPrompt, presetPrompt);
|
||||
expect(result).toEqual(['before ', currentPrompt, '']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { PRESET_PLACEHOLDER } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
|
||||
|
||||
export const getViewModeChunks = (currentPrompt: string, presetPrompt?: string): [string, string, string] => {
|
||||
if (!presetPrompt || !presetPrompt.length) {
|
||||
return ['', currentPrompt, ''];
|
||||
}
|
||||
|
||||
// When preset prompt does not contain the placeholder, we append the preset to the current prompt
|
||||
if (!presetPrompt.includes(PRESET_PLACEHOLDER)) {
|
||||
return ['', `${currentPrompt} `, presetPrompt];
|
||||
}
|
||||
|
||||
// Otherwise, we split the preset prompt into 3 parts: before, current, and after the placeholder
|
||||
const [before, ...after] = presetPrompt.split(PRESET_PLACEHOLDER);
|
||||
|
||||
return [before || '', currentPrompt, after.join(PRESET_PLACEHOLDER) || ''];
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
import { PRESET_PLACEHOLDER } from 'features/stylePresets/hooks/usePresetModifiedPrompts';
|
||||
|
||||
export const getViewModeChunks = (currentPrompt: string, presetPrompt?: string): [string, string, string] => {
|
||||
if (!presetPrompt || !presetPrompt.length) {
|
||||
return ['', currentPrompt, ''];
|
||||
}
|
||||
|
||||
const [before, after] = presetPrompt.split(PRESET_PLACEHOLDER, 2);
|
||||
|
||||
if (!before || !after) {
|
||||
return ['', `${currentPrompt} `, before || after || ''];
|
||||
}
|
||||
|
||||
return [before ?? '', currentPrompt, after ?? ''];
|
||||
};
|
||||
@@ -94,7 +94,7 @@ export const stylePresetsApi = api.injectEndpoints({
|
||||
}),
|
||||
exportStylePresets: build.query<string, void>({
|
||||
query: () => ({
|
||||
url: buildStylePresetsUrl('/export'),
|
||||
url: buildStylePresetsUrl('export'),
|
||||
responseHandler: (response) => response.text(),
|
||||
}),
|
||||
providesTags: ['FetchOnReconnect', { type: 'StylePreset', id: LIST_TAG }],
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "4.2.8rc1"
|
||||
__version__ = "4.2.8"
|
||||
|
||||
@@ -158,6 +158,10 @@ version = { attr = "invokeai.version.__version__" }
|
||||
[tool.setuptools.package-data]
|
||||
"invokeai.app.assets" = ["**/*.png"]
|
||||
"invokeai.app.services.workflow_records.default_workflows" = ["*.json"]
|
||||
"invokeai.app.services.style_preset_records" = ["*.json"]
|
||||
"invokeai.app.services.style_preset_images.default_style_preset_images" = [
|
||||
"*.png",
|
||||
]
|
||||
"invokeai.assets.fonts" = ["**/*.ttf"]
|
||||
"invokeai.backend" = ["**.png"]
|
||||
"invokeai.configs" = ["*.example", "**/*.yaml", "*.txt"]
|
||||
|
||||
Reference in New Issue
Block a user