add duration and aspect ratio to video settings

This commit is contained in:
Mary Hipp
2025-08-15 14:56:51 -04:00
committed by Mary Hipp Rogers
parent 5c93e53195
commit 4d8bcad15b
13 changed files with 140 additions and 169 deletions

View File

@@ -1191,6 +1191,7 @@
},
"parameters": {
"aspect": "Aspect",
"duration": "Duration",
"lockAspectRatio": "Lock Aspect Ratio",
"swapDimensions": "Swap Dimensions",
"setToOptimalSize": "Optimize size for model",

View File

@@ -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,

View File

@@ -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({

View File

@@ -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]);

View File

@@ -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);

View File

@@ -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) => {

View File

@@ -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>

View File

@@ -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>;
};

View File

@@ -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);

View File

@@ -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>;

View File

@@ -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>
);

View File

@@ -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>
);
};

View File

@@ -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>
);