Browse Source

优化参数配置逻辑

feature/add_provider_popi
weige 2 months ago
parent
commit
cfd9a499ea
  1. 185
      src/components/composer/GenerationComposer.tsx
  2. 2
      src/components/nodes/GenerateAudioNode.tsx
  3. 3
      src/components/nodes/GenerateImageNode.tsx
  4. 3
      src/components/nodes/GenerateVideoNode.tsx
  5. 6
      src/store/execution/generateAudioExecutor.ts
  6. 10
      src/store/execution/generateVideoExecutor.ts
  7. 13
      src/store/execution/generationParameters.ts
  8. 22
      src/store/execution/nanoBananaExecutor.ts
  9. 31
      src/utils/__tests__/modelSchemaDefaults.test.ts
  10. 10
      src/utils/modelSchemaDefaults.ts

185
src/components/composer/GenerationComposer.tsx

@ -188,13 +188,15 @@ function getSchemaSelectOptions(param: ModelParameter): SchemaSelectOption[] {
} }
function getSelectedSchemaOptionKey(options: SchemaSelectOption[], value: unknown): string | undefined { function getSelectedSchemaOptionKey(options: SchemaSelectOption[], value: unknown): string | undefined {
const normalizedValue = String(value ?? "").toLowerCase();
return options.find((option) => return options.find((option) =>
Object.is(option.value, value) || String(option.value) === String(value ?? "") Object.is(option.value, value) || String(option.value).toLowerCase() === normalizedValue
)?.key; )?.key;
} }
function valueMatchesSchemaOptions(value: unknown, options: string[]): boolean { function valueMatchesSchemaOptions(value: unknown, options: string[]): boolean {
return options.some((option) => String(option) === String(value)); const normalizedValue = String(value ?? "").toLowerCase();
return options.some((option) => String(option).toLowerCase() === normalizedValue);
} }
function buildGatewayDimensionSelects( function buildGatewayDimensionSelects(
@ -207,8 +209,14 @@ function buildGatewayDimensionSelects(
.filter((param) => nameSet.has(param.name) && param.options && param.options.length > 0) .filter((param) => nameSet.has(param.name) && param.options && param.options.length > 0)
.map((parameter) => { .map((parameter) => {
const options = getSchemaSelectOptions(parameter); const options = getSchemaSelectOptions(parameter);
const value = draft.parameters?.[parameter.name] const value =
?? options[0]?.value; parameter.name === "resolution"
? draft.resolution
: parameter.name === "ratio" || parameter.name === "videoRatio"
? draft.aspectRatio
: parameter.name === "duration"
? draft.durationSeconds
: draft.parameters?.[parameter.name] ?? options[0]?.value;
return { return {
parameter, parameter,
options, options,
@ -217,28 +225,6 @@ function buildGatewayDimensionSelects(
}); });
} }
function getSchemaParameterFallbackValue(parameter: ModelParameter): unknown {
return parameter.default ?? parameter.options?.[0]?.value ?? parameter.enum?.[0];
}
function applySchemaParameterDefaults(
schema: ModelParameter[] | undefined,
parameters: Record<string, unknown> | undefined
): { parameters: Record<string, unknown>; changed: boolean } {
const next = { ...(parameters ?? {}) };
let changed = false;
for (const parameter of schema ?? []) {
if (next[parameter.name] !== undefined && next[parameter.name] !== null && next[parameter.name] !== "") continue;
const value = getSchemaParameterFallbackValue(parameter);
if (value === undefined || value === null || value === "") continue;
next[parameter.name] = value;
changed = true;
}
return { parameters: next, changed };
}
function getImageCountOptions(schema: ModelParameter[] | undefined): ImageGenerationCount[] { function getImageCountOptions(schema: ModelParameter[] | undefined): ImageGenerationCount[] {
return getSchemaOptionValues(schema, IMAGE_COUNT_PARAMETER_NAMES) return getSchemaOptionValues(schema, IMAGE_COUNT_PARAMETER_NAMES)
.map((value) => Number(value)) .map((value) => Number(value))
@ -602,13 +588,14 @@ function buildPopiserverTaskPricePayload(
if (!subType) return null; if (!subType) return null;
const aspectRatio = const aspectRatio =
draft.aspectRatio ||
stringFromUnknown(draft.parameters?.aspectRatio) || stringFromUnknown(draft.parameters?.aspectRatio) ||
stringFromUnknown(draft.parameters?.aspect_ratio) || stringFromUnknown(draft.parameters?.aspect_ratio) ||
draft.aspectRatio ||
"16:9"; "16:9";
const dimensions = dimensionsForAspectRatio(aspectRatio); const dimensions = dimensionsForAspectRatio(aspectRatio);
const duration = finiteIntegerFromUnknown(draft.parameters?.duration) ?? const duration = type === 2
(type === 2 ? normalizeVideoDurationSeconds(draft.durationSeconds) : null); ? normalizeVideoDurationSeconds(draft.durationSeconds)
: finiteIntegerFromUnknown(draft.parameters?.duration);
const modelName = getPopiserverModelName(model); const modelName = getPopiserverModelName(model);
return { return {
@ -620,14 +607,11 @@ function buildPopiserverTaskPricePayload(
model: modelName, model: modelName,
aspectRatio, aspectRatio,
ratio: aspectRatio, ratio: aspectRatio,
resolution: resolution: draft.resolution || stringFromUnknown(draft.parameters?.resolution) || (type === 2 ? DEFAULT_VIDEO_RESOLUTION : "2K"),
stringFromUnknown(draft.parameters?.resolution) ||
draft.resolution ||
(type === 2 ? DEFAULT_VIDEO_RESOLUTION : "2K"),
duration: duration ?? undefined, duration: duration ?? undefined,
width: finiteIntegerFromUnknown(draft.parameters?.width) ?? dimensions.width, width: dimensions.width,
height: finiteIntegerFromUnknown(draft.parameters?.height) ?? dimensions.height, height: dimensions.height,
batchSize: finiteIntegerFromUnknown(draft.parameters?.batchSize) ?? (type === 1 ? draft.imageCount : 1), batchSize: type === 1 ? draft.imageCount : 1,
}; };
} }
@ -925,6 +909,20 @@ function readDraftFromContext(context: ComposerContext): ComposerDraft {
return adapter && context.node ? adapter.readDraft(context.node) : createEmptyDraft(); return adapter && context.node ? adapter.readDraft(context.node) : createEmptyDraft();
} }
function composerDraftShallowEqual(a: ComposerDraft, b: ComposerDraft): boolean {
return (
a.prompt === b.prompt &&
a.aspectRatio === b.aspectRatio &&
a.resolution === b.resolution &&
a.selectedModel === b.selectedModel &&
a.inputImages === b.inputImages &&
a.imageCount === b.imageCount &&
a.durationSeconds === b.durationSeconds &&
a.soundEnabled === b.soundEnabled &&
a.parameters === b.parameters
);
}
function nodeDraftFingerprint(context: ComposerContext): string { function nodeDraftFingerprint(context: ComposerContext): string {
if (context.mode !== "node-edit" || !context.node) return contextIdentity(context); if (context.mode !== "node-edit" || !context.node) return contextIdentity(context);
const data = context.node.data as NanoBananaNodeData | GenerateVideoNodeData | GenerateAudioNodeData; const data = context.node.data as NanoBananaNodeData | GenerateVideoNodeData | GenerateAudioNodeData;
@ -1051,6 +1049,7 @@ export function GenerationComposer() {
const identity = contextIdentity(context); const identity = contextIdentity(context);
const fingerprint = nodeDraftFingerprint(context); const fingerprint = nodeDraftFingerprint(context);
const [draft, setDraft] = useState(() => readDraftFromContext(context)); const [draft, setDraft] = useState(() => readDraftFromContext(context));
const [draftIdentity, setDraftIdentity] = useState(() => identity);
const [dirtyFields, setDirtyFields] = useState<Set<DraftField>>(() => new Set()); const [dirtyFields, setDirtyFields] = useState<Set<DraftField>>(() => new Set());
const [assistMenu, setAssistMenu] = useState<"command" | "reference" | null>(null); const [assistMenu, setAssistMenu] = useState<"command" | "reference" | null>(null);
const [modelDialogOpen, setModelDialogOpen] = useState(false); const [modelDialogOpen, setModelDialogOpen] = useState(false);
@ -1114,7 +1113,16 @@ export function GenerationComposer() {
flushDraftForContext(previousContext); flushDraftForContext(previousContext);
contextRef.current = context; contextRef.current = context;
setDraft(readDraftFromContext(context)); const nextDraft = readDraftFromContext(context);
setDraft((current) => {
if (composerDraftShallowEqual(current, nextDraft)) {
draftRef.current = current;
return current;
}
draftRef.current = nextDraft;
return nextDraft;
});
setDraftIdentity(identity);
clearDirtyFields(); clearDirtyFields();
}, [clearDirtyFields, context, flushDraftForContext, identity]); }, [clearDirtyFields, context, flushDraftForContext, identity]);
@ -1123,7 +1131,7 @@ export function GenerationComposer() {
const nextDraft = readDraftFromContext(context); const nextDraft = readDraftFromContext(context);
setDraft((current) => { setDraft((current) => {
const dirty = dirtyFieldsRef.current; const dirty = dirtyFieldsRef.current;
return { const mergedDraft = {
prompt: dirty.has("prompt") ? current.prompt : nextDraft.prompt, prompt: dirty.has("prompt") ? current.prompt : nextDraft.prompt,
aspectRatio: dirty.has("aspectRatio") ? current.aspectRatio : nextDraft.aspectRatio, aspectRatio: dirty.has("aspectRatio") ? current.aspectRatio : nextDraft.aspectRatio,
resolution: dirty.has("resolution") ? current.resolution : nextDraft.resolution, resolution: dirty.has("resolution") ? current.resolution : nextDraft.resolution,
@ -1134,7 +1142,14 @@ export function GenerationComposer() {
soundEnabled: dirty.has("soundEnabled") ? current.soundEnabled : nextDraft.soundEnabled, soundEnabled: dirty.has("soundEnabled") ? current.soundEnabled : nextDraft.soundEnabled,
parameters: dirty.has("parameters") ? current.parameters : nextDraft.parameters, parameters: dirty.has("parameters") ? current.parameters : nextDraft.parameters,
}; };
if (composerDraftShallowEqual(current, mergedDraft)) {
draftRef.current = current;
return current;
}
draftRef.current = mergedDraft;
return mergedDraft;
}); });
setDraftIdentity(identity);
}, [context, fingerprint]); }, [context, fingerprint]);
const connectedInputs = useMemo(() => { const connectedInputs = useMemo(() => {
@ -1571,20 +1586,6 @@ export function GenerationComposer() {
? context.node?.data as NanoBananaNodeData | GenerateVideoNodeData | GenerateAudioNodeData | undefined ? context.node?.data as NanoBananaNodeData | GenerateVideoNodeData | GenerateAudioNodeData | undefined
: undefined; : undefined;
const activeParameterSchema = generationNodeData?.parameterSchema ?? []; const activeParameterSchema = generationNodeData?.parameterSchema ?? [];
useEffect(() => {
if (context.mode !== "node-edit" || !context.node || activeParameterSchema.length === 0) return;
const normalized = applySchemaParameterDefaults(activeParameterSchema, draftRef.current.parameters);
if (!normalized.changed) return;
setDraft((current) => {
const currentNormalized = applySchemaParameterDefaults(activeParameterSchema, current.parameters);
if (!currentNormalized.changed) return current;
const next = { ...current, parameters: currentNormalized.parameters };
draftRef.current = next;
return next;
});
updateNodeData(context.node.id, { parameters: normalized.parameters });
}, [activeParameterSchema, context, updateNodeData]);
const gatewayDimensionSelects = buildGatewayDimensionSelects(activeParameterSchema, draft); const gatewayDimensionSelects = buildGatewayDimensionSelects(activeParameterSchema, draft);
const imageCountOptions = getImageCountOptions(activeParameterSchema); const imageCountOptions = getImageCountOptions(activeParameterSchema);
const visibleImageCountOptions = imageCountOptions.length > 0 const visibleImageCountOptions = imageCountOptions.length > 0
@ -1592,7 +1593,7 @@ export function GenerationComposer() {
: DEFAULT_IMAGE_COUNT_OPTIONS; : DEFAULT_IMAGE_COUNT_OPTIONS;
const imageCountParameterName = getSchemaParameterName(activeParameterSchema, IMAGE_COUNT_PARAMETER_NAMES); const imageCountParameterName = getSchemaParameterName(activeParameterSchema, IMAGE_COUNT_PARAMETER_NAMES);
const selectedImageCountValue = imageCountParameterName const selectedImageCountValue = imageCountParameterName
? Number(draft.parameters?.[imageCountParameterName] ?? draft.imageCount) ? draft.imageCount
: draft.imageCount; : draft.imageCount;
const videoSoundParameterName = getSchemaParameterName(activeParameterSchema, VIDEO_SOUND_PARAMETER_NAMES); const videoSoundParameterName = getSchemaParameterName(activeParameterSchema, VIDEO_SOUND_PARAMETER_NAMES);
const audioVoiceSelect = useMemo(() => { const audioVoiceSelect = useMemo(() => {
@ -1733,11 +1734,18 @@ export function GenerationComposer() {
}, []); }, []);
const updateSchemaDraftParameter = useCallback( const updateSchemaDraftParameter = useCallback(
(name: string, value: unknown, patch: Partial<ComposerDraft> = {}, fields: DraftField[] = []) => { (
name: string,
value: unknown,
patch: Partial<ComposerDraft> = {},
fields: DraftField[] = [],
expectedContextIdentity?: string
) => {
const parameters = updateParameterValue(draftRef.current.parameters, name, value); const parameters = updateParameterValue(draftRef.current.parameters, name, value);
const nextPatch = { ...patch, parameters }; const nextPatch = { ...patch, parameters };
const updatedFields: DraftField[] = [...fields, "parameters"]; const updatedFields: DraftField[] = [...fields, "parameters"];
const activeContext = contextRef.current; const activeContext = contextRef.current;
if (expectedContextIdentity && contextIdentity(activeContext) !== expectedContextIdentity) return;
setDraft((current) => { setDraft((current) => {
const next = { ...current, ...nextPatch }; const next = { ...current, ...nextPatch };
@ -1770,14 +1778,15 @@ export function GenerationComposer() {
const resolutionSelect = gatewayDimensionSelects.find((select) => select.parameter.name === "resolution"); const resolutionSelect = gatewayDimensionSelects.find((select) => select.parameter.name === "resolution");
if ( if (
isProcessNode || isProcessNode ||
activeCapability !== "image" || (activeCapability !== "image" && activeCapability !== "video") ||
draftIdentity !== identity ||
!resolutionSelect || !resolutionSelect ||
resolutionSelect.options.length === 0 resolutionSelect.options.length === 0
) { ) {
return; return;
} }
const currentResolution = draft.parameters?.[resolutionSelect.parameter.name] ?? draft.resolution; const currentResolution = draft.resolution ?? draft.parameters?.[resolutionSelect.parameter.name];
if (valueMatchesSchemaOptions( if (valueMatchesSchemaOptions(
currentResolution, currentResolution,
resolutionSelect.options.map((option) => String(option.value)) resolutionSelect.options.map((option) => String(option.value))
@ -1792,14 +1801,17 @@ export function GenerationComposer() {
resolutionSelect.parameter.name, resolutionSelect.parameter.name,
nextResolution, nextResolution,
{ resolution: String(nextResolution) }, { resolution: String(nextResolution) },
["resolution"] ["resolution"],
identity
); );
}, [ }, [
activeCapability, activeCapability,
activeParameterSchema, activeParameterSchema,
draft.parameters, draft.parameters,
draft.resolution, draft.resolution,
draftIdentity,
gatewayDimensionSelects, gatewayDimensionSelects,
identity,
isProcessNode, isProcessNode,
updateSchemaDraftParameter, updateSchemaDraftParameter,
]); ]);
@ -1832,6 +1844,8 @@ export function GenerationComposer() {
const activeContext = contextRef.current; const activeContext = contextRef.current;
const nextResolution = modelSupportsCapability(nextModel, "video") const nextResolution = modelSupportsCapability(nextModel, "video")
? DEFAULT_VIDEO_RESOLUTION ? DEFAULT_VIDEO_RESOLUTION
: modelSupportsCapability(nextModel, "image")
? "2K"
: draftRef.current.resolution; : draftRef.current.resolution;
if (activeContext.mode === "node-edit") { if (activeContext.mode === "node-edit") {
@ -1857,37 +1871,48 @@ export function GenerationComposer() {
? DEFAULT_VIDEO_DURATION_SECONDS ? DEFAULT_VIDEO_DURATION_SECONDS
: draftRef.current.durationSeconds, : draftRef.current.durationSeconds,
soundEnabled: getDefaultVideoSoundEnabled(nextModel), soundEnabled: getDefaultVideoSoundEnabled(nextModel),
parameters: {},
}, },
new Set<DraftField>(["selectedModel"]) new Set<DraftField>(["selectedModel", "resolution", "parameters"])
); );
updateNodeData(activeContext.node.id, modelPatch); updateNodeData(activeContext.node.id, modelPatch);
setDraft((current) => ({ setDraft((current) => {
...current, const next = {
selectedModel: nextModel, ...current,
resolution: nextResolution, selectedModel: nextModel,
durationSeconds: modelSupportsCapability(nextModel, "video") resolution: nextResolution,
? DEFAULT_VIDEO_DURATION_SECONDS durationSeconds: modelSupportsCapability(nextModel, "video")
: current.durationSeconds, ? DEFAULT_VIDEO_DURATION_SECONDS
soundEnabled: getDefaultVideoSoundEnabled(nextModel), : current.durationSeconds,
parameters: {}, soundEnabled: getDefaultVideoSoundEnabled(nextModel),
})); parameters: {},
};
draftRef.current = next;
return next;
});
clearDirtyFields(); clearDirtyFields();
return; return;
} }
if (activeContext.mode === "empty-create") { if (activeContext.mode === "empty-create") {
setDraft((current) => ({ setDraft((current) => {
...current, const next = {
selectedModel: nextModel, ...current,
resolution: modelSupportsCapability(nextModel, "video") selectedModel: nextModel,
? DEFAULT_VIDEO_RESOLUTION resolution: modelSupportsCapability(nextModel, "video")
: current.resolution, ? DEFAULT_VIDEO_RESOLUTION
durationSeconds: modelSupportsCapability(nextModel, "video") : modelSupportsCapability(nextModel, "image")
? DEFAULT_VIDEO_DURATION_SECONDS ? "2K"
: current.durationSeconds, : current.resolution,
soundEnabled: getDefaultVideoSoundEnabled(nextModel), durationSeconds: modelSupportsCapability(nextModel, "video")
parameters: {}, ? DEFAULT_VIDEO_DURATION_SECONDS
})); : current.durationSeconds,
soundEnabled: getDefaultVideoSoundEnabled(nextModel),
parameters: {},
};
draftRef.current = next;
return next;
});
} }
}, },
[clearDirtyFields, updateNodeData] [clearDirtyFields, updateNodeData]

2
src/components/nodes/GenerateAudioNode.tsx

@ -25,7 +25,6 @@ import { useI18n } from "@/i18n";
import { formatUserFacingError } from "@/utils/userFacingErrors"; import { formatUserFacingError } from "@/utils/userFacingErrors";
import { chooseModelFromList, shouldUpdateSelectedModelFromProviderModel, toSelectedModel } from "@/utils/modelSelection"; import { chooseModelFromList, shouldUpdateSelectedModelFromProviderModel, toSelectedModel } from "@/utils/modelSelection";
import { useModelStore } from "@/store/modelStore"; import { useModelStore } from "@/store/modelStore";
import { buildDefaultParametersFromSchema } from "@/utils/modelSchemaDefaults";
import { withSpeechVoiceParameter } from "@/utils/speechVoiceParameter"; import { withSpeechVoiceParameter } from "@/utils/speechVoiceParameter";
type GenerateAudioNodeType = Node<GenerateAudioNodeData, "generateAudio">; type GenerateAudioNodeType = Node<GenerateAudioNodeData, "generateAudio">;
@ -149,7 +148,6 @@ export function GenerateAudioNode({ id, data, selected }: NodeProps<GenerateAudi
updateNodeData(id, { updateNodeData(id, {
inputSchema: inputs, inputSchema: inputs,
parameterSchema: nextParameterSchema, parameterSchema: nextParameterSchema,
parameters: buildDefaultParametersFromSchema(nextParameterSchema),
...(model && shouldUpdateSelectedModelFromProviderModel(nodeData.selectedModel, model) ...(model && shouldUpdateSelectedModelFromProviderModel(nodeData.selectedModel, model)
? { selectedModel: toSelectedModel(model) } ? { selectedModel: toSelectedModel(model) }
: {}), : {}),

3
src/components/nodes/GenerateImageNode.tsx

@ -27,7 +27,6 @@ import { MediaPreviewModal } from "@/components/MediaPreviewModal";
import { createGeminiLegacyImageModel } from "@/store/utils/defaultImageModel"; import { createGeminiLegacyImageModel } from "@/store/utils/defaultImageModel";
import { getProviderDisplayName, isPopiProviderMode, POPI_PROVIDER_ID } from "@/lib/providerMode"; import { getProviderDisplayName, isPopiProviderMode, POPI_PROVIDER_ID } from "@/lib/providerMode";
import { chooseModelFromList, shouldUpdateSelectedModelFromProviderModel, toSelectedModel } from "@/utils/modelSelection"; import { chooseModelFromList, shouldUpdateSelectedModelFromProviderModel, toSelectedModel } from "@/utils/modelSelection";
import { buildImageDefaultsFromSchema } from "@/utils/modelSchemaDefaults";
import { useModelStore } from "@/store/modelStore"; import { useModelStore } from "@/store/modelStore";
import { import {
isBrowserFileSystemPath, isBrowserFileSystemPath,
@ -516,11 +515,9 @@ export function GenerateImageNode({ id, data, selected }: NodeProps<NanoBananaNo
enabled: Boolean(nodeData.selectedModel?.modelId) && !usesFirstClassImageControls && canFetchPopiModelSchema, enabled: Boolean(nodeData.selectedModel?.modelId) && !usesFirstClassImageControls && canFetchPopiModelSchema,
refreshKey: nodeData.modelSchemaRequestId, refreshKey: nodeData.modelSchemaRequestId,
onLoaded: ({ inputs, parameters, model }) => { onLoaded: ({ inputs, parameters, model }) => {
const schemaDefaults = buildImageDefaultsFromSchema(parameters);
updateNodeData(id, { updateNodeData(id, {
inputSchema: inputs, inputSchema: inputs,
parameterSchema: parameters, parameterSchema: parameters,
...schemaDefaults,
...(model && shouldUpdateSelectedModelFromProviderModel(nodeData.selectedModel, model) ...(model && shouldUpdateSelectedModelFromProviderModel(nodeData.selectedModel, model)
? { selectedModel: toSelectedModel(model) } ? { selectedModel: toSelectedModel(model) }
: {}), : {}),

3
src/components/nodes/GenerateVideoNode.tsx

@ -27,7 +27,6 @@ import { useI18n } from "@/i18n";
import { MediaPreviewModal } from "@/components/MediaPreviewModal"; import { MediaPreviewModal } from "@/components/MediaPreviewModal";
import { getProviderDisplayName, isPopiProviderMode, POPI_PROVIDER_ID } from "@/lib/providerMode"; import { getProviderDisplayName, isPopiProviderMode, POPI_PROVIDER_ID } from "@/lib/providerMode";
import { chooseModelFromList, shouldUpdateSelectedModelFromProviderModel, toSelectedModel } from "@/utils/modelSelection"; import { chooseModelFromList, shouldUpdateSelectedModelFromProviderModel, toSelectedModel } from "@/utils/modelSelection";
import { buildVideoDefaultsFromSchema } from "@/utils/modelSchemaDefaults";
import { useModelStore } from "@/store/modelStore"; import { useModelStore } from "@/store/modelStore";
import { formatUserFacingError } from "@/utils/userFacingErrors"; import { formatUserFacingError } from "@/utils/userFacingErrors";
import { import {
@ -343,11 +342,9 @@ export function GenerateVideoNode({ id, data, selected }: NodeProps<GenerateVide
enabled: Boolean(nodeData.selectedModel?.modelId) && canFetchPopiModelSchema, enabled: Boolean(nodeData.selectedModel?.modelId) && canFetchPopiModelSchema,
refreshKey: nodeData.modelSchemaRequestId, refreshKey: nodeData.modelSchemaRequestId,
onLoaded: ({ inputs, parameters, model }) => { onLoaded: ({ inputs, parameters, model }) => {
const schemaDefaults = buildVideoDefaultsFromSchema(parameters);
updateNodeData(id, { updateNodeData(id, {
inputSchema: inputs, inputSchema: inputs,
parameterSchema: parameters, parameterSchema: parameters,
...schemaDefaults,
...(model && shouldUpdateSelectedModelFromProviderModel(nodeData.selectedModel, model) ...(model && shouldUpdateSelectedModelFromProviderModel(nodeData.selectedModel, model)
? { selectedModel: toSelectedModel(model) } ? { selectedModel: toSelectedModel(model) }
: {}), : {}),

6
src/store/execution/generateAudioExecutor.ts

@ -11,6 +11,7 @@ import { pollGenerateTask } from "./pollTaskCompletion";
import { runWithFallback } from "./runWithFallback"; import { runWithFallback } from "./runWithFallback";
import type { NodeExecutionContext } from "./types"; import type { NodeExecutionContext } from "./types";
import { uploadPopiserverDynamicInputsForGeneration } from "@/utils/temporaryImageUpload"; import { uploadPopiserverDynamicInputsForGeneration } from "@/utils/temporaryImageUpload";
import { buildDefaultParametersFromSchema, mergeSchemaDefaults } from "@/utils/modelSchemaDefaults";
export interface GenerateAudioOptions { export interface GenerateAudioOptions {
/** When true, falls back to stored inputPrompt if no connections provide it. */ /** When true, falls back to stored inputPrompt if no connections provide it. */
@ -83,7 +84,10 @@ export async function executeGenerateAudio(
images: [], images: [],
prompt: text, prompt: text,
selectedModel: modelToUse, selectedModel: modelToUse,
parameters: parametersOverride ?? nodeData.parameters, parameters: mergeSchemaDefaults(
buildDefaultParametersFromSchema(nodeData.parameterSchema ?? []),
parametersOverride ?? nodeData.parameters
),
dynamicInputs: preparedDynamicInputs.dynamicInputs, dynamicInputs: preparedDynamicInputs.dynamicInputs,
mediaType: "audio" as const, mediaType: "audio" as const,
}; };

10
src/store/execution/generateVideoExecutor.ts

@ -32,7 +32,8 @@ import {
uploadTemporaryDynamicInputsForGeneration, uploadTemporaryDynamicInputsForGeneration,
uploadTemporaryImagesForGeneration, uploadTemporaryImagesForGeneration,
} from "@/utils/temporaryImageUpload"; } from "@/utils/temporaryImageUpload";
import { mergeConfiguredParameters } from "./generationParameters"; import { mergeConfiguredParametersOverride } from "./generationParameters";
import { buildDefaultParametersFromSchema, mergeSchemaDefaults } from "@/utils/modelSchemaDefaults";
export interface GenerateVideoOptions { export interface GenerateVideoOptions {
/** When true, falls back to stored inputImages/inputPrompt if no connections provide them. */ /** When true, falls back to stored inputImages/inputPrompt if no connections provide them. */
@ -44,13 +45,16 @@ function buildVideoParameters(
modelToUse: SelectedModel, modelToUse: SelectedModel,
parametersOverride?: Record<string, unknown> parametersOverride?: Record<string, unknown>
): Record<string, unknown> | undefined { ): Record<string, unknown> | undefined {
const parameters = { ...(parametersOverride ?? nodeData.parameters ?? {}) }; const parameters = mergeSchemaDefaults(
buildDefaultParametersFromSchema(nodeData.parameterSchema ?? []),
parametersOverride ?? nodeData.parameters
);
const durationSeconds = normalizeVideoDurationSeconds(nodeData.durationSeconds); const durationSeconds = normalizeVideoDurationSeconds(nodeData.durationSeconds);
const resolution = nodeData.resolution ?? DEFAULT_VIDEO_RESOLUTION; const resolution = nodeData.resolution ?? DEFAULT_VIDEO_RESOLUTION;
const soundParameterName = getVideoSoundParameterName(modelToUse); const soundParameterName = getVideoSoundParameterName(modelToUse);
if (modelToUse.provider === "popiserver") { if (modelToUse.provider === "popiserver") {
const merged = mergeConfiguredParameters(parameters, { const merged = mergeConfiguredParametersOverride(parameters, {
...(nodeData.aspectRatio ? { aspectRatio: nodeData.aspectRatio, aspect_ratio: nodeData.aspectRatio } : {}), ...(nodeData.aspectRatio ? { aspectRatio: nodeData.aspectRatio, aspect_ratio: nodeData.aspectRatio } : {}),
resolution, resolution,
duration: durationSeconds, duration: durationSeconds,

13
src/store/execution/generationParameters.ts

@ -30,6 +30,19 @@ export function mergeConfiguredParameters(
return parameters; return parameters;
} }
export function mergeConfiguredParametersOverride(
base: Record<string, unknown> | undefined,
configured: Record<string, unknown>
): Record<string, unknown> {
const parameters = { ...(base ?? {}) };
for (const [key, value] of Object.entries(configured)) {
if (value !== undefined && value !== null) {
parameters[key] = value;
}
}
return parameters;
}
export function resolveImageGenerationCount( export function resolveImageGenerationCount(
parameters: Record<string, unknown> | undefined, parameters: Record<string, unknown> | undefined,
nodeImageCount: number | undefined nodeImageCount: number | undefined

22
src/store/execution/nanoBananaExecutor.ts

@ -35,10 +35,10 @@ import {
uploadTemporaryImagesForGeneration, uploadTemporaryImagesForGeneration,
} from "@/utils/temporaryImageUpload"; } from "@/utils/temporaryImageUpload";
import { import {
mergeConfiguredParameters, mergeConfiguredParametersOverride,
mergeImageGenerationParameters,
resolveImageGenerationCount, resolveImageGenerationCount,
} from "./generationParameters"; } from "./generationParameters";
import { buildDefaultParametersFromSchema, mergeSchemaDefaults } from "@/utils/modelSchemaDefaults";
export interface NanoBananaOptions { export interface NanoBananaOptions {
/** When true, falls back to stored inputImages/inputPrompt if no connections provide them. */ /** When true, falls back to stored inputImages/inputPrompt if no connections provide them. */
@ -166,10 +166,11 @@ export async function executeNanoBanana(
...preparedDynamicInputs.cleanupIds, ...preparedDynamicInputs.cleanupIds,
]; ];
const requestedImageCount = resolveImageGenerationCount( const baseParameters = mergeSchemaDefaults(
parametersOverride ?? nodeData.parameters, buildDefaultParametersFromSchema(nodeData.parameterSchema ?? []),
nodeData.imageCount parametersOverride ?? nodeData.parameters
); );
const requestedImageCount = resolveImageGenerationCount(baseParameters, nodeData.imageCount);
const configuredParameters = { const configuredParameters = {
aspectRatio: (parametersOverride?.aspectRatio as string | undefined) ?? nodeData.aspectRatio, aspectRatio: (parametersOverride?.aspectRatio as string | undefined) ?? nodeData.aspectRatio,
aspect_ratio: (parametersOverride?.aspectRatio as string | undefined) ?? nodeData.aspectRatio, aspect_ratio: (parametersOverride?.aspectRatio as string | undefined) ?? nodeData.aspectRatio,
@ -179,12 +180,11 @@ export async function executeNanoBanana(
useImageSearch: (parametersOverride?.useImageSearch as boolean | undefined) ?? nodeData.useImageSearch, useImageSearch: (parametersOverride?.useImageSearch as boolean | undefined) ?? nodeData.useImageSearch,
}; };
const effectiveParameters = provider === "popiserver" const effectiveParameters = provider === "popiserver"
? mergeImageGenerationParameters( ? {
parametersOverride ?? nodeData.parameters, ...mergeConfiguredParametersOverride(baseParameters, configuredParameters),
configuredParameters, batchSize: requestedImageCount,
requestedImageCount }
) : mergeConfiguredParametersOverride(baseParameters, configuredParameters);
: mergeConfiguredParameters(parametersOverride ?? nodeData.parameters, configuredParameters);
const requestPayload = { const requestPayload = {
images: preparedImages.images, images: preparedImages.images,
prompt: finalPrompt, prompt: finalPrompt,

31
src/utils/__tests__/modelSchemaDefaults.test.ts

@ -0,0 +1,31 @@
import { describe, expect, it } from "vitest";
import { mergeSchemaDefaults } from "@/utils/modelSchemaDefaults";
describe("mergeSchemaDefaults", () => {
it("fills missing schema defaults without overwriting saved parameters", () => {
expect(
mergeSchemaDefaults(
{
resolution: "720p",
duration: 5,
quality: "standard",
},
{
resolution: "1080p",
prompt_strength: 0.8,
}
)
).toEqual({
resolution: "1080p",
duration: 5,
quality: "standard",
prompt_strength: 0.8,
});
});
it("returns defaults when there are no saved parameters", () => {
expect(mergeSchemaDefaults({ voice: "default" }, undefined)).toEqual({
voice: "default",
});
});
});

10
src/utils/modelSchemaDefaults.ts

@ -26,6 +26,16 @@ export function buildDefaultParametersFromSchema(schema: ModelParameter[]): Reco
return parameters; return parameters;
} }
export function mergeSchemaDefaults(
defaults: Record<string, unknown>,
existing: Record<string, unknown> | undefined
): Record<string, unknown> {
return {
...defaults,
...(existing ?? {}),
};
}
export function buildImageDefaultsFromSchema(schema: ModelParameter[]): { export function buildImageDefaultsFromSchema(schema: ModelParameter[]): {
aspectRatio?: AspectRatio; aspectRatio?: AspectRatio;
resolution?: string; resolution?: string;

Loading…
Cancel
Save