|
|
|
@ -49,7 +49,6 @@ import { |
|
|
|
getDefaultVideoSoundEnabled, |
|
|
|
modelSupportsVideoSound, |
|
|
|
normalizeVideoDurationSeconds, |
|
|
|
VIDEO_DURATION_PARAMETER_NAMES, |
|
|
|
VIDEO_SOUND_PARAMETER_NAMES, |
|
|
|
} from "@/utils/videoGenerationSettings"; |
|
|
|
import { defaultNodeDimensions } from "@/store/utils/nodeDefaults"; |
|
|
|
@ -142,25 +141,71 @@ const COMPOSER_COLLAPSED_WIDTH = 560; |
|
|
|
const COMPOSER_EXPANDED_HEIGHT_ESTIMATE = 300; |
|
|
|
const COMPOSER_COLLAPSED_HEIGHT_ESTIMATE = 80; |
|
|
|
const IMAGE_COUNT_PARAMETER_NAMES = ["imageCount", "numImages", "num_images", "numOutputs", "num_outputs", "count"]; |
|
|
|
const GATEWAY_DIMENSION_PARAMETER_NAMES = ["ratio", "videoRatio", "resolution", "duration"]; |
|
|
|
const DEFAULT_IMAGE_COUNT_OPTIONS: ImageGenerationCount[] = [1, 2, 4]; |
|
|
|
|
|
|
|
interface SchemaSelectOption { |
|
|
|
key: string; |
|
|
|
label: string; |
|
|
|
value: unknown; |
|
|
|
} |
|
|
|
|
|
|
|
interface GatewayDimensionSelect { |
|
|
|
parameter: ModelParameter; |
|
|
|
options: SchemaSelectOption[]; |
|
|
|
selectedKey: string | undefined; |
|
|
|
} |
|
|
|
|
|
|
|
function findSchemaParameter(schema: ModelParameter[] | undefined, names: string[]): ModelParameter | null { |
|
|
|
if (!schema || schema.length === 0) return null; |
|
|
|
const nameSet = new Set(names); |
|
|
|
return schema.find((param) => nameSet.has(param.name)) ?? null; |
|
|
|
} |
|
|
|
|
|
|
|
function getSchemaEnumOptions(schema: ModelParameter[] | undefined, names: string[]): string[] { |
|
|
|
function getSchemaOptionValues(schema: ModelParameter[] | undefined, names: string[]): string[] { |
|
|
|
const param = findSchemaParameter(schema, names); |
|
|
|
return param?.enum?.map((item) => String(item)).filter(Boolean) ?? []; |
|
|
|
return param?.options?.map((item) => String(item.value)).filter(Boolean) ?? []; |
|
|
|
} |
|
|
|
|
|
|
|
function getSchemaParameterName(schema: ModelParameter[] | undefined, names: string[]): string | null { |
|
|
|
return findSchemaParameter(schema, names)?.name ?? null; |
|
|
|
} |
|
|
|
|
|
|
|
function getSchemaSelectOptions(param: ModelParameter): SchemaSelectOption[] { |
|
|
|
return param.options?.map((option, index) => ({ |
|
|
|
key: String(option.value ?? index), |
|
|
|
label: option.label, |
|
|
|
value: option.value, |
|
|
|
})) ?? []; |
|
|
|
} |
|
|
|
|
|
|
|
function getSelectedSchemaOptionKey(options: SchemaSelectOption[], value: unknown): string | undefined { |
|
|
|
return options.find((option) => |
|
|
|
Object.is(option.value, value) || String(option.value) === String(value ?? "") |
|
|
|
)?.key; |
|
|
|
} |
|
|
|
|
|
|
|
function buildGatewayDimensionSelects( |
|
|
|
schema: ModelParameter[] | undefined, |
|
|
|
parameters: Record<string, unknown> | undefined |
|
|
|
): GatewayDimensionSelect[] { |
|
|
|
if (!schema || schema.length === 0) return []; |
|
|
|
const nameSet = new Set(GATEWAY_DIMENSION_PARAMETER_NAMES); |
|
|
|
return schema |
|
|
|
.filter((param) => nameSet.has(param.name) && param.options && param.options.length > 0) |
|
|
|
.map((parameter) => { |
|
|
|
const options = getSchemaSelectOptions(parameter); |
|
|
|
const value = parameters?.[parameter.name] ?? parameter.default ?? options[0]?.value; |
|
|
|
return { |
|
|
|
parameter, |
|
|
|
options, |
|
|
|
selectedKey: getSelectedSchemaOptionKey(options, value), |
|
|
|
}; |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
function getImageCountOptions(schema: ModelParameter[] | undefined): ImageGenerationCount[] { |
|
|
|
return getSchemaEnumOptions(schema, IMAGE_COUNT_PARAMETER_NAMES) |
|
|
|
return getSchemaOptionValues(schema, IMAGE_COUNT_PARAMETER_NAMES) |
|
|
|
.map((value) => Number(value)) |
|
|
|
.filter((value): value is ImageGenerationCount => Number.isInteger(value) && value > 0); |
|
|
|
} |
|
|
|
@ -1353,24 +1398,15 @@ export function GenerationComposer() { |
|
|
|
? context.node?.data as NanoBananaNodeData | GenerateVideoNodeData | GenerateAudioNodeData | undefined |
|
|
|
: undefined; |
|
|
|
const activeParameterSchema = generationNodeData?.parameterSchema ?? []; |
|
|
|
const imageAspectRatioOptions = getSchemaEnumOptions(activeParameterSchema, ["aspectRatio", "aspect_ratio"]); |
|
|
|
const imageResolutionOptions = getSchemaEnumOptions(activeParameterSchema, ["resolution"]); |
|
|
|
const gatewayDimensionSelects = buildGatewayDimensionSelects(activeParameterSchema, draft.parameters); |
|
|
|
const imageCountOptions = getImageCountOptions(activeParameterSchema); |
|
|
|
const visibleImageCountOptions = imageCountOptions.length > 0 |
|
|
|
? imageCountOptions |
|
|
|
: DEFAULT_IMAGE_COUNT_OPTIONS; |
|
|
|
const imageAspectRatioParameterName = getSchemaParameterName(activeParameterSchema, ["aspectRatio", "aspect_ratio"]); |
|
|
|
const imageResolutionParameterName = getSchemaParameterName(activeParameterSchema, ["resolution"]); |
|
|
|
const imageCountParameterName = getSchemaParameterName(activeParameterSchema, IMAGE_COUNT_PARAMETER_NAMES); |
|
|
|
const selectedImageCountValue = imageCountParameterName |
|
|
|
? Number(draft.parameters?.[imageCountParameterName] ?? draft.imageCount) |
|
|
|
: draft.imageCount; |
|
|
|
const videoDurationOptionsFromSchema = getSchemaEnumOptions(activeParameterSchema, VIDEO_DURATION_PARAMETER_NAMES); |
|
|
|
const videoResolutionOptions = getSchemaEnumOptions(activeParameterSchema, ["resolution"]); |
|
|
|
const videoRatioOptions = getSchemaEnumOptions(activeParameterSchema, ["videoRatio"]); |
|
|
|
const videoDurationParameterName = getSchemaParameterName(activeParameterSchema, VIDEO_DURATION_PARAMETER_NAMES); |
|
|
|
const videoResolutionParameterName = getSchemaParameterName(activeParameterSchema, ["resolution"]); |
|
|
|
const videoRatioParameterName = getSchemaParameterName(activeParameterSchema, ["videoRatio"]); |
|
|
|
const videoSoundParameterName = getSchemaParameterName(activeParameterSchema, VIDEO_SOUND_PARAMETER_NAMES); |
|
|
|
const errorMessage = nodeData?.status === "error" ? nodeData.error : null; |
|
|
|
const nodeEditModeLabel = |
|
|
|
@ -2299,48 +2335,40 @@ export function GenerationComposer() { |
|
|
|
|
|
|
|
{!isProcessNode && activeCapability === "image" && ( |
|
|
|
<> |
|
|
|
{imageAspectRatioParameterName && imageAspectRatioOptions.length > 0 && ( |
|
|
|
<select |
|
|
|
aria-label={t("node.aspectRatio")} |
|
|
|
value={String(draft.parameters?.[imageAspectRatioParameterName] ?? draft.aspectRatio)} |
|
|
|
onChange={(event) => |
|
|
|
updateSchemaDraftParameter( |
|
|
|
imageAspectRatioParameterName, |
|
|
|
event.target.value, |
|
|
|
{ aspectRatio: event.target.value as AspectRatio }, |
|
|
|
["aspectRatio"] |
|
|
|
) |
|
|
|
} |
|
|
|
className="rounded-md border border-transparent bg-transparent px-1.5 py-1 text-neutral-200 outline-none transition-colors hover:border-neutral-700 hover:bg-neutral-700/60" |
|
|
|
> |
|
|
|
{imageAspectRatioOptions.map((ratio) => ( |
|
|
|
<option key={ratio} value={ratio} className="bg-neutral-900"> |
|
|
|
{ratio} |
|
|
|
</option> |
|
|
|
))} |
|
|
|
</select> |
|
|
|
)} |
|
|
|
{imageResolutionParameterName && imageResolutionOptions.length > 0 && ( |
|
|
|
<select |
|
|
|
aria-label={t("node.resolution")} |
|
|
|
value={String(draft.parameters?.[imageResolutionParameterName] ?? draft.resolution)} |
|
|
|
onChange={(event) => |
|
|
|
updateSchemaDraftParameter( |
|
|
|
imageResolutionParameterName, |
|
|
|
event.target.value, |
|
|
|
{ resolution: event.target.value }, |
|
|
|
["resolution"] |
|
|
|
) |
|
|
|
} |
|
|
|
className="rounded-md border border-transparent bg-transparent px-1.5 py-1 text-neutral-200 outline-none transition-colors hover:border-neutral-700 hover:bg-neutral-700/60" |
|
|
|
> |
|
|
|
{imageResolutionOptions.map((item) => ( |
|
|
|
<option key={item} value={item} className="bg-neutral-900"> |
|
|
|
{item} |
|
|
|
</option> |
|
|
|
))} |
|
|
|
</select> |
|
|
|
)} |
|
|
|
{gatewayDimensionSelects.map(({ parameter, options, selectedKey }) => ( |
|
|
|
<label key={parameter.name} className="flex items-center gap-1 text-xs text-neutral-400"> |
|
|
|
<span>{parameter.label ?? parameter.name}</span> |
|
|
|
<select |
|
|
|
aria-label={parameter.label ?? parameter.name} |
|
|
|
value={selectedKey} |
|
|
|
onChange={(event) => { |
|
|
|
const option = options.find((item) => item.key === event.target.value); |
|
|
|
const patch: Partial<ComposerDraft> = {}; |
|
|
|
const fields: DraftField[] = []; |
|
|
|
if (parameter.name === "ratio") { |
|
|
|
patch.aspectRatio = String(option?.value ?? "") as AspectRatio; |
|
|
|
fields.push("aspectRatio"); |
|
|
|
} |
|
|
|
if (parameter.name === "resolution") { |
|
|
|
patch.resolution = String(option?.value ?? ""); |
|
|
|
fields.push("resolution"); |
|
|
|
} |
|
|
|
if (parameter.name === "duration") { |
|
|
|
patch.durationSeconds = normalizeVideoDurationSeconds(option?.value); |
|
|
|
fields.push("durationSeconds"); |
|
|
|
} |
|
|
|
updateSchemaDraftParameter(parameter.name, option?.value, patch, fields); |
|
|
|
}} |
|
|
|
className="rounded-md border border-transparent bg-transparent px-1.5 py-1 text-neutral-200 outline-none transition-colors hover:border-neutral-700 hover:bg-neutral-700/60" |
|
|
|
> |
|
|
|
{options.map((option) => ( |
|
|
|
<option key={option.key} value={option.key} className="bg-neutral-900"> |
|
|
|
{option.label} |
|
|
|
</option> |
|
|
|
))} |
|
|
|
</select> |
|
|
|
</label> |
|
|
|
))} |
|
|
|
{visibleImageCountOptions.length > 0 && ( |
|
|
|
<select |
|
|
|
aria-label={t("composer.imageCount")} |
|
|
|
@ -2372,65 +2400,40 @@ export function GenerationComposer() { |
|
|
|
|
|
|
|
{!isProcessNode && activeCapability === "video" && ( |
|
|
|
<> |
|
|
|
{videoDurationParameterName && videoDurationOptionsFromSchema.length > 0 && ( |
|
|
|
<select |
|
|
|
aria-label={t("node.duration")} |
|
|
|
value={String(draft.parameters?.[videoDurationParameterName] ?? draft.durationSeconds)} |
|
|
|
onChange={(event) => |
|
|
|
updateSchemaDraftParameter( |
|
|
|
videoDurationParameterName, |
|
|
|
event.target.value, |
|
|
|
{ durationSeconds: normalizeVideoDurationSeconds(event.target.value) }, |
|
|
|
["durationSeconds"] |
|
|
|
) |
|
|
|
} |
|
|
|
className="rounded-md border border-transparent bg-transparent px-1.5 py-1 text-neutral-200 outline-none transition-colors hover:border-neutral-700 hover:bg-neutral-700/60" |
|
|
|
> |
|
|
|
{videoDurationOptionsFromSchema.map((value) => { |
|
|
|
const seconds = Number(value); |
|
|
|
return ( |
|
|
|
<option key={value} value={value} className="bg-neutral-900"> |
|
|
|
{Number.isFinite(seconds) ? t("node.seconds", { count: seconds }) : value} |
|
|
|
{gatewayDimensionSelects.map(({ parameter, options, selectedKey }) => ( |
|
|
|
<label key={parameter.name} className="flex items-center gap-1 text-xs text-neutral-400"> |
|
|
|
<span>{parameter.label ?? parameter.name}</span> |
|
|
|
<select |
|
|
|
aria-label={parameter.label ?? parameter.name} |
|
|
|
value={selectedKey} |
|
|
|
onChange={(event) => { |
|
|
|
const option = options.find((item) => item.key === event.target.value); |
|
|
|
const patch: Partial<ComposerDraft> = {}; |
|
|
|
const fields: DraftField[] = []; |
|
|
|
if (parameter.name === "ratio") { |
|
|
|
patch.aspectRatio = String(option?.value ?? "") as AspectRatio; |
|
|
|
fields.push("aspectRatio"); |
|
|
|
} |
|
|
|
if (parameter.name === "resolution") { |
|
|
|
patch.resolution = String(option?.value ?? ""); |
|
|
|
fields.push("resolution"); |
|
|
|
} |
|
|
|
if (parameter.name === "duration") { |
|
|
|
patch.durationSeconds = normalizeVideoDurationSeconds(option?.value); |
|
|
|
fields.push("durationSeconds"); |
|
|
|
} |
|
|
|
updateSchemaDraftParameter(parameter.name, option?.value, patch, fields); |
|
|
|
}} |
|
|
|
className="rounded-md border border-transparent bg-transparent px-1.5 py-1 text-neutral-200 outline-none transition-colors hover:border-neutral-700 hover:bg-neutral-700/60" |
|
|
|
> |
|
|
|
{options.map((option) => ( |
|
|
|
<option key={option.key} value={option.key} className="bg-neutral-900"> |
|
|
|
{option.label} |
|
|
|
</option> |
|
|
|
); |
|
|
|
})} |
|
|
|
</select> |
|
|
|
)} |
|
|
|
{videoResolutionParameterName && videoResolutionOptions.length > 0 && ( |
|
|
|
<select |
|
|
|
aria-label={t("node.resolution")} |
|
|
|
value={String(draft.parameters?.[videoResolutionParameterName] ?? draft.resolution)} |
|
|
|
onChange={(event) => |
|
|
|
updateSchemaDraftParameter( |
|
|
|
videoResolutionParameterName, |
|
|
|
event.target.value, |
|
|
|
{ resolution: event.target.value }, |
|
|
|
["resolution"] |
|
|
|
) |
|
|
|
} |
|
|
|
className="rounded-md border border-transparent bg-transparent px-1.5 py-1 text-neutral-200 outline-none transition-colors hover:border-neutral-700 hover:bg-neutral-700/60" |
|
|
|
> |
|
|
|
{videoResolutionOptions.map((item) => ( |
|
|
|
<option key={item} value={item} className="bg-neutral-900"> |
|
|
|
{item} |
|
|
|
</option> |
|
|
|
))} |
|
|
|
</select> |
|
|
|
)} |
|
|
|
{videoRatioParameterName && videoRatioOptions.length > 0 && ( |
|
|
|
<select |
|
|
|
aria-label={t("node.aspectRatio")} |
|
|
|
value={String(draft.parameters?.[videoRatioParameterName] ?? videoRatioOptions[0])} |
|
|
|
onChange={(event) => updateSchemaDraftParameter(videoRatioParameterName, event.target.value)} |
|
|
|
className="rounded-md border border-transparent bg-transparent px-1.5 py-1 text-neutral-200 outline-none transition-colors hover:border-neutral-700 hover:bg-neutral-700/60" |
|
|
|
> |
|
|
|
{videoRatioOptions.map((ratio) => ( |
|
|
|
<option key={ratio} value={ratio} className="bg-neutral-900"> |
|
|
|
{ratio} |
|
|
|
</option> |
|
|
|
))} |
|
|
|
</select> |
|
|
|
)} |
|
|
|
))} |
|
|
|
</select> |
|
|
|
</label> |
|
|
|
))} |
|
|
|
{videoSoundParameterName && ( |
|
|
|
<button |
|
|
|
type="button" |
|
|
|
|