refactor(ui): simplified metadata parsing (WIP)

This commit is contained in:
psychedelicious
2025-07-03 18:49:01 +10:00
parent 42688a0993
commit 98ecefdce0
2 changed files with 692 additions and 93 deletions

View File

@@ -1,17 +1,19 @@
import { Flex } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { MetadataItem } from 'features/metadata/components/MetadataItem';
import type { FlexProps } from '@invoke-ai/ui-library';
import { Flex, IconButton, Text } from '@invoke-ai/ui-library';
import { typedMemo } from 'common/util/typedMemo';
import { isPrimitive } from 'es-toolkit';
import { MetadataLoRAs } from 'features/metadata/components/MetadataLoRAs';
import { handlers } from 'features/metadata/util/handlers';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { MetadataHanders, type MetadataHandler, useMetadata } from 'features/metadata/parsing';
import type { ReactNode } from 'react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowBendUpLeftBold } from 'react-icons/pi';
type Props = {
metadata?: unknown;
};
const ImageMetadataActions = (props: Props) => {
const activeTabName = useAppSelector(selectActiveTab);
const { metadata } = props;
if (!metadata || Object.keys(metadata).length === 0) {
@@ -19,38 +21,89 @@ const ImageMetadataActions = (props: Props) => {
}
return (
<Flex flexDir="column" pl={8}>
<MetadataItem metadata={metadata} handlers={handlers.generationMode} />
<MetadataItem metadata={metadata} handlers={handlers.positivePrompt} direction="column" />
<MetadataItem metadata={metadata} handlers={handlers.negativePrompt} direction="column" />
<MetadataItem metadata={metadata} handlers={handlers.sdxlPositiveStylePrompt} direction="column" />
<MetadataItem metadata={metadata} handlers={handlers.sdxlNegativeStylePrompt} direction="column" />
<MetadataItem metadata={metadata} handlers={handlers.model} />
<MetadataItem metadata={metadata} handlers={handlers.vae} />
<MetadataItem metadata={metadata} handlers={handlers.width} />
<MetadataItem metadata={metadata} handlers={handlers.height} />
<MetadataItem metadata={metadata} handlers={handlers.seed} />
<MetadataItem metadata={metadata} handlers={handlers.steps} />
<MetadataItem metadata={metadata} handlers={handlers.scheduler} />
<MetadataItem metadata={metadata} handlers={handlers.cfgScale} />
<MetadataItem metadata={metadata} handlers={handlers.cfgRescaleMultiplier} />
<MetadataItem metadata={metadata} handlers={handlers.guidance} />
{activeTabName !== 'canvas' && <MetadataItem metadata={metadata} handlers={handlers.strength} />}
<MetadataItem metadata={metadata} handlers={handlers.seamlessX} />
<MetadataItem metadata={metadata} handlers={handlers.seamlessY} />
<MetadataItem metadata={metadata} handlers={handlers.hrfEnabled} />
<MetadataItem metadata={metadata} handlers={handlers.hrfMethod} />
<MetadataItem metadata={metadata} handlers={handlers.hrfStrength} />
<MetadataItem metadata={metadata} handlers={handlers.refinerCFGScale} />
<MetadataItem metadata={metadata} handlers={handlers.refinerModel} />
<MetadataItem metadata={metadata} handlers={handlers.refinerNegativeAestheticScore} />
<MetadataItem metadata={metadata} handlers={handlers.refinerPositiveAestheticScore} />
<MetadataItem metadata={metadata} handlers={handlers.refinerScheduler} />
<MetadataItem metadata={metadata} handlers={handlers.refinerStart} />
<MetadataItem metadata={metadata} handlers={handlers.refinerSteps} />
<Flex flexDir="column" ps={8}>
<MetadataItem2 metadata={metadata} handler={MetadataHanders.CreatedBy} />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.GenerationMode} />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.PositivePrompt} flexDir="column" />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.NegativePrompt} flexDir="column" />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.PositiveStylePrompt} flexDir="column" />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.NegativeStylePrompt} flexDir="column" />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.NegativePrompt} flexDir="column" />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.MainModel} />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.VAEModel} />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.Width} />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.Height} />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.Seed} />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.Steps} />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.Scheduler} />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.CFGScale} />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.CFGRescaleMultiplier} />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.Guidance} />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.DenoisingStrength} />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.SeamlessX} />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.SeamlessY} />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.RefinerModel} />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.RefinerCFGScale} />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.RefinerPositiveAestheticScore} />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.RefinerNegativeAestheticScore} />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.RefinerScheduler} />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.RefinerDenoisingStart} />
<MetadataItem2 metadata={metadata} handler={MetadataHanders.RefinerSteps} />
<MetadataLoRAs metadata={metadata} />
</Flex>
);
};
export default memo(ImageMetadataActions);
const MetadataItem2 = typedMemo(
<T,>({ metadata, handler, ...rest }: { metadata: unknown; handler: MetadataHandler<T> } & FlexProps) => {
const { t } = useTranslation();
const { data, recall } = useMetadata(metadata, handler);
if (!data.isParsed) {
return null;
}
if (data.isSuccess) {
const label = handler.renderLabel(data.value, t);
const value = handler.renderValue(data.value, t);
return (
<Flex gap={2}>
<IconButton
aria-label="Recall Parameter"
icon={<PiArrowBendUpLeftBold />}
size="xs"
variant="ghost"
onClick={recall}
/>
<Flex {...rest}>
<MetadataLabel label={label} />
<MetadataValue value={value} />
</Flex>
</Flex>
);
}
}
);
MetadataItem2.displayName = 'MetadataItem2';
const MetadataLabel = ({ label }: { label: ReactNode }) => {
if (isPrimitive(label)) {
return (
<Text fontWeight="semibold" whiteSpace="pre-wrap" me={2}>
{label}
</Text>
);
} else {
return <>{label}</>;
}
};
const MetadataValue = ({ value }: { value: ReactNode }) => {
if (isPrimitive(value)) {
return <Text>{value}</Text>;
}
return <>{value}</>;
};

