feat(ui): metadata recall for videos

This commit is contained in:
psychedelicious
2025-08-26 18:43:08 +10:00
committed by Mary Hipp Rogers
parent 47ffe365bc
commit 82893804ff
23 changed files with 247 additions and 111 deletions

View File

@@ -810,7 +810,11 @@
"vae": "VAE",
"width": "Width",
"workflow": "Workflow",
"canvasV2Metadata": "Canvas Layers"
"canvasV2Metadata": "Canvas Layers",
"videoModel": "Model",
"videoDuration": "Duration",
"videoAspectRatio": "Aspect Ratio",
"videoResolution": "Resolution"
},
"modelManager": {
"active": "active",

View File

@@ -2,7 +2,7 @@ import { useAppSelector } from 'app/store/storeHooks';
import { useIsRegionFocused } from 'common/hooks/focus';
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
import { useLoadWorkflow } from 'features/gallery/hooks/useLoadWorkflow';
import { useRecallAll } from 'features/gallery/hooks/useRecallAll';
import { useRecallAll } from 'features/gallery/hooks/useRecallAllImageMetadata';
import { useRecallDimensions } from 'features/gallery/hooks/useRecallDimensions';
import { useRecallPrompts } from 'features/gallery/hooks/useRecallPrompts';
import { useRecallRemix } from 'features/gallery/hooks/useRecallRemix';

View File

@@ -118,7 +118,7 @@ export const useStudioInitAction = (action?: StudioInitAction) => {
const metadata = getImageMetadataResult.value;
store.dispatch(canvasReset());
// This shows a toast
await MetadataUtils.recallAll(metadata, store);
await MetadataUtils.recallAllImageMetadata(metadata, store);
},
[store, t]
);

View File

@@ -32,7 +32,7 @@ import {
} from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
import { simplifyFlatNumbersArray } from 'features/controlLayers/util/simplify';
import { isMainModelBase, zModelIdentifierField } from 'features/nodes/types/common';
import { API_BASE_MODELS, VIDEO_BASE_MODELS } from 'features/parameters/types/constants';
import { API_BASE_MODELS } from 'features/parameters/types/constants';
import { getGridSize, getIsSizeOptimal, getOptimalDimension } from 'features/parameters/util/optimalDimension';
import type { IRect } from 'konva/lib/types';
import type { UndoableOptions } from 'redux-undo';

View File

