mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
video metadata support
This commit is contained in:
committed by
psychedelicious
parent
2b688ed855
commit
32e2d176de
@@ -0,0 +1,90 @@
|
||||
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 { 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';
|
||||
|
||||
type VideoMetadataViewerProps = {
|
||||
video: VideoDTO;
|
||||
};
|
||||
|
||||
const VideoMetadataViewer = ({ video }: VideoMetadataViewerProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { metadata, isLoading } = useDebouncedVideoMetadata(video.video_id);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
layerStyle="first"
|
||||
padding={4}
|
||||
gap={1}
|
||||
flexDirection="column"
|
||||
width="full"
|
||||
height="full"
|
||||
borderRadius="base"
|
||||
position="absolute"
|
||||
overflow="hidden"
|
||||
>
|
||||
<ExternalLink href={video.video_url} label={video.video_id} />
|
||||
<UnrecallableMetadataDatum metadata={metadata} handler={MetadataHandlers.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>
|
||||
<TabPanel>
|
||||
{isLoading && <IAINoContentFallbackWithSpinner label="Loading metadata..." />}
|
||||
{metadata && !isLoading && (
|
||||
<ScrollableContent>
|
||||
<ImageMetadataActions metadata={metadata} />
|
||||
</ScrollableContent>
|
||||
)}
|
||||
{!metadata && !isLoading && <IAINoContentFallback label={t('metadata.noRecallParameters')} />}
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
{metadata ? (
|
||||
<DataViewer
|
||||
fileName={`${video.video_id.replace('.png', '')}_metadata`}
|
||||
data={metadata}
|
||||
label={t('metadata.metadata')}
|
||||
/>
|
||||
) : (
|
||||
<IAINoContentFallback label={t('metadata.noMetaData')} />
|
||||
)}
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
{video ? (
|
||||
<DataViewer
|
||||
fileName={`${video.video_id.replace('.png', '')}_details`}
|
||||
data={video}
|
||||
label={t('metadata.imageDetails')}
|
||||
/>
|
||||
) : (
|
||||
<IAINoContentFallback label={t('metadata.noImageDetails')} />
|
||||
)}
|
||||
</TabPanel>
|
||||
{/* <TabPanel>
|
||||
<ImageMetadataWorkflowTabContent image={image} />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ImageMetadataGraphTabContent image={image} />
|
||||
</TabPanel> */}
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(VideoMetadataViewer);
|
||||
@@ -5,7 +5,7 @@ import { CanvasAlertsInvocationProgress } from 'features/controlLayers/component
|
||||
import { DndImage } from 'features/dnd/DndImage';
|
||||
import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
|
||||
import NextPrevItemButtons from 'features/gallery/components/NextPrevItemButtons';
|
||||
import { selectShouldShowImageDetails, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
|
||||
import { selectShouldShowItemDetails, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
|
||||
import type { AnimationProps } from 'framer-motion';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { memo, useCallback, useRef, useState } from 'react';
|
||||
@@ -17,7 +17,7 @@ import { ProgressImage } from './ProgressImage2';
|
||||
import { ProgressIndicator } from './ProgressIndicator2';
|
||||
|
||||
export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO: ImageDTO | null }) => {
|
||||
const shouldShowImageDetails = useAppSelector(selectShouldShowImageDetails);
|
||||
const shouldShowItemDetails = useAppSelector(selectShouldShowItemDetails);
|
||||
const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer);
|
||||
const { onLoadImage, $progressEvent, $progressImage } = useImageViewerContext();
|
||||
const progressEvent = useStore($progressEvent);
|
||||
@@ -65,7 +65,7 @@ export const CurrentImagePreview = memo(({ imageDTO }: { imageDTO: ImageDTO | nu
|
||||
<Flex flexDir="column" gap={2} position="absolute" top={0} insetInlineStart={0} alignItems="flex-start">
|
||||
<CanvasAlertsInvocationProgress />
|
||||
</Flex>
|
||||
{shouldShowImageDetails && imageDTO && !withProgress && (
|
||||
{shouldShowItemDetails && imageDTO && !withProgress && (
|
||||
<Box position="absolute" opacity={0.8} top={0} width="full" height="full" borderRadius="base">
|
||||
<ImageMetadataViewer image={imageDTO} />
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import VideoMetadataViewer from 'features/gallery/components/ImageMetadataViewer/VideoMetadataViewer';
|
||||
import NextPrevItemButtons from 'features/gallery/components/NextPrevItemButtons';
|
||||
import { selectShouldShowItemDetails } from 'features/ui/store/uiSelectors';
|
||||
import { VideoPlayer } from 'features/video/components/VideoPlayer';
|
||||
import type { AnimationProps } from 'framer-motion';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { memo, useCallback, useRef, useState } from 'react';
|
||||
import type { VideoDTO } from 'services/api/types';
|
||||
|
||||
import { NoContentForViewer } from './NoContentForViewer';
|
||||
|
||||
export const CurrentVideoPreview = memo(({ videoDTO }: { videoDTO: VideoDTO | null }) => {
|
||||
const shouldShowItemDetails = useAppSelector(selectShouldShowItemDetails);
|
||||
|
||||
// Show and hide the next/prev buttons on mouse move
|
||||
const [shouldShowNextPrevButtons, setShouldShowNextPrevButtons] = useState<boolean>(false);
|
||||
const timeoutId = useRef(0);
|
||||
const onMouseOver = useCallback(() => {
|
||||
setShouldShowNextPrevButtons(true);
|
||||
window.clearTimeout(timeoutId.current);
|
||||
}, []);
|
||||
const onMouseOut = useCallback(() => {
|
||||
timeoutId.current = window.setTimeout(() => {
|
||||
setShouldShowNextPrevButtons(false);
|
||||
}, 500);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
onMouseOver={onMouseOver}
|
||||
onMouseOut={onMouseOut}
|
||||
width="full"
|
||||
height="full"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
position="relative"
|
||||
>
|
||||
{videoDTO && videoDTO.video_url && (
|
||||
<Flex w="full" h="full" position="absolute" alignItems="center" justifyContent="center">
|
||||
<VideoPlayer />
|
||||
</Flex>
|
||||
)}
|
||||
{!videoDTO && <NoContentForViewer />}
|
||||
{shouldShowItemDetails && videoDTO && (
|
||||
<Box position="absolute" opacity={0.8} top={0} width="full" height="full" borderRadius="base">
|
||||
<VideoMetadataViewer video={videoDTO} />
|
||||
</Box>
|
||||
)}
|
||||
<AnimatePresence>
|
||||
{shouldShowNextPrevButtons && videoDTO && (
|
||||
<Box
|
||||
as={motion.div}
|
||||
key="nextPrevButtons"
|
||||
initial={initial}
|
||||
animate={animateArrows}
|
||||
exit={exit}
|
||||
position="absolute"
|
||||
top={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
left={0}
|
||||
pointerEvents="none"
|
||||
>
|
||||
<NextPrevItemButtons />
|
||||
</Box>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
CurrentVideoPreview.displayName = 'CurrentVideoPreview';
|
||||
|
||||
const initial: AnimationProps['initial'] = {
|
||||
opacity: 0,
|
||||
};
|
||||
const animateArrows: AnimationProps['animate'] = {
|
||||
opacity: 1,
|
||||
transition: { duration: 0.07 },
|
||||
};
|
||||
const exit: AnimationProps['exit'] = {
|
||||
opacity: 0,
|
||||
transition: { duration: 0.07 },
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useImageDTO } from 'services/api/endpoints/images';
|
||||
|
||||
import { ViewerToolbar } from './ViewerToolbar';
|
||||
import { ImageViewerToolbar } from './ImageViewerToolbar';
|
||||
|
||||
const dndTargetData = setComparisonImageDndTarget.getData();
|
||||
|
||||
@@ -19,7 +19,7 @@ export const ImageViewer = memo(() => {
|
||||
const lastSelectedImageDTO = useImageDTO(lastSelectedItem?.type === 'image' ? lastSelectedItem.id : null);
|
||||
return (
|
||||
<Flex flexDir="column" w="full" h="full" overflow="hidden" gap={2} position="relative">
|
||||
<ViewerToolbar />
|
||||
<ImageViewerToolbar />
|
||||
<Divider />
|
||||
<Flex w="full" h="full" position="relative">
|
||||
<CurrentImagePreview imageDTO={lastSelectedImageDTO} />
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectImageToCompare, selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
|
||||
import { VideoPlayer } from 'features/video/components/VideoPlayer';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { ImageViewerContextProvider } from './context';
|
||||
import { ImageComparison } from './ImageComparison';
|
||||
import { ImageViewer } from './ImageViewer';
|
||||
import { VideoViewer } from './VideoViewer';
|
||||
|
||||
const selectIsComparing = createSelector(
|
||||
[selectLastSelectedItem, selectImageToCompare],
|
||||
@@ -20,7 +20,7 @@ export const ImageViewerPanel = memo(() => {
|
||||
return (
|
||||
<ImageViewerContextProvider>
|
||||
{!isComparing && lastSelectedItem?.type === 'image' && <ImageViewer />}
|
||||
{!isComparing && lastSelectedItem?.type === 'video' && <VideoPlayer />}
|
||||
{!isComparing && lastSelectedItem?.type === 'video' && <VideoViewer />}
|
||||
{isComparing && <ImageComparison />}
|
||||
</ImageViewerContextProvider>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useImageDTO } from 'services/api/endpoints/images';
|
||||
import { CurrentImageButtons } from './CurrentImageButtons';
|
||||
import { ToggleProgressButton } from './ToggleProgressButton';
|
||||
|
||||
export const ViewerToolbar = memo(() => {
|
||||
export const ImageViewerToolbar = memo(() => {
|
||||
const lastSelectedItem = useAppSelector(selectLastSelectedItem);
|
||||
const imageDTO = useImageDTO(lastSelectedItem?.id);
|
||||
|
||||
@@ -23,4 +23,4 @@ export const ViewerToolbar = memo(() => {
|
||||
);
|
||||
});
|
||||
|
||||
ViewerToolbar.displayName = 'ViewerToolbar';
|
||||
ImageViewerToolbar.displayName = 'ImageViewerToolbar';
|
||||
@@ -3,8 +3,8 @@ import { useStore } from '@nanostores/react';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
|
||||
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
|
||||
import { selectShouldShowImageDetails, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
|
||||
import { setShouldShowImageDetails } from 'features/ui/store/uiSlice';
|
||||
import { selectShouldShowItemDetails, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
|
||||
import { setShouldShowItemDetails } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiInfoBold } from 'react-icons/pi';
|
||||
@@ -19,19 +19,19 @@ export const ToggleMetadataViewerButton = memo(() => {
|
||||
|
||||
const isDisabledOverride = hasProgressImage && shouldShowProgressInViewer;
|
||||
|
||||
const shouldShowImageDetails = useAppSelector(selectShouldShowImageDetails);
|
||||
const shouldShowItemDetails = useAppSelector(selectShouldShowItemDetails);
|
||||
const imageDTO = useAppSelector(selectLastSelectedItem);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const toggleMetadataViewer = useCallback(() => {
|
||||
dispatch(setShouldShowImageDetails(!shouldShowImageDetails));
|
||||
}, [dispatch, shouldShowImageDetails]);
|
||||
dispatch(setShouldShowItemDetails(!shouldShowItemDetails));
|
||||
}, [dispatch, shouldShowItemDetails]);
|
||||
|
||||
useRegisteredHotkeys({
|
||||
id: 'toggleMetadata',
|
||||
category: 'viewer',
|
||||
callback: toggleMetadataViewer,
|
||||
dependencies: [imageDTO, shouldShowImageDetails],
|
||||
dependencies: [imageDTO, shouldShowItemDetails],
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -42,7 +42,7 @@ export const ToggleMetadataViewerButton = memo(() => {
|
||||
onClick={toggleMetadataViewer}
|
||||
variant="link"
|
||||
alignSelf="stretch"
|
||||
colorScheme={shouldShowImageDetails ? 'invokeBlue' : 'base'}
|
||||
colorScheme={shouldShowItemDetails ? 'invokeBlue' : 'base'}
|
||||
data-testid="toggle-show-metadata-button"
|
||||
isDisabled={isDisabledOverride}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Divider, Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
|
||||
import { memo } from 'react';
|
||||
import { useVideoDTO } from 'services/api/endpoints/videos';
|
||||
|
||||
import { CurrentVideoPreview } from './CurrentVideoPreview';
|
||||
import { VideoViewerToolbar } from './VideoViewerToolbar';
|
||||
|
||||
export const VideoViewer = memo(() => {
|
||||
const lastSelectedItem = useAppSelector(selectLastSelectedItem);
|
||||
const videoDTO = useVideoDTO(lastSelectedItem?.type === 'video' ? lastSelectedItem.id : null);
|
||||
return (
|
||||
<Flex flexDir="column" w="full" h="full" overflow="hidden" gap={2} position="relative">
|
||||
<VideoViewerToolbar />
|
||||
<Divider />
|
||||
<Flex w="full" h="full" position="relative">
|
||||
<CurrentVideoPreview videoDTO={videoDTO ?? null} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
VideoViewer.displayName = 'VideoViewer';
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
|
||||
import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
|
||||
import { memo } from 'react';
|
||||
import { useVideoDTO } from 'services/api/endpoints/videos';
|
||||
|
||||
export const VideoViewerToolbar = memo(() => {
|
||||
const lastSelectedItem = useAppSelector(selectLastSelectedItem);
|
||||
const videoDTO = useVideoDTO(lastSelectedItem?.type === 'video' ? lastSelectedItem.id : null);
|
||||
|
||||
return (
|
||||
<Flex w="full" justifyContent="flex-end" h={8}>
|
||||
{videoDTO && <ToggleMetadataViewerButton />}
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
VideoViewerToolbar.displayName = 'VideoViewerToolbar';
|
||||
@@ -4,7 +4,7 @@ import { fieldRunwayModelValueChanged } from 'features/nodes/store/nodesSlice';
|
||||
import type { RunwayModelFieldInputInstance, RunwayModelFieldInputTemplate } from 'features/nodes/types/field';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useRunwayModels } from 'services/api/hooks/modelsByType';
|
||||
import type { VideoApiModelConfig } from 'services/api/types';
|
||||
import type { VideoApiModelConfig } from 'services/api/types';
|
||||
|
||||
import type { FieldComponentProps } from './types';
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { GraphBuilderArg, GraphBuilderReturn } from 'features/nodes/util/gr
|
||||
import { UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
|
||||
import { selectStartingFrameImage, 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');
|
||||
@@ -33,7 +34,9 @@ export const buildRunwayVideoGraph = (arg: GraphBuilderArg): GraphBuilderReturn
|
||||
const firstFrameImageField = zImageField.parse(startingFrameImage);
|
||||
|
||||
const { seed, shouldRandomizeSeed } = params;
|
||||
const { videoDuration } = videoParams;
|
||||
const { videoModel, videoDuration, videoAspectRatio, videoResolution } = videoParams;
|
||||
|
||||
assert(videoModel, 'Runway video requires a model');
|
||||
|
||||
const finalSeed = shouldRandomizeSeed ? undefined : seed;
|
||||
|
||||
@@ -51,7 +54,7 @@ export const buildRunwayVideoGraph = (arg: GraphBuilderArg): GraphBuilderReturn
|
||||
// @ts-expect-error: This node is not available in the OSS application
|
||||
type: 'runway_generate_video',
|
||||
duration: parseInt(videoDuration || '0', 10),
|
||||
aspect_ratio: params.dimensions.aspectRatio.id,
|
||||
aspect_ratio: videoAspectRatio,
|
||||
seed: finalSeed,
|
||||
first_frame_image: firstFrameImageField,
|
||||
});
|
||||
@@ -61,12 +64,13 @@ export const buildRunwayVideoGraph = (arg: GraphBuilderArg): GraphBuilderReturn
|
||||
|
||||
// Set up metadata
|
||||
g.upsertMetadata({
|
||||
model: Graph.getModelMetadataField(videoModel as VideoApiModelConfig),
|
||||
positive_prompt: prompts.positive,
|
||||
negative_prompt: prompts.negative || '',
|
||||
video_duration: videoDuration,
|
||||
video_aspect_ratio: params.dimensions.aspectRatio.id,
|
||||
duration: videoDuration,
|
||||
aspect_ratio: videoAspectRatio,
|
||||
resolution: videoResolution,
|
||||
seed: finalSeed,
|
||||
generation_type: 'image-to-video',
|
||||
first_frame_image: startingFrameImage,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { GraphBuilderArg, GraphBuilderReturn } from 'features/nodes/util/gr
|
||||
import { UnsupportedGenerationModeError } from 'features/nodes/util/graph/types';
|
||||
import { selectStartingFrameImage, 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');
|
||||
@@ -28,9 +29,11 @@ export const buildVeo3VideoGraph = (arg: GraphBuilderArg): GraphBuilderReturn =>
|
||||
assert(prompts.positive.length > 0, 'Veo3 video requires positive prompt to have at least one character');
|
||||
|
||||
const { seed, shouldRandomizeSeed } = params;
|
||||
const { videoModel, videoResolution, videoDuration } = videoParams;
|
||||
const { videoModel, videoResolution, videoDuration, videoAspectRatio } = videoParams;
|
||||
const finalSeed = shouldRandomizeSeed ? undefined : seed;
|
||||
|
||||
assert(videoModel, 'Veo3 video requires a model');
|
||||
|
||||
const g = new Graph(getPrefixedId('veo3_video_graph'));
|
||||
|
||||
const positivePrompt = g.addNode({
|
||||
@@ -63,14 +66,14 @@ export const buildVeo3VideoGraph = (arg: GraphBuilderArg): GraphBuilderReturn =>
|
||||
|
||||
// Set up metadata
|
||||
g.upsertMetadata({
|
||||
model: Graph.getModelMetadataField(videoModel as VideoApiModelConfig),
|
||||
positive_prompt: prompts.positive,
|
||||
negative_prompt: prompts.negative || '',
|
||||
video_duration: videoDuration,
|
||||
video_aspect_ratio: params.dimensions.aspectRatio.id,
|
||||
duration: videoDuration,
|
||||
aspect_ratio: videoAspectRatio,
|
||||
resolution: videoResolution,
|
||||
seed: finalSeed,
|
||||
generation_type: 'image-to-video',
|
||||
starting_image: startingFrameImage,
|
||||
video_model: videoParams.videoModel,
|
||||
first_frame_image: startingFrameImage,
|
||||
});
|
||||
|
||||
g.setMetadataReceivingNode(veo3VideoNode);
|
||||
|
||||
@@ -16,9 +16,9 @@ import {
|
||||
zChatGPT4oAspectRatioID,
|
||||
zFluxKontextAspectRatioID,
|
||||
zGemini2_5AspectRatioID,
|
||||
zImagen3AspectRatioID,
|
||||
zRunwayAspectRatioID,
|
||||
zVeo3AspectRatioID,
|
||||
zImagen3AspectRatioID,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { selectIsRunway, selectIsVeo3 } from 'features/parameters/store/videoSlice';
|
||||
import type { ChangeEventHandler } from 'react';
|
||||
|
||||
@@ -2,6 +2,6 @@ import { createSelector } from '@reduxjs/toolkit';
|
||||
import { selectUiSlice } from 'features/ui/store/uiSlice';
|
||||
|
||||
export const selectActiveTab = createSelector(selectUiSlice, (ui) => ui.activeTab);
|
||||
export const selectShouldShowImageDetails = createSelector(selectUiSlice, (ui) => ui.shouldShowImageDetails);
|
||||
export const selectShouldShowItemDetails = createSelector(selectUiSlice, (ui) => ui.shouldShowItemDetails);
|
||||
export const selectShouldShowProgressInViewer = createSelector(selectUiSlice, (ui) => ui.shouldShowProgressInViewer);
|
||||
export const selectPickerCompactViewStates = createSelector(selectUiSlice, (ui) => ui.pickerCompactViewStates);
|
||||
|
||||
@@ -14,8 +14,8 @@ const slice = createSlice({
|
||||
setActiveTab: (state, action: PayloadAction<UIState['activeTab']>) => {
|
||||
state.activeTab = action.payload;
|
||||
},
|
||||
setShouldShowImageDetails: (state, action: PayloadAction<UIState['shouldShowImageDetails']>) => {
|
||||
state.shouldShowImageDetails = action.payload;
|
||||
setShouldShowItemDetails: (state, action: PayloadAction<UIState['shouldShowItemDetails']>) => {
|
||||
state.shouldShowItemDetails = action.payload;
|
||||
},
|
||||
setShouldShowProgressInViewer: (state, action: PayloadAction<UIState['shouldShowProgressInViewer']>) => {
|
||||
state.shouldShowProgressInViewer = action.payload;
|
||||
@@ -75,7 +75,7 @@ const slice = createSlice({
|
||||
|
||||
export const {
|
||||
setActiveTab,
|
||||
setShouldShowImageDetails,
|
||||
setShouldShowItemDetails,
|
||||
setShouldShowProgressInViewer,
|
||||
accordionStateChanged,
|
||||
expanderStateChanged,
|
||||
@@ -111,6 +111,6 @@ export const uiSliceConfig: SliceConfig<typeof slice> = {
|
||||
}
|
||||
return zUIState.parse(state);
|
||||
},
|
||||
persistDenylist: ['shouldShowImageDetails'],
|
||||
persistDenylist: ['shouldShowItemDetails'],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ export type Serializable = z.infer<typeof zSerializable>;
|
||||
export const zUIState = z.object({
|
||||
_version: z.literal(4),
|
||||
activeTab: zTabName,
|
||||
shouldShowImageDetails: z.boolean(),
|
||||
shouldShowItemDetails: z.boolean(),
|
||||
shouldShowProgressInViewer: z.boolean(),
|
||||
accordions: z.record(z.string(), z.boolean()),
|
||||
expanders: z.record(z.string(), z.boolean()),
|
||||
@@ -28,7 +28,7 @@ export type UIState = z.infer<typeof zUIState>;
|
||||
export const getInitialUIState = (): UIState => ({
|
||||
_version: 4 as const,
|
||||
activeTab: 'generate' as const,
|
||||
shouldShowImageDetails: false,
|
||||
shouldShowItemDetails: false,
|
||||
shouldShowProgressInViewer: true,
|
||||
accordions: {},
|
||||
expanders: {},
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useFocusRegion } from 'common/hooks/focus';
|
||||
import { selectLastSelectedItem } from 'features/gallery/store/gallerySelectors';
|
||||
import { memo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ReactPlayer from 'react-player';
|
||||
import { useGetVideoDTOQuery } from 'services/api/endpoints/videos';
|
||||
import { useVideoDTO } from 'services/api/endpoints/videos';
|
||||
|
||||
export const VideoPlayer = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const lastSelectedItem = useAppSelector(selectLastSelectedItem);
|
||||
const { data: videoDTO } = useGetVideoDTOQuery(lastSelectedItem?.id ?? skipToken);
|
||||
const videoDTO = useVideoDTO(lastSelectedItem?.id);
|
||||
|
||||
useFocusRegion('video', ref);
|
||||
|
||||
@@ -21,7 +18,6 @@ export const VideoPlayer = memo(() => {
|
||||
{videoDTO?.video_url && (
|
||||
<ReactPlayer src={videoDTO.video_url} controls={true} width={videoDTO.width} height={videoDTO.height} />
|
||||
)}
|
||||
{!videoDTO?.video_url && <Text>{t('gallery.noVideoSelected')}</Text>}
|
||||
</Flex>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { getStore } from 'app/store/nanostores/store';
|
||||
import type { paths } from 'services/api/schema';
|
||||
import type { GetVideoIdsArgs, GetVideoIdsResult, VideoDTO } from 'services/api/types';
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
} from 'services/api/util/tagInvalidation';
|
||||
import stableHash from 'stable-hash';
|
||||
import type { Param0 } from 'tsafe';
|
||||
import type { JsonObject } from 'type-fest';
|
||||
|
||||
import { api, buildV1Url, LIST_TAG } from '..';
|
||||
|
||||
@@ -32,6 +34,11 @@ export const videosApi = api.injectEndpoints({
|
||||
providesTags: (result, error, video_id) => [{ type: 'Video', id: video_id }],
|
||||
}),
|
||||
|
||||
getVideoMetadata: build.query<JsonObject | undefined, string>({
|
||||
query: (video_id) => ({ url: buildVideosUrl(`i/${video_id}/metadata`) }),
|
||||
providesTags: (result, error, video_id) => [{ type: 'VideoMetadata', id: video_id }],
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get ordered list of image names for selection operations
|
||||
*/
|
||||
@@ -201,6 +208,7 @@ export const {
|
||||
useDeleteVideosMutation,
|
||||
useAddVideosToBoardMutation,
|
||||
useRemoveVideosFromBoardMutation,
|
||||
useGetVideoMetadataQuery,
|
||||
} = videosApi;
|
||||
|
||||
/**
|
||||
@@ -224,3 +232,8 @@ export const getVideoDTOSafe = async (
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const useVideoDTO = (video_id: string | null | undefined) => {
|
||||
const { currentData: videoDTO } = useGetVideoDTOQuery(video_id ?? skipToken);
|
||||
return videoDTO ?? null;
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectMetadataFetchDebounce } from 'features/system/store/configSlice';
|
||||
import { imagesApi, useGetImageMetadataQuery } from 'services/api/endpoints/images';
|
||||
import { useGetVideoMetadataQuery, videosApi } from 'services/api/endpoints/videos';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
|
||||
export const useDebouncedMetadata = (imageName?: string | null) => {
|
||||
@@ -16,3 +17,16 @@ export const useDebouncedMetadata = (imageName?: string | null) => {
|
||||
isLoading: cachedData ? false : isFetching || imageName !== debouncedImageName,
|
||||
};
|
||||
};
|
||||
|
||||
export const useDebouncedVideoMetadata = (videoId?: string | null) => {
|
||||
const metadataFetchDebounce = useAppSelector(selectMetadataFetchDebounce);
|
||||
|
||||
const [debouncedVideoId] = useDebounce(videoId, metadataFetchDebounce);
|
||||
const { currentData: cachedData } = videosApi.endpoints.getVideoMetadata.useQueryState(videoId ?? skipToken);
|
||||
const { currentData: data, isFetching } = useGetVideoMetadataQuery(debouncedVideoId ?? skipToken);
|
||||
|
||||
return {
|
||||
metadata: cachedData ?? data,
|
||||
isLoading: cachedData ? false : isFetching || videoId !== debouncedVideoId,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -55,6 +55,7 @@ const tagTypes = [
|
||||
'Schema',
|
||||
'QueueCountsByDestination',
|
||||
'Video',
|
||||
'VideoMetadata',
|
||||
'VideoList',
|
||||
'VideoIdList',
|
||||
'VideoCollectionCounts',
|
||||
|
||||
Reference in New Issue
Block a user