View File

@@ -1,16 +1,88 @@
import type { AppStore } from 'app/store/store';
import { type Atom, atom } from 'nanostores';
import type { ReactNode } from 'react';
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
type Item<T> = T extends any[] ? T[number] : T;
import { useAppStore } from 'app/store/storeHooks';
import { withResultAsync } from 'common/util/result';
import { get } from 'es-toolkit/compat';
import { bboxHeightChanged, bboxWidthChanged } from 'features/controlLayers/store/canvasSlice';
import {
negativePrompt2Changed,
negativePromptChanged,
positivePrompt2Changed,
positivePromptChanged,
refinerModelChanged,
setCfgRescaleMultiplier,
setCfgScale,
setGuidance,
setImg2imgStrength,
setRefinerCFGScale,
setRefinerNegativeAestheticScore,
setRefinerPositiveAestheticScore,
setRefinerScheduler,
setRefinerStart,
setRefinerSteps,
setScheduler,
setSeamlessXAxis,
setSeamlessYAxis,
setSeed,
setSteps,
vaeSelected,
} from 'features/controlLayers/store/paramsSlice';
import type { ModelIdentifierField } from 'features/nodes/types/common';
import { zModelIdentifierField } from 'features/nodes/types/common';
import { zModelIdentifier } from 'features/nodes/types/v2/common';
import { modelSelected } from 'features/parameters/store/actions';
import type {
ParameterCFGRescaleMultiplier,
ParameterCFGScale,
ParameterGuidance,
ParameterHeight,
ParameterModel,
ParameterNegativePrompt,
ParameterPositivePrompt,
ParameterPositiveStylePromptSDXL,
ParameterScheduler,
ParameterSDXLRefinerModel,
ParameterSDXLRefinerNegativeAestheticScore,
ParameterSDXLRefinerPositiveAestheticScore,
ParameterSDXLRefinerStart,
ParameterSeamlessX,
ParameterSeamlessY,
ParameterSeed,
ParameterSteps,
ParameterStrength,
ParameterVAEModel,
ParameterWidth,
} from 'features/parameters/types/parameterSchemas';
import {
zParameterCFGRescaleMultiplier,
zParameterCFGScale,
zParameterGuidance,
zParameterImageDimension,
zParameterNegativePrompt,
zParameterNegativeStylePromptSDXL,
zParameterPositivePrompt,
zParameterPositiveStylePromptSDXL,
zParameterScheduler,
zParameterSDXLRefinerNegativeAestheticScore,
zParameterSDXLRefinerPositiveAestheticScore,
zParameterSDXLRefinerStart,
zParameterSeamlessX,
zParameterSeamlessY,
zParameterSeed,
zParameterSteps,
zParameterStrength,
} from 'features/parameters/types/parameterSchemas';
import type { TFunction } from 'i18next';
import { type ReactNode, useCallback, useEffect, useState } from 'react';
import { modelsApi } from 'services/api/endpoints/models';
import type { ModelType } from 'services/api/types';
import { assert } from 'tsafe';
import z from 'zod/v4';
type UnparsedData = {
isParsed: false;
isSuccess: false;
isError: false;
raw: unknown;
parsed: null;
value: null;
error: null;
};
@@ -18,8 +90,7 @@ type ParsedSuccessData<T> = {
isParsed: true;
isSuccess: true;
isError: false;
raw: unknown;
parsed: T;
value: T;
error: null;
};
@@ -27,62 +98,537 @@ type ParsedErrorData = {
isParsed: true;
isSuccess: false;
isError: true;
raw: unknown;
parsed: null;
value: null;
error: Error;
};
type Data<T> = UnparsedData | ParsedSuccessData<T> | ParsedErrorData;
export type Data<T> = UnparsedData | ParsedSuccessData<T> | ParsedErrorData;
abstract class MetadataParser<T> {
$data: Atom<Data<T>>;
store: AppStore;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
export type MetadataHandler<T = any> = {
type: string;
parse: (metadata: unknown, store: AppStore) => Promise<T> | T;
recall?: (value: T, store: AppStore) => void;
renderLabel: (value: T, t: TFunction) => ReactNode;
renderValue: (value: T, t: TFunction) => ReactNode;
};
abstract extract: (metadata: unknown) => Promise<unknown>;
abstract parse: (metadata: unknown) => Promise<T>;
abstract recall: (data: Item<T>) => Promise<void>;
abstract renderLabel: (data: Item<T>) => ReactNode;
abstract renderValue: (data: Item<T>) => ReactNode;
//#region Created By
const CreatedBy: MetadataHandler<string> = {
type: 'CreatedBy',
parse: (metadata, _store) => {
const raw = get(metadata, 'created_by');
const parsed = z.string().parse(raw);
return parsed;
},
renderLabel: (_value, t) => t('metadata.createdBy'),
renderValue: (value) => value,
};
//#endregion Created By
constructor(store: AppStore) {
this.$data = atom(MetadataParser.getInitialData());
this.store = store;
}
//#region Generation Mode
const GenerationMode: MetadataHandler<string> = {
type: 'GenerationMode',
parse: (metadata, _store) => {
const raw = get(metadata, 'generation_mode');
const parsed = z.string().parse(raw);
return parsed;
},
renderLabel: (_value, t) => t('metadata.generationMode'),
renderValue: (value) => value,
};
//#endregion Generation Mode
static getInitialData = (): UnparsedData => {
return {
isParsed: false,
isSuccess: false,
isError: false,
raw: null,
parsed: null,
error: null,
};
};
//#region Positive Prompt
const PositivePrompt: MetadataHandler<ParameterPositivePrompt> = {
type: 'PositivePrompt',
parse: (metadata, _store) => {
const raw = get(metadata, 'positive_prompt');
const parsed = zParameterPositivePrompt.parse(raw);
return parsed;
},
recall: (value, store) => {
store.dispatch(positivePromptChanged(value));
},
renderLabel: (_value, t) => t('metadata.positivePrompt'),
renderValue: (value) => value,
};
//#endregion Positive Prompt
//#region Negative Prompt
const NegativePrompt: MetadataHandler<ParameterNegativePrompt> = {
type: 'NegativePrompt',
parse: (metadata, _store) => {
const raw = get(metadata, 'negative_prompt');
const parsed = zParameterNegativePrompt.parse(raw);
return parsed;
},
recall: (value, store) => {
store.dispatch(negativePromptChanged(value));
},
renderLabel: (_value, t) => t('metadata.negativePrompt'),
renderValue: (value) => value,
};
//#endregion Negative Prompt
//#region SDXL Positive Style Prompt
const PositiveStylePrompt: MetadataHandler<ParameterPositiveStylePromptSDXL> = {
type: 'PositiveStylePrompt',
parse: (metadata, _store) => {
const raw = get(metadata, 'positive_style_prompt');
const parsed = zParameterPositiveStylePromptSDXL.parse(raw);
return parsed;
},
recall: (value, store) => {
store.dispatch(positivePrompt2Changed(value));
},
renderLabel: (_value, t) => t('sdxl.posStylePrompt'),
renderValue: (value) => value,
};
//#endregion SDXL Positive Style Prompt
//#region SDXL Negative Style Prompt
const NegativeStylePrompt: MetadataHandler<ParameterPositiveStylePromptSDXL> = {
type: 'NegativeStylePrompt',
parse: (metadata, _store) => {
const raw = get(metadata, 'negative_style_prompt');
const parsed = zParameterNegativeStylePromptSDXL.parse(raw);
return parsed;
},
recall: (value, store) => {
store.dispatch(negativePrompt2Changed(value));
},
renderLabel: (_value, t) => t('sdxl.negStylePrompt'),
renderValue: (value) => value,
};
//#endregion SDXL Negative Style Prompt
//#region CFG Scale
const CFGScale: MetadataHandler<ParameterCFGScale> = {
type: 'CFGScale',
parse: (metadata, _store) => {
const raw = get(metadata, 'cfg_scale');
const parsed = zParameterCFGScale.parse(raw);
return parsed;
},
recall: (value, store) => {
store.dispatch(setCfgScale(value));
},
renderLabel: (_value, t) => t('metadata.cfgScale'),
renderValue: (value) => value,
};
//#endregion CFG Scale
//#region CFG Rescale Multiplier
const CFGRescaleMultiplier: MetadataHandler<ParameterCFGRescaleMultiplier> = {
type: 'CFGRescaleMultiplier',
parse: (metadata, _store) => {
const raw = get(metadata, 'cfg_rescale_multiplier');
const parsed = zParameterCFGRescaleMultiplier.parse(raw);
return parsed;
},
recall: (value, store) => {
store.dispatch(setCfgRescaleMultiplier(value));
},
renderLabel: (_value, t) => t('metadata.cfgRescaleMultiplier'),
renderValue: (value) => value,
};
//#endregion CFG Rescale Multiplier
//#region Guidance
const Guidance: MetadataHandler<ParameterGuidance> = {
type: 'Guidance',
parse: (metadata, _store) => {
const raw = get(metadata, 'guidance');
const parsed = zParameterGuidance.parse(raw);
return parsed;
},
recall: (value, store) => {
store.dispatch(setGuidance(value));
},
renderLabel: (_value, t) => t('metadata.guidance'),
renderValue: (value) => value,
};
//#endregion Guidance
//#region Scheduler
const Scheduler: MetadataHandler<ParameterScheduler> = {
type: 'Scheduler',
parse: (metadata, _store) => {
const raw = get(metadata, 'scheduler');
const parsed = zParameterScheduler.parse(raw);
return parsed;
},
recall: (value, store) => {
store.dispatch(setScheduler(value));
},
renderLabel: (_value, t) => t('metadata.scheduler'),
renderValue: (value) => value,
};
//#endregion Scheduler
//#region Width
const Width: MetadataHandler<ParameterWidth> = {
type: 'Width',
parse: (metadata, _store) => {
const raw = get(metadata, 'width');
const parsed = zParameterImageDimension.parse(raw);
return parsed;
},
recall: (value, store) => {
store.dispatch(bboxWidthChanged({ width: value, updateAspectRatio: true, clamp: true }));
},
renderLabel: (_value, t) => t('metadata.width'),
renderValue: (value) => value,
};
//#endregion Width
//#region Height
const Height: MetadataHandler<ParameterHeight> = {
type: 'Height',
parse: (metadata, _store) => {
const raw = get(metadata, 'height');
const parsed = zParameterImageDimension.parse(raw);
return parsed;
},
recall: (value, store) => {
store.dispatch(bboxHeightChanged({ height: value, updateAspectRatio: true, clamp: true }));
},
renderLabel: (_value, t) => t('metadata.height'),
renderValue: (value) => value,
};
//#endregion Height
//#region Seed
const Seed: MetadataHandler<ParameterSeed> = {
type: 'Seed',
parse: (metadata, _store) => {
const raw = get(metadata, 'seed');
const parsed = zParameterSeed.parse(raw);
return parsed;
},
recall: (value, store) => {
store.dispatch(setSeed(value));
},
renderLabel: (_value, t) => t('metadata.seed'),
renderValue: (value) => value,
};
//#endregion Seed
//#region Steps
const Steps: MetadataHandler<ParameterSteps> = {
type: 'Steps',
parse: (metadata, _store) => {
const raw = get(metadata, 'steps');
const parsed = zParameterSteps.parse(raw);
return parsed;
},
recall: (value, store) => {
store.dispatch(setSteps(value));
},
renderLabel: (_value, t) => t('metadata.steps'),
renderValue: (value) => value,
};
//#endregion Steps
//#region DenoisingStrength
const DenoisingStrength: MetadataHandler<ParameterStrength> = {
type: 'DenoisingStrength',
parse: (metadata, _store) => {
const raw = get(metadata, 'strength');
const parsed = zParameterStrength.parse(raw);
return parsed;
},
recall: (value, store) => {
store.dispatch(setImg2imgStrength(value));
},
renderLabel: (_value, t) => t('metadata.strength'),
renderValue: (value) => value,
};
//#endregion DenoisingStrength
//#region SeamlessX
const SeamlessX: MetadataHandler<ParameterSeamlessX> = {
type: 'SeamlessX',
parse: (metadata, _store) => {
const raw = get(metadata, 'seamless_x');
const parsed = zParameterSeamlessX.parse(raw);
return parsed;
},
recall: (value, store) => {
store.dispatch(setSeamlessXAxis(value));
},
renderLabel: (_value, t) => t('metadata.seamlessXAxis'),
renderValue: (value) => value,
};
//#endregion SeamlessX
//#region SeamlessY
const SeamlessY: MetadataHandler<ParameterSeamlessY> = {
type: 'SeamlessY',
parse: (metadata, _store) => {
const raw = get(metadata, 'seamless_y');
const parsed = zParameterSeamlessY.parse(raw);
return parsed;
},
recall: (value, store) => {
store.dispatch(setSeamlessYAxis(value));
},
renderLabel: (_value, t) => t('metadata.seamlessYAxis'),
renderValue: (value) => value,
};
//#endregion SeamlessY
//#region RefinerModel
const RefinerModel: MetadataHandler<ParameterSDXLRefinerModel> = {
type: 'RefinerModel',
parse: async (metadata, store) => {
const raw = get(metadata, 'refiner_model');
const parsed = await parseModelIdentifier(raw, store, 'main');
assert(parsed.type === 'main');
assert(parsed.base === 'sdxl-refiner');
return parsed;
},
recall: (value, store) => {
store.dispatch(refinerModelChanged(value));
},
renderLabel: (_value, t) => t('sdxl.refinermodel'),
renderValue: (value) => `${value.name} (${value.base.toUpperCase()})`,
};
//#endregion RefinerModel
//#region RefinerSteps
const RefinerSteps: MetadataHandler<ParameterSteps> = {
type: 'RefinerSteps',
parse: (metadata, _store) => {
const raw = get(metadata, 'refiner_steps');
const parsed = zParameterSteps.parse(raw);
return parsed;
},
recall: (value, store) => {
store.dispatch(setRefinerSteps(value));
},
renderLabel: (_value, t) => t('sdxl.refinerSteps'),
renderValue: (value) => value,
};
//#endregion RefinerSteps
//#region RefinerCFGScale
const RefinerCFGScale: MetadataHandler<ParameterSteps> = {
type: 'RefinerCFGScale',
parse: (metadata, _store) => {
const raw = get(metadata, 'refiner_cfg_scale');
const parsed = zParameterCFGScale.parse(raw);
return parsed;
},
recall: (value, store) => {
store.dispatch(setRefinerCFGScale(value));
},
renderLabel: (_value, t) => t('sdxl.cfgScale'),
renderValue: (value) => value,
};
//#endregion RefinerCFGScale
//#region RefinerScheduler
const RefinerScheduler: MetadataHandler<ParameterScheduler> = {
type: 'RefinerScheduler',
parse: (metadata, _store) => {
const raw = get(metadata, 'refiner_scheduler');
const parsed = zParameterScheduler.parse(raw);
return parsed;
},
recall: (value, store) => {
store.dispatch(setRefinerScheduler(value));
},
renderLabel: (_value, t) => t('sdxl.scheduler'),
renderValue: (value) => value,
};
//#endregion RefinerScheduler
//#region RefinerPositiveAestheticScore
const RefinerPositiveAestheticScore: MetadataHandler<ParameterSDXLRefinerPositiveAestheticScore> = {
type: 'RefinerPositiveAestheticScore',
parse: (metadata, _store) => {
const raw = get(metadata, 'refiner_positive_aesthetic_score');
const parsed = zParameterSDXLRefinerPositiveAestheticScore.parse(raw);
return parsed;
},
recall: (value, store) => {
store.dispatch(setRefinerPositiveAestheticScore(value));
},
renderLabel: (_value, t) => t('sdxl.posAestheticScore'),
renderValue: (value) => value,
};
//#endregion RefinerPositiveAestheticScore
//#region RefinerNegativeAestheticScore
const RefinerNegativeAestheticScore: MetadataHandler<ParameterSDXLRefinerNegativeAestheticScore> = {
type: 'RefinerNegativeAestheticScore',
parse: (metadata, _store) => {
const raw = get(metadata, 'refiner_negative_aesthetic_score');
const parsed = zParameterSDXLRefinerNegativeAestheticScore.parse(raw);
return parsed;
},
recall: (value, store) => {
store.dispatch(setRefinerNegativeAestheticScore(value));
},
renderLabel: (_value, t) => t('sdxl.negAestheticScore'),
renderValue: (value) => value,
};
//#endregion RefinerNegativeAestheticScore
//#region RefinerDenoisingStart
const RefinerDenoisingStart: MetadataHandler<ParameterSDXLRefinerStart> = {
type: 'RefinerDenoisingStart',
parse: (metadata, _store) => {
const raw = get(metadata, 'refiner_start');
const parsed = zParameterSDXLRefinerStart.parse(raw);
return parsed;
},
recall: (value, store) => {
store.dispatch(setRefinerStart(value));
},
renderLabel: (_value, t) => t('sdxl.refinerStart'),
renderValue: (value) => value,
};
//#endregion RefinerDenoisingStart
//#region MainModel
const MainModel: MetadataHandler<ParameterModel> = {
type: 'MainModel',
parse: async (metadata, store) => {
const raw = get(metadata, 'model');
const parsed = await parseModelIdentifier(raw, store, 'main');
assert(parsed.type === 'main');
return parsed;
},
recall: (value, store) => {
store.dispatch(modelSelected(value));
},
renderLabel: (_value, t) => t('metadata.model'),
renderValue: (value) => `${value.name} (${value.base.toUpperCase()})`,
};
//#endregion MainModel
//#region VAEModel
const VAEModel: MetadataHandler<ParameterVAEModel> = {
type: 'VAEModel',
parse: async (metadata, store) => {
const raw = get(metadata, 'vae');
const parsed = await parseModelIdentifier(raw, store, 'vae');
assert(parsed.type === 'vae');
return parsed;
},
recall: (value, store) => {
store.dispatch(vaeSelected(value));
},
renderLabel: (_value, t) => t('metadata.vae'),
renderValue: (value) => `${value.name} (${value.base.toUpperCase()})`,
};
//#endregion VAEModel
export const MetadataHanders = {
CreatedBy,
GenerationMode,
PositivePrompt,
NegativePrompt,
PositiveStylePrompt,
NegativeStylePrompt,
CFGScale,
CFGRescaleMultiplier,
Guidance,
Scheduler,
Width,
Height,
Seed,
Steps,
DenoisingStrength,
SeamlessX,
SeamlessY,
RefinerModel,
RefinerSteps,
RefinerCFGScale,
RefinerScheduler,
RefinerPositiveAestheticScore,
RefinerNegativeAestheticScore,
RefinerDenoisingStart,
MainModel,
VAEModel,
} satisfies Record<string, MetadataHandler>;
export function useMetadata<T>(metadata: unknown, handler: MetadataHandler<T>) {
const store = useAppStore();
const [data, setData] = useState<Data<T>>(() => ({
isParsed: false,
isSuccess: false,
isError: false,
value: null,
error: null,
}));
const parse = useCallback(
async (metadata: unknown) => {
const result = await withResultAsync(async () => await Promise.resolve(handler.parse(metadata, store)));
if (result.isOk()) {
setData({
isParsed: true,
isSuccess: true,
isError: false,
value: result.value,
error: null,
});
} else {
setData({
isParsed: true,
isSuccess: false,
isError: true,
value: null,
error: result.error,
});
}
},
[handler, store]
);
useEffect(() => {
parse(metadata);
}, [metadata, parse]);
const recall = useCallback(() => {
if (!data.isSuccess) {
return;
}
handler.recall?.(data.value, store);
}, [data.isSuccess, data.value, handler, store]);
return { data, recall };
}
export class PositivePromptParser extends MetadataParser<string> {
constructor(store: AppStore) {
super(store);
const parseModelIdentifier = async (raw: unknown, store: AppStore, type: ModelType): Promise<ModelIdentifierField> => {
// First try the current format identifier: key, name, base, type, hash
try {
const identifier = zModelIdentifierField.parse(raw);
const modelConfig = store
.dispatch(modelsApi.endpoints.getModelConfig.initiate(identifier.key, { subscribe: false }))
.unwrap();
return zModelIdentifierField.parse(modelConfig);
} catch {
// noop
}
extract = (metadata: unknown) => {
return Promise.resolve(metadata);
};
parse = (_metadata: unknown) => {
return Promise.resolve('test');
};
recall = (_data: string) => {
return Promise.resolve();
};
renderLabel = (data: string) => {
return <div>{data}</div>;
};
renderValue = (data: string) => {
return <div>{data}</div>;
};
}
// Fall back to old format identifier: model_name, base_model
try {
const identifier = zModelIdentifier.parse(raw);
const modelConfig = await store
.dispatch(
modelsApi.endpoints.getModelConfigByAttrs.initiate(
{ name: identifier.model_name, base: identifier.base_model, type },
{ subscribe: false }
)
)
.unwrap();
return zModelIdentifierField.parse(modelConfig);
} catch {
// noop
}
throw new Error('Unable to parse model identifier');
};