mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-01-15 06:18:03 -05:00
add duration and aspect ratio to video settings
This commit is contained in:
committed by
Mary Hipp Rogers
parent
5c93e53195
commit
4d8bcad15b
@@ -1191,6 +1191,7 @@
|
||||
},
|
||||
"parameters": {
|
||||
"aspect": "Aspect",
|
||||
"duration": "Duration",
|
||||
"lockAspectRatio": "Lock Aspect Ratio",
|
||||
"swapDimensions": "Swap Dimensions",
|
||||
"setToOptimalSize": "Optimize size for model",
|
||||
|
||||
@@ -18,6 +18,8 @@ import {
|
||||
isFluxKontextAspectRatioID,
|
||||
isGemini2_5AspectRatioID,
|
||||
isImagenAspectRatioID,
|
||||
isRunwayAspectRatioID,
|
||||
RUNWAY_ASPECT_RATIOS,
|
||||
zParamsState,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { calculateNewSize } from 'features/controlLayers/util/getScaledBoundingBoxDimensions';
|
||||
@@ -39,6 +41,7 @@ import type {
|
||||
ParameterCLIPGEmbedModel,
|
||||
ParameterCLIPLEmbedModel,
|
||||
ParameterControlLoRAModel,
|
||||
ParameterDuration,
|
||||
ParameterGuidance,
|
||||
ParameterModel,
|
||||
ParameterNegativePrompt,
|
||||
@@ -380,6 +383,9 @@ const slice = createSlice({
|
||||
state.dimensions.rect.height = bboxDims.height;
|
||||
}
|
||||
},
|
||||
setVideoDuration: (state, action: PayloadAction<ParameterDuration>) => {
|
||||
state.videoDuration = action.payload;
|
||||
},
|
||||
paramsReset: (state) => resetState(state),
|
||||
},
|
||||
});
|
||||
@@ -481,6 +487,7 @@ export const {
|
||||
syncedToOptimalDimension,
|
||||
|
||||
paramsReset,
|
||||
setVideoDuration,
|
||||
} = slice.actions;
|
||||
|
||||
export const paramsSliceConfig: SliceConfig<typeof slice> = {
|
||||
@@ -605,6 +612,7 @@ export const selectHeight = createParamsSelector((params) => params.dimensions.r
|
||||
export const selectAspectRatioID = createParamsSelector((params) => params.dimensions.aspectRatio.id);
|
||||
export const selectAspectRatioValue = createParamsSelector((params) => params.dimensions.aspectRatio.value);
|
||||
export const selectAspectRatioIsLocked = createParamsSelector((params) => params.dimensions.aspectRatio.isLocked);
|
||||
export const selectVideoDuration = createParamsSelector((params) => params.videoDuration);
|
||||
|
||||
export const selectMainModelConfig = createSelector(
|
||||
selectModelConfigsQuery,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
zParameterCLIPGEmbedModel,
|
||||
zParameterCLIPLEmbedModel,
|
||||
zParameterControlLoRAModel,
|
||||
zParameterDuration,
|
||||
zParameterGuidance,
|
||||
zParameterImageDimension,
|
||||
zParameterMaskBlurMethod,
|
||||
@@ -482,6 +483,19 @@ export const FLUX_KONTEXT_ASPECT_RATIOS: Record<FluxKontextAspectRatio, Dimensio
|
||||
'1:1': { width: 1024, height: 1024 },
|
||||
};
|
||||
|
||||
export const zRunwayAspectRatioID = z.enum(['16:9', '4:3', '1:1', '3:4', '9:16', '21:9']);
|
||||
type RunwayAspectRatio = z.infer<typeof zRunwayAspectRatioID>;
|
||||
export const isRunwayAspectRatioID = (v: unknown): v is RunwayAspectRatio =>
|
||||
zRunwayAspectRatioID.safeParse(v).success;
|
||||
export const RUNWAY_ASPECT_RATIOS: Record<RunwayAspectRatio, Dimensions> = {
|
||||
'16:9': { width: 1280, height: 720 },
|
||||
'4:3': { width: 1104, height: 832 },
|
||||
'1:1': { width: 960, height: 960 },
|
||||
'3:4': { width: 832, height: 1104 },
|
||||
'9:16': { width: 720, height: 1280 },
|
||||
'21:9': { width: 1584, height: 672 },
|
||||
};
|
||||
|
||||
const zAspectRatioConfig = z.object({
|
||||
id: zAspectRatioID,
|
||||
value: z.number().gt(0),
|
||||
@@ -568,6 +582,7 @@ export const zParamsState = z.object({
|
||||
clipGEmbedModel: zParameterCLIPGEmbedModel.nullable(),
|
||||
controlLora: zParameterControlLoRAModel.nullable(),
|
||||
dimensions: zDimensionsState,
|
||||
videoDuration: zParameterDuration,
|
||||
});
|
||||
export type ParamsState = z.infer<typeof zParamsState>;
|
||||
export const getInitialParamsState = (): ParamsState => ({
|
||||
@@ -618,6 +633,7 @@ export const getInitialParamsState = (): ParamsState => ({
|
||||
rect: { x: 0, y: 0, width: 512, height: 512 },
|
||||
aspectRatio: deepClone(DEFAULT_ASPECT_RATIO_CONFIG),
|
||||
},
|
||||
videoDuration: 5,
|
||||
});
|
||||
|
||||
const zInpaintMasks = z.object({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MenuItem } from '@invoke-ai/ui-library';
|
||||
import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext';
|
||||
import { selectVideoFirstFrameImage, videoFirstFrameImageChanged } from 'features/parameters/store/videoSlice';
|
||||
import { startingFrameImageChanged } from 'features/parameters/store/videoSlice';
|
||||
import { navigationApi } from 'features/ui/layouts/navigation-api';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -13,7 +13,7 @@ export const ImageMenuItemSendToVideo = memo(() => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
dispatch(videoFirstFrameImageChanged(imageDTO));
|
||||
dispatch(startingFrameImageChanged(imageDTO));
|
||||
navigationApi.switchToTab('video');
|
||||
}, [imageDTO]);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { logger } from 'app/logging/logger';
|
||||
import type { RootState } from 'app/store/store';
|
||||
import { getPrefixedId } from 'features/controlLayers/konva/util';
|
||||
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectVideoFirstFrameImage, selectVideoLastFrameImage } from 'features/parameters/store/videoSlice';
|
||||
import { selectStartingFrameImage } from 'features/parameters/store/videoSlice';
|
||||
import { zImageField } from 'features/nodes/types/common';
|
||||
import { Graph } from 'features/nodes/util/graph/generation/Graph';
|
||||
import { selectPresetModifiedPrompts } from 'features/nodes/util/graph/graphBuilderUtils';
|
||||
@@ -13,29 +13,11 @@ import { assert } from 'tsafe';
|
||||
|
||||
const log = logger('system');
|
||||
|
||||
// Default video parameters - these could be moved to a video params slice in the future
|
||||
const DEFAULT_VIDEO_DURATION = 5;
|
||||
const DEFAULT_VIDEO_ASPECT_RATIO = "1280:768"; // Default landscape
|
||||
const DEFAULT_ENHANCE_PROMPT = true;
|
||||
|
||||
// Video parameter extraction helper
|
||||
const getVideoParameters = (state: RootState) => {
|
||||
// In the future, these could come from a dedicated video parameters slice
|
||||
// For now, we use defaults but allow them to be overridden by any video-specific state
|
||||
return {
|
||||
duration: DEFAULT_VIDEO_DURATION,
|
||||
aspectRatio: DEFAULT_VIDEO_ASPECT_RATIO,
|
||||
enhancePrompt: DEFAULT_ENHANCE_PROMPT,
|
||||
};
|
||||
};
|
||||
|
||||
export const buildRunwayVideoGraph = (arg: GraphBuilderArg): GraphBuilderReturn => {
|
||||
const { generationMode, state, manager } = arg;
|
||||
|
||||
log.debug({ generationMode, manager: manager?.id }, 'Building Runway video graph');
|
||||
|
||||
// Runway video generation supports text-to-video and image-to-video
|
||||
// We can support multiple generation modes depending on whether frame images are provided
|
||||
const supportedModes = ['txt2img'] as const;
|
||||
if (!supportedModes.includes(generationMode as any)) {
|
||||
throw new UnsupportedGenerationModeError(t('toast.runwayIncompatibleGenerationMode'));
|
||||
@@ -43,17 +25,15 @@ export const buildRunwayVideoGraph = (arg: GraphBuilderArg): GraphBuilderReturn
|
||||
|
||||
const params = selectParamsSlice(state);
|
||||
const prompts = selectPresetModifiedPrompts(state);
|
||||
const videoFirstFrameImage = selectVideoFirstFrameImage(state);
|
||||
const videoLastFrameImage = selectVideoLastFrameImage(state);
|
||||
const videoParams = getVideoParameters(state);
|
||||
const startingFrameImage = selectStartingFrameImage(state);
|
||||
|
||||
assert(startingFrameImage, 'Video starting frame is required for runway video generation');
|
||||
const firstFrameImageField = zImageField.parse(startingFrameImage);
|
||||
|
||||
// Get seed from params
|
||||
const { seed, shouldRandomizeSeed } = params;
|
||||
const finalSeed = shouldRandomizeSeed ? undefined : seed;
|
||||
|
||||
// Determine if this is image-to-video or text-to-video
|
||||
const hasFrameImages = videoFirstFrameImage || videoLastFrameImage;
|
||||
|
||||
const g = new Graph(getPrefixedId('runway_video_graph'));
|
||||
|
||||
const positivePrompt = g.addNode({
|
||||
@@ -67,47 +47,27 @@ export const buildRunwayVideoGraph = (arg: GraphBuilderArg): GraphBuilderReturn
|
||||
id: getPrefixedId('runway_generate_video'),
|
||||
// @ts-expect-error: This node is not available in the OSS application
|
||||
type: 'runway_generate_video',
|
||||
duration: videoParams.duration,
|
||||
aspect_ratio: videoParams.aspectRatio,
|
||||
duration: params.videoDuration,
|
||||
aspect_ratio: params.dimensions.aspectRatio.id,
|
||||
seed: finalSeed,
|
||||
first_frame_image: firstFrameImageField,
|
||||
});
|
||||
|
||||
// @ts-expect-error: This node is not available in the OSS application
|
||||
g.addEdge(positivePrompt, 'value', runwayVideoNode, 'prompt');
|
||||
|
||||
|
||||
// Add first frame image if provided
|
||||
if (videoFirstFrameImage) {
|
||||
const firstFrameImageField = zImageField.parse(videoFirstFrameImage);
|
||||
// @ts-expect-error: This connection is specific to runway node
|
||||
runwayVideoNode.first_frame_image = firstFrameImageField;
|
||||
}
|
||||
|
||||
// Add last frame image if provided
|
||||
if (videoLastFrameImage) {
|
||||
const lastFrameImageField = zImageField.parse(videoLastFrameImage);
|
||||
// @ts-expect-error: This connection is specific to runway node
|
||||
runwayVideoNode.last_frame_image = lastFrameImageField;
|
||||
}
|
||||
|
||||
// Set up metadata
|
||||
g.upsertMetadata({
|
||||
positive_prompt: prompts.positive,
|
||||
negative_prompt: prompts.negative || '',
|
||||
video_duration: videoParams.duration,
|
||||
video_aspect_ratio: videoParams.aspectRatio,
|
||||
video_duration: params.videoDuration,
|
||||
video_aspect_ratio: params.dimensions.aspectRatio.id,
|
||||
seed: finalSeed,
|
||||
enhance_prompt: videoParams.enhancePrompt,
|
||||
generation_type: hasFrameImages ? 'image-to-video' : 'text-to-video',
|
||||
generation_type: 'image-to-video',
|
||||
first_frame_image: startingFrameImage,
|
||||
});
|
||||
|
||||
// Add video frame images to metadata if they exist
|
||||
if (hasFrameImages) {
|
||||
g.upsertMetadata({
|
||||
first_frame_image: videoFirstFrameImage,
|
||||
last_frame_image: videoLastFrameImage,
|
||||
}, 'merge');
|
||||
}
|
||||
|
||||
|
||||
g.setMetadataReceivingNode(runwayVideoNode);
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@ import {
|
||||
zFluxKontextAspectRatioID,
|
||||
zGemini2_5AspectRatioID,
|
||||
zImagen3AspectRatioID,
|
||||
zRunwayAspectRatioID,
|
||||
} from 'features/controlLayers/store/types';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import type { ChangeEventHandler } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -32,6 +34,7 @@ export const DimensionsAspectRatioSelect = memo(() => {
|
||||
const isImagen4 = useAppSelector(selectIsImagen4);
|
||||
const isFluxKontext = useAppSelector(selectIsFluxKontext);
|
||||
const isGemini2_5 = useAppSelector(selectIsGemini2_5);
|
||||
const activeTab = useAppSelector(selectActiveTab);
|
||||
const options = useMemo(() => {
|
||||
// Imagen3 and ChatGPT4o have different aspect ratio options, and do not support freeform sizes
|
||||
if (isImagen3 || isImagen4) {
|
||||
@@ -46,9 +49,12 @@ export const DimensionsAspectRatioSelect = memo(() => {
|
||||
if (isGemini2_5) {
|
||||
return zGemini2_5AspectRatioID.options;
|
||||
}
|
||||
if (activeTab === 'video') {
|
||||
return zRunwayAspectRatioID.options;
|
||||
}
|
||||
// All other models
|
||||
return zAspectRatioID.options;
|
||||
}, [isImagen3, isChatGPT4o, isImagen4, isFluxKontext, isGemini2_5]);
|
||||
}, [isImagen3, isChatGPT4o, isImagen4, isFluxKontext, activeTab, isGemini2_5]);
|
||||
|
||||
const onChange = useCallback<ChangeEventHandler<HTMLSelectElement>>(
|
||||
(e) => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { selectGridSize, selectOptimalDimension } from 'features/controlLayers/s
|
||||
import { selectWidthConfig } from 'features/system/store/configSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
|
||||
export const DimensionsWidth = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
@@ -15,6 +16,7 @@ export const DimensionsWidth = memo(() => {
|
||||
const config = useAppSelector(selectWidthConfig);
|
||||
const isApiModel = useAppSelector(selectIsApiBaseModel);
|
||||
const gridSize = useAppSelector(selectGridSize);
|
||||
const activeTab = useAppSelector(selectActiveTab);
|
||||
|
||||
const onChange = useCallback(
|
||||
(v: number) => {
|
||||
@@ -29,7 +31,7 @@ export const DimensionsWidth = memo(() => {
|
||||
);
|
||||
|
||||
return (
|
||||
<FormControl isDisabled={isApiModel}>
|
||||
<FormControl isDisabled={isApiModel || activeTab === 'video'}>
|
||||
<InformationalPopover feature="paramWidth">
|
||||
<FormLabel>{t('parameters.width')}</FormLabel>
|
||||
</InformationalPopover>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { FormControl, FormLabel, Select } from "@invoke-ai/ui-library";
|
||||
import { useAppDispatch, useAppSelector } from "app/store/storeHooks";
|
||||
import { selectVideoDuration, setVideoDuration } from "features/controlLayers/store/paramsSlice";
|
||||
import { isParameterDuration, ParameterDuration } from "features/parameters/types/parameterSchemas";
|
||||
import { ChangeEventHandler, useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PiCaretDownBold } from "react-icons/pi";
|
||||
|
||||
const options: { label: string; value: ParameterDuration }[] = [
|
||||
{ label: '5 seconds', value: 5 },
|
||||
{ label: '10 seconds', value: 10 },
|
||||
];
|
||||
|
||||
export const ParamDuration = () => {
|
||||
const videoDuration = useAppSelector(selectVideoDuration);
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const onChange = useCallback<ChangeEventHandler<HTMLSelectElement>>(
|
||||
(e) => {
|
||||
if (!isParameterDuration(e.target.value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(setVideoDuration(e.target.value));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const value = useMemo(() => options.find((o) => o.value === videoDuration), [videoDuration]);
|
||||
|
||||
return <FormControl>
|
||||
<FormLabel>{t('parameters.duration')}</FormLabel>
|
||||
<Select size="sm" value={value?.value} onChange={onChange} cursor="pointer" iconSize="0.75rem" icon={<PiCaretDownBold />}>
|
||||
{options.map((duration) => (
|
||||
<option key={duration.value} value={duration.value}>
|
||||
{duration.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>;
|
||||
};
|
||||
@@ -11,8 +11,7 @@ import z from 'zod';
|
||||
|
||||
const zVideoState = z.object({
|
||||
_version: z.literal(1),
|
||||
videoFirstFrameImage: zImageWithDims.nullable(),
|
||||
videoLastFrameImage: zImageWithDims.nullable(),
|
||||
startingFrameImage: zImageWithDims.nullable(),
|
||||
generatedVideo: zVideoOutput.nullable(),
|
||||
});
|
||||
|
||||
@@ -20,8 +19,7 @@ export type VideoState = z.infer<typeof zVideoState>;
|
||||
|
||||
const getInitialState = (): VideoState => ({
|
||||
_version: 1,
|
||||
videoFirstFrameImage: null,
|
||||
videoLastFrameImage: null,
|
||||
startingFrameImage: null,
|
||||
generatedVideo: null,
|
||||
});
|
||||
|
||||
@@ -29,12 +27,8 @@ const slice = createSlice({
|
||||
name: 'video',
|
||||
initialState: getInitialState(),
|
||||
reducers: {
|
||||
videoFirstFrameImageChanged: (state, action: PayloadAction<ImageWithDims | null>) => {
|
||||
state.videoFirstFrameImage = action.payload;
|
||||
},
|
||||
|
||||
videoLastFrameImageChanged: (state, action: PayloadAction<ImageWithDims | null>) => {
|
||||
state.videoLastFrameImage = action.payload;
|
||||
startingFrameImageChanged: (state, action: PayloadAction<ImageWithDims | null>) => {
|
||||
state.startingFrameImage = action.payload;
|
||||
},
|
||||
|
||||
generatedVideoChanged: (state, action: PayloadAction<VideoOutput | null>) => {
|
||||
@@ -45,8 +39,7 @@ const slice = createSlice({
|
||||
});
|
||||
|
||||
export const {
|
||||
videoFirstFrameImageChanged,
|
||||
videoLastFrameImageChanged,
|
||||
startingFrameImageChanged,
|
||||
generatedVideoChanged,
|
||||
} = slice.actions;
|
||||
|
||||
@@ -68,6 +61,5 @@ export const videoSliceConfig: SliceConfig<typeof slice> = {
|
||||
export const selectVideoSlice = (state: RootState) => state.video;
|
||||
const createVideoSelector = <T>(selector: Selector<VideoState, T>) => createSelector(selectVideoSlice, selector);
|
||||
|
||||
export const selectVideoFirstFrameImage = createVideoSelector((video) => video.videoFirstFrameImage);
|
||||
export const selectVideoLastFrameImage = createVideoSelector((video) => video.videoLastFrameImage);
|
||||
export const selectStartingFrameImage = createVideoSelector((video) => video.startingFrameImage);
|
||||
export const selectGeneratedVideo = createVideoSelector((video) => video.generatedVideo);
|
||||
@@ -145,6 +145,11 @@ export const [zParameterSeamlessY, isParameterSeamlessY] = buildParameter(z.bool
|
||||
export type ParameterSeamlessY = z.infer<typeof zParameterSeamlessY>;
|
||||
// #endregion
|
||||
|
||||
// #region Duration
|
||||
export const [zParameterDuration, isParameterDuration] = buildParameter(z.union([z.literal(5), z.literal(10)]));
|
||||
export type ParameterDuration = z.infer<typeof zParameterDuration>;
|
||||
// #endregion
|
||||
|
||||
// #region Precision
|
||||
export const [zParameterPrecision, isParameterPrecision] = buildParameter(z.enum(['fp16', 'fp32']));
|
||||
export type ParameterPrecision = z.infer<typeof zParameterPrecision>;
|
||||
|
||||
@@ -1,39 +1,35 @@
|
||||
import { Flex, FormLabel, Text } from '@invoke-ai/ui-library';
|
||||
import { Flex, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
|
||||
import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||
import type { SetUpscaleInitialImageDndTargetData } from 'features/dnd/dnd';
|
||||
import { setUpscaleInitialImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { DndImage } from 'features/dnd/DndImage';
|
||||
import { DndImageIcon } from 'features/dnd/DndImageIcon';
|
||||
import { selectVideoLastFrameImage, videoLastFrameImageChanged } from 'features/parameters/store/videoSlice';
|
||||
import { selectStartingFrameImage, startingFrameImageChanged } from 'features/parameters/store/videoSlice';
|
||||
import { t } from 'i18next';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
|
||||
import { useImageDTO } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const VideoLastFrameImage = () => {
|
||||
export const StartingFrameImage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const videoLastFrameImage = useAppSelector(selectVideoLastFrameImage);
|
||||
const imageDTO = useImageDTO(videoLastFrameImage?.image_name);
|
||||
|
||||
const startingFrameImage = useAppSelector(selectStartingFrameImage);
|
||||
const imageDTO = useImageDTO(startingFrameImage?.image_name);
|
||||
|
||||
|
||||
const onReset = useCallback(() => {
|
||||
dispatch(videoLastFrameImageChanged(null));
|
||||
dispatch(startingFrameImageChanged(null));
|
||||
}, [dispatch]);
|
||||
|
||||
const onUpload = useCallback(
|
||||
(imageDTO: ImageDTO) => {
|
||||
dispatch(videoLastFrameImageChanged(imageDTOToImageWithDims(imageDTO)));
|
||||
dispatch(startingFrameImageChanged(imageDTOToImageWithDims(imageDTO)));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex justifyContent="flex-start" flexDir="column" gap={2}>
|
||||
<FormLabel>Last Frame Image</FormLabel>
|
||||
<Flex position="relative" w={36} h={36} alignItems="center" justifyContent="center">
|
||||
{!imageDTO && <UploadImageIconButton w="full" h="full" isError={!imageDTO} onUpload={onUpload} fontSize={36} />}
|
||||
{imageDTO && (
|
||||
@@ -63,7 +59,7 @@ export const VideoLastFrameImage = () => {
|
||||
>{`${imageDTO.width}x${imageDTO.height}`}</Text>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
@@ -1,71 +0,0 @@
|
||||
import { Flex, FormLabel, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { UploadImageIconButton } from 'common/hooks/useImageUploadButton';
|
||||
import { imageDTOToImageWithDims } from 'features/controlLayers/store/util';
|
||||
import type { SetUpscaleInitialImageDndTargetData } from 'features/dnd/dnd';
|
||||
import { setUpscaleInitialImageDndTarget } from 'features/dnd/dnd';
|
||||
import { DndDropTarget } from 'features/dnd/DndDropTarget';
|
||||
import { DndImage } from 'features/dnd/DndImage';
|
||||
import { DndImageIcon } from 'features/dnd/DndImageIcon';
|
||||
import { selectUpscaleInitialImage, upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
|
||||
import { selectVideoFirstFrameImage, videoFirstFrameImageChanged } from 'features/parameters/store/videoSlice';
|
||||
import { t } from 'i18next';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
|
||||
import { useImageDTO } from 'services/api/endpoints/images';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const VideoFirstFrameImage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const videoFirstFrameImage = useAppSelector(selectVideoFirstFrameImage);
|
||||
const imageDTO = useImageDTO(videoFirstFrameImage?.image_name);
|
||||
|
||||
|
||||
const onReset = useCallback(() => {
|
||||
dispatch(videoFirstFrameImageChanged(null));
|
||||
}, [dispatch]);
|
||||
|
||||
const onUpload = useCallback(
|
||||
(imageDTO: ImageDTO) => {
|
||||
dispatch(videoFirstFrameImageChanged(imageDTOToImageWithDims(imageDTO)));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<Flex justifyContent="flex-start" flexDir="column" gap={2}>
|
||||
<FormLabel>First Frame Image</FormLabel>
|
||||
<Flex position="relative" w={36} h={36} alignItems="center" justifyContent="center">
|
||||
{!imageDTO && <UploadImageIconButton w="full" h="full" isError={!imageDTO} onUpload={onUpload} fontSize={36} />}
|
||||
{imageDTO && (
|
||||
<>
|
||||
<DndImage imageDTO={imageDTO} borderRadius="base" />
|
||||
<Flex position="absolute" flexDir="column" top={1} insetInlineEnd={1} gap={1}>
|
||||
<DndImageIcon
|
||||
onClick={onReset}
|
||||
icon={<PiArrowCounterClockwiseBold size={16} />}
|
||||
tooltip={t('common.reset')}
|
||||
/>
|
||||
</Flex>
|
||||
<Text
|
||||
position="absolute"
|
||||
background="base.900"
|
||||
color="base.50"
|
||||
fontSize="sm"
|
||||
fontWeight="semibold"
|
||||
bottom={0}
|
||||
left={0}
|
||||
opacity={0.7}
|
||||
px={2}
|
||||
lineHeight={1.25}
|
||||
borderTopEndRadius="base"
|
||||
borderBottomStartRadius="base"
|
||||
pointerEvents="none"
|
||||
>{`${imageDTO.width}x${imageDTO.height}`}</Text>
|
||||
</>
|
||||
)}
|
||||
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,15 @@
|
||||
import { Flex, StandaloneAccordion } from '@invoke-ai/ui-library';
|
||||
import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle';
|
||||
import { memo } from 'react';
|
||||
import { memo, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { VideoFirstFrameImage } from './VideoFirstFrameImage';
|
||||
import { VideoLastFrameImage } from './VideoLastFrameImage';
|
||||
import { ParamDuration } from 'features/parameters/components/Video/ParamDuration';
|
||||
import { ParamSeed } from 'features/parameters/components/Seed/ParamSeed';
|
||||
import { Dimensions } from 'features/parameters/components/Dimensions/Dimensions';
|
||||
import { RUNWAY_ASPECT_RATIOS } from 'features/controlLayers/store/types';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { widthChanged, heightChanged, aspectRatioIdChanged, aspectRatioLockToggled } from 'features/controlLayers/store/paramsSlice';
|
||||
import { StartingFrameImage } from './StartingFrameImage';
|
||||
|
||||
|
||||
export const VideoSettingsAccordion = memo(() => {
|
||||
@@ -13,26 +18,35 @@ export const VideoSettingsAccordion = memo(() => {
|
||||
id: 'video-settings',
|
||||
defaultIsOpen: true,
|
||||
});
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
useEffect(() => { // hack to get the default aspect ratio for runway models outside paramsSlice
|
||||
const { width, height } = RUNWAY_ASPECT_RATIOS['16:9'];
|
||||
dispatch(widthChanged({ width, updateAspectRatio: true, clamp: true }));
|
||||
dispatch(heightChanged({ height, updateAspectRatio: true, clamp: true }));
|
||||
dispatch(aspectRatioIdChanged({ id: '16:9' }));
|
||||
dispatch(aspectRatioLockToggled());
|
||||
}, [dispatch]);
|
||||
|
||||
|
||||
return (
|
||||
<StandaloneAccordion
|
||||
label={t('upscaling.upscale')}
|
||||
label={"Video"}
|
||||
badges={[]}
|
||||
isOpen={isOpenAccordion}
|
||||
onToggle={onToggleAccordion}
|
||||
>
|
||||
<Flex p={4} w="full" h="full" flexDir="column" data-testid="upscale-settings-accordion">
|
||||
<Flex gap={4}>
|
||||
|
||||
<VideoFirstFrameImage />
|
||||
<VideoLastFrameImage />
|
||||
|
||||
|
||||
|
||||
|
||||
<Flex gap={4} flexDirection="column" width="full">
|
||||
<Flex gap={4}>
|
||||
<StartingFrameImage />
|
||||
<Flex gap={4} flexDirection="column" width="full">
|
||||
<ParamDuration />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Dimensions />
|
||||
<ParamSeed />
|
||||
</Flex>
|
||||
|
||||
</Flex>
|
||||
</StandaloneAccordion>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user