@@ -501,6 +501,10 @@ export const RUNWAY_ASPECT_RATIOS: Record<RunwayAspectRatio, Dimensions> = {
'21:9': { width: 1584, height: 672 },
};
export const zVideoAspectRatio = z.union([zVeo3AspectRatioID, zRunwayAspectRatioID]);
export type VideoAspectRatio = z.infer<typeof zVideoAspectRatio>;
export const isVideoAspectRatio = (v: unknown): v is VideoAspectRatio => zVideoAspectRatio.safeParse(v).success;
export const zVeo3Resolution = z.enum(['720p', '1080p']);
export type Veo3Resolution = z.infer<typeof zVeo3Resolution>;
export const isVeo3Resolution = (v: unknown): v is Veo3Resolution => zVeo3Resolution.safeParse(v).success;
@@ -513,6 +517,9 @@ export const zRunwayResolution = z.enum(['720p']);
export type RunwayResolution = z.infer<typeof zRunwayResolution>;
export const isRunwayResolution = (v: unknown): v is RunwayResolution => zRunwayResolution.safeParse(v).success;
export const zVideoResolution = z.union([zVeo3Resolution, zRunwayResolution]);
export type VideoResolution = z.infer<typeof zVideoResolution>;
const zAspectRatioConfig = z.object({
id: zAspectRatioID,
value: z.number().gt(0),
@@ -541,6 +548,9 @@ export const RUNWAY_DURATIONS: Record<RunwayDuration, string> = {
'10': '10 seconds',
};
export const zVideoDuration = z.union([zVeo3DurationID, zRunwayDurationID]);
export type VideoDuration = z.infer<typeof zVideoDuration>;
const zBboxState = z.object({
rect: z.object({
x: z.number().int(),

View File

@@ -1,7 +1,7 @@
import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library';
import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu';
import { useItemDTOContext } from 'features/gallery/contexts/ItemDTOContext';
import { useRecallAll } from 'features/gallery/hooks/useRecallAll';
import { useRecallAll } from 'features/gallery/hooks/useRecallAllImageMetadata';
import { useRecallCLIPSkip } from 'features/gallery/hooks/useRecallCLIPSkip';
import { useRecallDimensions } from 'features/gallery/hooks/useRecallDimensions';
import { useRecallPrompts } from 'features/gallery/hooks/useRecallPrompts';

View File

@@ -9,10 +9,11 @@ import type {
UnrecallableMetadataHandler,
} from 'features/metadata/parsing';
import {
MetadataHandlers,
ImageMetadataHandlers,
useCollectionMetadataDatum,
useSingleMetadataDatum,
useUnrecallableMetadataDatum,
VideoMetadataHandlers,
} from 'features/metadata/parsing';
import { memo, useCallback } from 'react';
import { PiArrowBendUpLeftBold } from 'react-icons/pi';
@@ -21,7 +22,7 @@ type Props = {
metadata?: unknown;
};
const ImageMetadataActions = (props: Props) => {
export const ImageMetadataActions = memo((props: Props) => {
const { metadata } = props;
if (!metadata || Object.keys(metadata).length === 0) {
@@ -30,38 +31,60 @@ const ImageMetadataActions = (props: Props) => {
return (
<Flex flexDir="column" ps={8}>
<UnrecallableMetadataDatum metadata={metadata} handler={MetadataHandlers.GenerationMode} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.PositivePrompt} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.NegativePrompt} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.MainModel} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.VAEModel} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.Width} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.Height} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.Seed} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.Steps} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.Scheduler} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.CLIPSkip} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.CFGScale} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.CFGRescaleMultiplier} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.Guidance} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.DenoisingStrength} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.SeamlessX} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.SeamlessY} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.RefinerModel} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.RefinerCFGScale} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.RefinerPositiveAestheticScore} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.RefinerNegativeAestheticScore} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.RefinerScheduler} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.RefinerDenoisingStart} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.RefinerSteps} />
<SingleMetadataDatum metadata={metadata} handler={MetadataHandlers.CanvasLayers} />
<CollectionMetadataDatum metadata={metadata} handler={MetadataHandlers.RefImages} />
<CollectionMetadataDatum metadata={metadata} handler={MetadataHandlers.LoRAs} />
<UnrecallableMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.GenerationMode} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.PositivePrompt} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.NegativePrompt} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.MainModel} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.VAEModel} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.Width} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.Height} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.Seed} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.Steps} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.Scheduler} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.CLIPSkip} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.CFGScale} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.CFGRescaleMultiplier} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.Guidance} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.DenoisingStrength} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.SeamlessX} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.SeamlessY} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.RefinerModel} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.RefinerCFGScale} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.RefinerPositiveAestheticScore} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.RefinerNegativeAestheticScore} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.RefinerScheduler} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.RefinerDenoisingStart} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.RefinerSteps} />
<SingleMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.CanvasLayers} />
<CollectionMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.RefImages} />
<CollectionMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.LoRAs} />
</Flex>
);
};
});
export default memo(ImageMetadataActions);
ImageMetadataActions.displayName = 'ImageMetadataActions';
export const VideoMetadataActions = memo((props: Props) => {
const { metadata } = props;
if (!metadata || Object.keys(metadata).length === 0) {
return null;
}
return (
<Flex flexDir="column" ps={8}>
<UnrecallableMetadataDatum metadata={metadata} handler={VideoMetadataHandlers.GenerationMode} />
<SingleMetadataDatum metadata={metadata} handler={VideoMetadataHandlers.PositivePrompt} />
<SingleMetadataDatum metadata={metadata} handler={VideoMetadataHandlers.Seed} />
<SingleMetadataDatum metadata={metadata} handler={VideoMetadataHandlers.VideoAspectRatio} />
<SingleMetadataDatum metadata={metadata} handler={VideoMetadataHandlers.VideoDuration} />
<SingleMetadataDatum metadata={metadata} handler={VideoMetadataHandlers.VideoResolution} />
<SingleMetadataDatum metadata={metadata} handler={VideoMetadataHandlers.VideoModel} />
</Flex>
);
});
VideoMetadataActions.displayName = 'VideoMetadataActions';
export const UnrecallableMetadataDatum = typedMemo(
<T,>({ metadata, handler }: { metadata: unknown; handler: UnrecallableMetadataHandler<T> }) => {

View File

@@ -2,14 +2,14 @@ import { ExternalLink, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@in
import { IAINoContentFallback, IAINoContentFallbackWithSpinner } from 'common/components/IAIImageFallback';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import ImageMetadataGraphTabContent from 'features/gallery/components/ImageMetadataViewer/ImageMetadataGraphTabContent';
import { MetadataHandlers } from 'features/metadata/parsing';
import { ImageMetadataHandlers } from 'features/metadata/parsing';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
import type { ImageDTO } from 'services/api/types';
import DataViewer from './DataViewer';
import ImageMetadataActions, { UnrecallableMetadataDatum } from './ImageMetadataActions';
import { ImageMetadataActions, UnrecallableMetadataDatum } from './ImageMetadataActions';
import ImageMetadataWorkflowTabContent from './ImageMetadataWorkflowTabContent';
type ImageMetadataViewerProps = {
@@ -39,7 +39,7 @@ const ImageMetadataViewer = ({ image }: ImageMetadataViewerProps) => {
overflow="hidden"
>
<ExternalLink href={image.image_url} label={image.image_name} />
<UnrecallableMetadataDatum metadata={metadata} handler={MetadataHandlers.CreatedBy} />
<UnrecallableMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.CreatedBy} />
<Tabs variant="line" isLazy={true} display="flex" flexDir="column" w="full" h="full">
<TabList>

View File

@@ -1,14 +1,14 @@
import { ExternalLink, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
import { IAINoContentFallback, IAINoContentFallbackWithSpinner } from 'common/components/IAIImageFallback';
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
import { MetadataHandlers } from 'features/metadata/parsing';
import { ImageMetadataHandlers } from 'features/metadata/parsing';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { useDebouncedVideoMetadata } from 'services/api/hooks/useDebouncedMetadata';
import type { VideoDTO } from 'services/api/types';
import DataViewer from './DataViewer';
import ImageMetadataActions, { UnrecallableMetadataDatum } from './ImageMetadataActions';
import { UnrecallableMetadataDatum, VideoMetadataActions } from './ImageMetadataActions';
type VideoMetadataViewerProps = {
video: VideoDTO;
@@ -32,15 +32,13 @@ const VideoMetadataViewer = ({ video }: VideoMetadataViewerProps) => {
overflow="hidden"
>
<ExternalLink href={video.video_url} label={video.video_id} />
<UnrecallableMetadataDatum metadata={metadata} handler={MetadataHandlers.CreatedBy} />
<UnrecallableMetadataDatum metadata={metadata} handler={ImageMetadataHandlers.CreatedBy} />
<Tabs variant="line" isLazy={true} display="flex" flexDir="column" w="full" h="full">
<TabList>
<Tab>{t('metadata.recallParameters')}</Tab>
<Tab>{t('metadata.metadata')}</Tab>
<Tab>{t('metadata.imageDetails')}</Tab>
<Tab>{t('metadata.workflow')}</Tab>
<Tab>{t('nodes.graph')}</Tab>
</TabList>
<TabPanels>
@@ -48,7 +46,7 @@ const VideoMetadataViewer = ({ video }: VideoMetadataViewerProps) => {
{isLoading && <IAINoContentFallbackWithSpinner label="Loading metadata..." />}
{metadata && !isLoading && (
<ScrollableContent>
<ImageMetadataActions metadata={metadata} />
<VideoMetadataActions metadata={metadata} />
</ScrollableContent>
)}
{!metadata && !isLoading && <IAINoContentFallback label={t('metadata.noRecallParameters')} />}
@@ -75,7 +73,7 @@ const VideoMetadataViewer = ({ video }: VideoMetadataViewerProps) => {
<IAINoContentFallback label={t('metadata.noImageDetails')} />
)}
</TabPanel>
{/* <TabPanel>
{/* <TabPanel>
<ImageMetadataWorkflowTabContent image={image} />
</TabPanel>
<TabPanel>

View File

@@ -5,7 +5,7 @@ import SingleSelectionMenuItems from 'features/gallery/components/ContextMenu/Si
import { useDeleteImage } from 'features/gallery/hooks/useDeleteImage';
import { useEditImage } from 'features/gallery/hooks/useEditImage';
import { useLoadWorkflow } from 'features/gallery/hooks/useLoadWorkflow';
import { useRecallAll } from 'features/gallery/hooks/useRecallAll';
import { useRecallAll } from 'features/gallery/hooks/useRecallAllImageMetadata';
import { useRecallDimensions } from 'features/gallery/hooks/useRecallDimensions';
import { useRecallPrompts } from 'features/gallery/hooks/useRecallPrompts';
import { useRecallRemix } from 'features/gallery/hooks/useRecallRemix';

View File

@@ -1,5 +1,5 @@
import { useAppStore } from 'app/store/storeHooks';
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
import { ImageMetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
@@ -13,7 +13,7 @@ export const useCreateStylePresetFromMetadata = (imageDTO?: ImageDTO | null) =>
useEffect(() => {
MetadataUtils.hasMetadataByHandlers({
handlers: [MetadataHandlers.PositivePrompt, MetadataHandlers.NegativePrompt],
handlers: [ImageMetadataHandlers.PositivePrompt, ImageMetadataHandlers.NegativePrompt],
metadata,
store,
require: 'some',
@@ -51,12 +51,12 @@ export const useCreateStylePresetFromMetadata = (imageDTO?: ImageDTO | null) =>
let negativePrompt: string;
try {
positivePrompt = await MetadataHandlers.PositivePrompt.parse(metadata, store);
positivePrompt = await ImageMetadataHandlers.PositivePrompt.parse(metadata, store);
} catch {
positivePrompt = '';
}
try {
negativePrompt = (await MetadataHandlers.NegativePrompt.parse(metadata, store)) ?? '';
negativePrompt = (await ImageMetadataHandlers.NegativePrompt.parse(metadata, store)) ?? '';
} catch {
negativePrompt = '';
}

View File

@@ -1,6 +1,6 @@
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
import { ImageMetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { useCallback, useMemo } from 'react';
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
@@ -34,7 +34,7 @@ export const useRecallAll = (imageDTO: ImageDTO) => {
const handlersToSkip = useMemo(() => {
if (tab === 'canvas' && isStaging) {
// When we are staging and on canvas, the bbox is locked - we cannot recall width and height
return [MetadataHandlers.Width, MetadataHandlers.Height];
return [ImageMetadataHandlers.Width, ImageMetadataHandlers.Height];
}
return undefined;
}, [isStaging, tab]);
@@ -46,7 +46,7 @@ export const useRecallAll = (imageDTO: ImageDTO) => {
if (!isEnabled) {
return;
}
MetadataUtils.recallAll(metadata, store, handlersToSkip);
MetadataUtils.recallAllImageMetadata(metadata, store, handlersToSkip);
clearStylePreset();
}, [metadata, isEnabled, store, handlersToSkip, clearStylePreset]);

View File

@@ -1,6 +1,6 @@
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { selectHasModelCLIPSkip } from 'features/controlLayers/store/paramsSlice';
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
import { ImageMetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import type { TabName } from 'features/ui/store/uiTypes';
import { useCallback, useEffect, useMemo, useState } from 'react';
@@ -20,7 +20,7 @@ export const useRecallCLIPSkip = (imageDTO: ImageDTO) => {
useEffect(() => {
const parse = async () => {
try {
await MetadataHandlers.CLIPSkip.parse(metadata, store);
await ImageMetadataHandlers.CLIPSkip.parse(metadata, store);
setHasCLIPSkip(true);
} catch {
setHasCLIPSkip(false);
@@ -62,7 +62,7 @@ export const useRecallCLIPSkip = (imageDTO: ImageDTO) => {
if (!isEnabled) {
return;
}
MetadataUtils.recallByHandler({ metadata, handler: MetadataHandlers.CLIPSkip, store });
MetadataUtils.recallByHandler({ metadata, handler: ImageMetadataHandlers.CLIPSkip, store });
}, [metadata, isEnabled, store]);
return {

View File

@@ -26,7 +26,7 @@ export const useRecallDimensions = (imageDTO: ImageDTO) => {
if (!isEnabled) {
return;
}
MetadataUtils.recallDimensions(imageDTO, store);
MetadataUtils.recallImageDimensions(imageDTO, store);
}, [isEnabled, imageDTO, store]);
return {

View File

@@ -1,5 +1,5 @@
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
import { ImageMetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import type { TabName } from 'features/ui/store/uiTypes';
import { useCallback, useEffect, useMemo, useState } from 'react';
@@ -22,7 +22,7 @@ export const useRecallPrompts = (imageDTO: ImageDTO) => {
const parse = async () => {
try {
const result = await MetadataUtils.hasMetadataByHandlers({
handlers: [MetadataHandlers.PositivePrompt, MetadataHandlers.NegativePrompt],
handlers: [ImageMetadataHandlers.PositivePrompt, ImageMetadataHandlers.NegativePrompt],
metadata,
store,
require: 'some',
@@ -59,7 +59,7 @@ export const useRecallPrompts = (imageDTO: ImageDTO) => {
if (!isEnabled) {
return;
}
MetadataUtils.recallPrompts(metadata, store);
MetadataUtils.recallImagePrompts(metadata, store);
clearStylePreset();
}, [metadata, isEnabled, store, clearStylePreset]);

View File

@@ -1,6 +1,6 @@
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { useCanvasIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice';
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
import { ImageMetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import { useCallback, useMemo } from 'react';
import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
@@ -34,10 +34,10 @@ export const useRecallRemix = (imageDTO: ImageDTO) => {
const handlersToSkip = useMemo(() => {
// Remix always skips the seed handler
const _handlersToSkip = [MetadataHandlers.Seed];
const _handlersToSkip = [ImageMetadataHandlers.Seed];
if (tab === 'canvas' && isStaging) {
// When we are staging and on canvas, the bbox is locked - we cannot recall width and height
_handlersToSkip.push(MetadataHandlers.Width, MetadataHandlers.Height);
_handlersToSkip.push(ImageMetadataHandlers.Width, ImageMetadataHandlers.Height);
}
return _handlersToSkip;
}, [isStaging, tab]);
@@ -49,7 +49,7 @@ export const useRecallRemix = (imageDTO: ImageDTO) => {
if (!isEnabled) {
return;
}
MetadataUtils.recallAll(metadata, store, handlersToSkip);
MetadataUtils.recallAllImageMetadata(metadata, store, handlersToSkip);
clearStylePreset();
}, [metadata, isEnabled, store, handlersToSkip, clearStylePreset]);

View File

@@ -1,5 +1,5 @@
import { useAppSelector, useAppStore } from 'app/store/storeHooks';
import { MetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
import { ImageMetadataHandlers, MetadataUtils } from 'features/metadata/parsing';
import { selectActiveTab } from 'features/ui/store/uiSelectors';
import type { TabName } from 'features/ui/store/uiTypes';
import { useCallback, useEffect, useMemo, useState } from 'react';
@@ -18,7 +18,7 @@ export const useRecallSeed = (imageDTO: ImageDTO) => {
useEffect(() => {
const parse = async () => {
try {
await MetadataHandlers.Seed.parse(metadata, store);
await ImageMetadataHandlers.Seed.parse(metadata, store);
setHasSeed(true);
} catch {
setHasSeed(false);
@@ -55,7 +55,7 @@ export const useRecallSeed = (imageDTO: ImageDTO) => {
if (!isEnabled) {
return;
}
MetadataUtils.recallByHandler({ metadata, handler: MetadataHandlers.Seed, store });
MetadataUtils.recallByHandler({ metadata, handler: ImageMetadataHandlers.Seed, store });
}, [metadata, isEnabled, store]);
return {

View File

@@ -33,12 +33,32 @@ import {
widthChanged,
} from 'features/controlLayers/store/paramsSlice';
import { refImagesRecalled } from 'features/controlLayers/store/refImagesSlice';
import type { CanvasMetadata, LoRA, RefImageState } from 'features/controlLayers/store/types';
import { zCanvasMetadata, zCanvasReferenceImageState_OLD, zRefImageState } from 'features/controlLayers/store/types';
import type {
CanvasMetadata,
LoRA,
RefImageState,
VideoAspectRatio as ParameterVideoAspectRatio,
VideoDuration as ParameterVideoDuration,
VideoResolution as ParameterVideoResolution,
} from 'features/controlLayers/store/types';
import {
zCanvasMetadata,
zCanvasReferenceImageState_OLD,
zRefImageState,
zVideoAspectRatio,
zVideoDuration,
zVideoResolution,
} from 'features/controlLayers/store/types';
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 {
videoAspectRatioChanged,
videoDurationChanged,
videoModelChanged,
videoResolutionChanged,
} from 'features/parameters/store/videoSlice';
import type {
ParameterCFGRescaleMultiplier,
ParameterCFGScale,
@@ -694,6 +714,87 @@ const VAEModel: SingleMetadataHandler<ParameterVAEModel> = {
};
//#endregion VAEModel
//#region VideoModel
const VideoModel: SingleMetadataHandler<ModelIdentifierField> = {
[SingleMetadataKey]: true,
type: 'VideoModel',
parse: async (metadata, store) => {
const raw = getProperty(metadata, 'model');
const parsed = await parseModelIdentifier(raw, store, 'video');
assert(parsed.type === 'video');
return Promise.resolve(parsed);
},
recall: (value, store) => {
store.dispatch(videoModelChanged({ videoModel: value }));
},
i18nKey: 'metadata.videoModel',
LabelComponent: MetadataLabel,
ValueComponent: ({ value }: SingleMetadataValueProps<ModelIdentifierField>) => (
<MetadataPrimitiveValue value={`${value.name} (${value.base.toUpperCase()})`} />
),
};
//#endregion VideoModel
//#region VideoDuration
const VideoDuration: SingleMetadataHandler<ParameterVideoDuration> = {
[SingleMetadataKey]: true,
type: 'VideoDuration',
parse: (metadata) => {
const raw = getProperty(metadata, 'duration');
const parsed = zVideoDuration.parse(raw);
return Promise.resolve(parsed);
},
recall: (value, store) => {
store.dispatch(videoDurationChanged(value));
},
i18nKey: 'metadata.videoDuration',
LabelComponent: MetadataLabel,
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterVideoDuration>) => (
<MetadataPrimitiveValue value={value} />
),
};
//#endregion VideoDuration
//#region VideoResolution
const VideoResolution: SingleMetadataHandler<ParameterVideoResolution> = {
[SingleMetadataKey]: true,
type: 'VideoResolution',
parse: (metadata) => {
const raw = getProperty(metadata, 'resolution');
const parsed = zVideoResolution.parse(raw);
return Promise.resolve(parsed);
},
recall: (value, store) => {
store.dispatch(videoResolutionChanged(value));
},
i18nKey: 'metadata.videoResolution',
LabelComponent: MetadataLabel,
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterVideoResolution>) => (
<MetadataPrimitiveValue value={value} />
),
};
//#endregion VideoResolution
//#region VideoAspectRatio
const VideoAspectRatio: SingleMetadataHandler<ParameterVideoAspectRatio> = {
[SingleMetadataKey]: true,
type: 'VideoAspectRatio',
parse: (metadata) => {
const raw = getProperty(metadata, 'aspect_ratio');
const parsed = zVideoAspectRatio.parse(raw);
return Promise.resolve(parsed);
},
recall: (value, store) => {
store.dispatch(videoAspectRatioChanged(value));
},
i18nKey: 'metadata.videoAspectRatio',
LabelComponent: MetadataLabel,
ValueComponent: ({ value }: SingleMetadataValueProps<ParameterVideoAspectRatio>) => (
<MetadataPrimitiveValue value={value} />
),
};
//#endregion VideoAspectRatio
//#region LoRAs
const LoRAs: CollectionMetadataHandler<LoRA[]> = {
[CollectionMetadataKey]: true,
@@ -897,7 +998,7 @@ const RefImages: CollectionMetadataHandler<RefImageState[]> = {
};
//#endregion RefImages
export const MetadataHandlers = {
export const ImageMetadataHandlers = {
CreatedBy,
GenerationMode,
PositivePrompt,
@@ -938,6 +1039,17 @@ export const MetadataHandlers = {
// ipAdapterToIPAdapterLayer: parseIPAdapterToIPAdapterLayer,
} as const;
export const VideoMetadataHandlers = {
CreatedBy,
GenerationMode,
PositivePrompt,
VideoModel,
Seed,
VideoAspectRatio,
VideoDuration,
VideoResolution,
};
const successToast = (parameter: string) => {
toast({
id: 'PARAMETER_SET',
@@ -1007,9 +1119,9 @@ const recallByHandlers = async (arg: {
// model is recalled first, so it doesn't accidentally override the width and height. This is the only known case
// where the order of recall matters.
const sortedHandlers = filteredHandlers.sort((a, b) => {
if (a === MetadataHandlers.MainModel) {
if (a === ImageMetadataHandlers.MainModel) {
return -1; // MainModel should be recalled first
} else if (b === MetadataHandlers.MainModel) {
} else if (b === ImageMetadataHandlers.MainModel) {
return 1; // MainModel should be recalled first
} else {
return 0; // Keep the original order for other handlers
@@ -1045,10 +1157,10 @@ const recallByHandlers = async (arg: {
return recalled;
};
const recallPrompts = async (metadata: unknown, store: AppStore) => {
const recallImagePrompts = async (metadata: unknown, store: AppStore) => {
const recalled = await recallByHandlers({
metadata,
handlers: [MetadataHandlers.PositivePrompt, MetadataHandlers.NegativePrompt],
handlers: [ImageMetadataHandlers.PositivePrompt, ImageMetadataHandlers.NegativePrompt],
store,
silent: true,
});
@@ -1079,10 +1191,10 @@ const hasMetadataByHandlers = async (arg: {
return true;
};
const recallDimensions = async (metadata: unknown, store: AppStore) => {
const recallImageDimensions = async (metadata: unknown, store: AppStore) => {
const recalled = await recallByHandlers({
metadata,
handlers: [MetadataHandlers.Width, MetadataHandlers.Height],
handlers: [ImageMetadataHandlers.Width, ImageMetadataHandlers.Height],
store,
silent: true,
});
@@ -1091,12 +1203,12 @@ const recallDimensions = async (metadata: unknown, store: AppStore) => {
}
};
const recallAll = async (
const recallAllImageMetadata = async (
metadata: unknown,
store: AppStore,
skip?: (SingleMetadataHandler<any> | CollectionMetadataHandler<any[]>)[]
) => {
const handlers = Object.values(MetadataHandlers).filter(
const handlers = Object.values(ImageMetadataHandlers).filter(
(handler) => isSingleMetadataHandler(handler) || isCollectionMetadataHandler(handler)
);
await recallByHandlers({
@@ -1111,9 +1223,9 @@ export const MetadataUtils = {
hasMetadataByHandlers,
recallByHandler,
recallByHandlers,
recallAll,
recallPrompts,
recallDimensions,
recallAllImageMetadata,
recallImagePrompts,
recallImageDimensions,
} as const;
export function useSingleMetadataDatum<T>(metadata: unknown, handler: SingleMetadataHandler<T>) {

View File

@@ -12,7 +12,6 @@ import {
selectVideoSlice,
} from 'features/parameters/store/videoSlice';
import { t } from 'i18next';
import type { VideoApiModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
const log = logger('system');

View File

@@ -12,7 +12,6 @@ import {
selectVideoSlice,
} from 'features/parameters/store/videoSlice';
import { t } from 'i18next';
import type { VideoApiModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
const log = logger('system');

View File

@@ -2,12 +2,7 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library';
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import {
isAspectRatioID,
zAspectRatioID,
zRunwayAspectRatioID,
zVeo3AspectRatioID,
} from 'features/controlLayers/store/types';
import { isVideoAspectRatio, zRunwayAspectRatioID, zVeo3AspectRatioID } from 'features/controlLayers/store/types';
import {
selectIsRunway,
selectIsVeo3,
@@ -31,12 +26,12 @@ export const VideoDimensionsAspectRatioSelect = memo(() => {
return zRunwayAspectRatioID.options.map((o) => ({ label: o, value: o }));
}
// All other models
return zAspectRatioID.options.map((o) => ({ label: o, value: o }));
return [];
}, [isVeo3, isRunway]);
const onChange = useCallback<ComboboxOnChange>(
(v) => {
if (!isAspectRatioID(v?.value)) {
if (!isVideoAspectRatio(v?.value)) {
return;
}
dispatch(videoAspectRatioChanged(v.value));

View File

@@ -13,7 +13,7 @@ export const VideoDimensionsPreview = memo(() => {
const currentVideoDimensions = useCurrentVideoDimensions();
const previewBoxSize = useMemo(() => {
if (!dims || aspectRatio === 'Free') {
if (!dims) {
return { width: 0, height: 0 };
}

View File

@@ -4,12 +4,10 @@ import type { RootState } from 'app/store/store';
import type { SliceConfig } from 'app/store/types';
import { isPlainObject } from 'es-toolkit';
import type {
AspectRatioID,
ImageWithDims,
RunwayDuration,
RunwayResolution,
Veo3Duration,
Veo3Resolution,
VideoAspectRatio,
VideoDuration,
VideoResolution,
} from 'features/controlLayers/store/types';
import {
isRunwayAspectRatioID,
@@ -18,17 +16,15 @@ import {
isVeo3AspectRatioID,
isVeo3DurationID,
isVeo3Resolution,
zAspectRatioID,
zImageWithDims,
zRunwayDurationID,
zRunwayResolution,
zVeo3DurationID,
zVeo3Resolution,
zVideoAspectRatio,
zVideoDuration,
zVideoResolution,
} from 'features/controlLayers/store/types';
import type { ModelIdentifierField, VideoField } from 'features/nodes/types/common';
import { zModelIdentifierField, zVideoField } from 'features/nodes/types/common';
import { modelConfigsAdapterSelectors, selectModelConfigsQuery } from 'services/api/endpoints/models';
import { isVideoModelConfig, type RunwayModelConfig, type Veo3ModelConfig } from 'services/api/types';
import { isVideoModelConfig } from 'services/api/types';
import { assert } from 'tsafe';
import z from 'zod';
@@ -37,9 +33,9 @@ const zVideoState = z.object({
startingFrameImage: zImageWithDims.nullable(),
generatedVideo: zVideoField.nullable(),
videoModel: zModelIdentifierField.nullable(),
videoResolution: zVeo3Resolution.or(zRunwayResolution),
videoDuration: zVeo3DurationID.or(zRunwayDurationID),
videoAspectRatio: zAspectRatioID,
videoResolution: zVideoResolution,
videoDuration: zVideoDuration,
videoAspectRatio: zVideoAspectRatio,
});
export type VideoState = z.infer<typeof zVideoState>;
@@ -97,15 +93,15 @@ const slice = createSlice({
}
},
videoResolutionChanged: (state, action: PayloadAction<Veo3Resolution | RunwayResolution>) => {
videoResolutionChanged: (state, action: PayloadAction<VideoResolution>) => {
state.videoResolution = action.payload;
},
videoDurationChanged: (state, action: PayloadAction<Veo3Duration | RunwayDuration>) => {
videoDurationChanged: (state, action: PayloadAction<VideoDuration>) => {
state.videoDuration = action.payload;
},
videoAspectRatioChanged: (state, action: PayloadAction<AspectRatioID>) => {
videoAspectRatioChanged: (state, action: PayloadAction<VideoAspectRatio>) => {
state.videoAspectRatio = action.payload;
},
},
@@ -136,7 +132,7 @@ export const videoSliceConfig: SliceConfig<typeof slice> = {
};
export const selectVideoSlice = (state: RootState) => state.video;
const createVideoSelector = <T,>(selector: Selector<VideoState, T>) => createSelector(selectVideoSlice, selector);
const createVideoSelector = <T>(selector: Selector<VideoState, T>) => createSelector(selectVideoSlice, selector);
export const selectStartingFrameImage = createVideoSelector((video) => video.startingFrameImage);
export const selectGeneratedVideo = createVideoSelector((video) => video.generatedVideo);