fix(platform): 模型能力 - 修复图片时长误判与后台默认参数同步
This commit is contained in:
@@ -7,8 +7,8 @@ import { configuredModelMatchesCapability, defaultConfig, modelOptionName, resol
|
||||
import { CreditSymbol, requestCreditCost } from "@/constant/credits";
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { normalizeVideoDuration, normalizeVideoResolution } from "@/lib/video-generation-options";
|
||||
import { modelCapabilityConfigFor, normalizeImageValue, normalizeVideoValue } from "@/lib/model-capabilities";
|
||||
import { modelCompatibilityError, modelRequestOptions, resolveCompatibleModel, defaultImageParamsForModel, type ModelRequirements } from "@/lib/model-selection";
|
||||
import { modelCapabilityConfigFor } from "@/lib/model-capabilities";
|
||||
import { modelCompatibilityError, modelRequestOptions, resolveCompatibleModel, resolveModelGenerationDefaults, defaultImageParamsForModel, type ModelRequirements } from "@/lib/model-selection";
|
||||
import { navigateToSettings } from "@/lib/settings-navigation";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
@@ -50,8 +50,20 @@ export function CanvasConfigNodePanel({ node, isRunning, inputSummary, onConfigC
|
||||
capability: mode,
|
||||
input: inputSummary,
|
||||
videoOperation: node.metadata?.videoEditOperation,
|
||||
videoSeconds: node.metadata?.seconds || globalConfig.videoSeconds,
|
||||
options: modelRequestOptions({ ...globalConfig, size: node.metadata?.size || globalConfig.size, quality: node.metadata?.quality || globalConfig.quality, count: String(node.metadata?.count || globalConfig.count), videoSeconds: node.metadata?.seconds || globalConfig.videoSeconds, vquality: node.metadata?.vquality || globalConfig.vquality, videoGenerateAudio: node.metadata?.generateAudio || globalConfig.videoGenerateAudio, videoWatermark: node.metadata?.watermark || globalConfig.videoWatermark, audioVoice: node.metadata?.audioVoice || globalConfig.audioVoice, audioFormat: node.metadata?.audioFormat || globalConfig.audioFormat, audioSpeed: node.metadata?.audioSpeed || globalConfig.audioSpeed }, mode),
|
||||
videoSeconds: mode === "video" ? node.metadata?.seconds || globalConfig.videoSeconds : undefined,
|
||||
options: modelRequestOptions({
|
||||
...globalConfig,
|
||||
size: node.metadata?.size || globalConfig.size,
|
||||
quality: node.metadata?.quality || globalConfig.quality,
|
||||
count: String(node.metadata?.count || globalConfig.count),
|
||||
videoSeconds: node.metadata?.seconds || globalConfig.videoSeconds,
|
||||
vquality: node.metadata?.vquality || globalConfig.vquality,
|
||||
videoGenerateAudio: node.metadata?.generateAudio || globalConfig.videoGenerateAudio,
|
||||
videoWatermark: node.metadata?.watermark || globalConfig.videoWatermark,
|
||||
audioVoice: node.metadata?.audioVoice || globalConfig.audioVoice,
|
||||
audioFormat: node.metadata?.audioFormat || globalConfig.audioFormat,
|
||||
audioSpeed: node.metadata?.audioSpeed || globalConfig.audioSpeed,
|
||||
}, mode),
|
||||
};
|
||||
const config = buildNodeConfig(globalConfig, node, mode, requirements);
|
||||
const videoProfile = mode === "video" ? modelCapabilityConfigFor(config, config.model).video! : undefined;
|
||||
@@ -229,25 +241,51 @@ function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode: Can
|
||||
const storedModel = node.metadata?.model;
|
||||
const preferredModel = storedModel && configuredModelMatchesCapability(globalConfig, storedModel, mode) ? storedModel : defaultModel && configuredModelMatchesCapability(globalConfig, defaultModel, mode) ? defaultModel : fallbackModel;
|
||||
const model = resolveCompatibleModel(globalConfig, preferredModel, mode === "image" ? { ...requirements, imageSize: node.metadata?.size || globalConfig.size || defaultConfig.size } : requirements) || preferredModel;
|
||||
const imageProfile = mode === "image" ? modelCapabilityConfigFor(globalConfig, model).image! : undefined;
|
||||
const normalizedImage = imageProfile ? normalizeImageValue(imageProfile, { size: node.metadata?.size || globalConfig.size || defaultConfig.size, quality: node.metadata?.quality || globalConfig.quality || defaultConfig.quality, transparentBackground: node.metadata?.transparentBackground || globalConfig.transparentBackground, count: String(node.metadata?.count || globalConfig.canvasImageCount || globalConfig.count || defaultConfig.count) }) : undefined;
|
||||
const generationDefaults = resolveModelGenerationDefaults(
|
||||
globalConfig,
|
||||
model,
|
||||
mode === "image" ? "image" : mode === "video" ? "video" : undefined,
|
||||
mode === "image"
|
||||
? {
|
||||
size: node.metadata?.size,
|
||||
quality: node.metadata?.quality,
|
||||
transparentBackground: node.metadata?.transparentBackground,
|
||||
count: String(node.metadata?.count || globalConfig.canvasImageCount || globalConfig.count || defaultConfig.count),
|
||||
}
|
||||
: {
|
||||
size: node.metadata?.size,
|
||||
videoSeconds: node.metadata?.seconds,
|
||||
vquality: node.metadata?.vquality,
|
||||
videoGenerateAudio: node.metadata?.generateAudio,
|
||||
videoWatermark: node.metadata?.watermark,
|
||||
},
|
||||
{
|
||||
size: globalConfig.size || defaultConfig.size,
|
||||
quality: globalConfig.quality || defaultConfig.quality,
|
||||
transparentBackground: globalConfig.transparentBackground || defaultConfig.transparentBackground,
|
||||
count: String(globalConfig.canvasImageCount || globalConfig.count || defaultConfig.count),
|
||||
videoSeconds: globalConfig.videoSeconds || defaultConfig.videoSeconds,
|
||||
vquality: globalConfig.vquality || defaultConfig.vquality,
|
||||
videoGenerateAudio: globalConfig.videoGenerateAudio || defaultConfig.videoGenerateAudio,
|
||||
videoWatermark: globalConfig.videoWatermark || defaultConfig.videoWatermark,
|
||||
},
|
||||
);
|
||||
const videoProfile = mode === "video" ? modelCapabilityConfigFor(globalConfig, model).video! : undefined;
|
||||
const normalizedVideo = videoProfile ? normalizeVideoValue(videoProfile, { seconds: node.metadata?.seconds || globalConfig.videoSeconds || defaultConfig.videoSeconds, ratio: node.metadata?.size || globalConfig.size || defaultConfig.size, resolution: node.metadata?.vquality || globalConfig.vquality || defaultConfig.vquality }) : undefined;
|
||||
return {
|
||||
...globalConfig,
|
||||
model,
|
||||
quality: normalizedImage?.quality || node.metadata?.quality || globalConfig.quality || defaultConfig.quality,
|
||||
size: normalizedImage?.size || normalizedVideo?.ratio || node.metadata?.size || globalConfig.size || defaultConfig.size,
|
||||
transparentBackground: normalizedImage?.transparentBackground || ((node.metadata?.transparentBackground || globalConfig.transparentBackground) === "true" ? "true" : "false"),
|
||||
videoSeconds: normalizedVideo?.seconds || normalizeVideoDuration(node.metadata?.seconds || globalConfig.videoSeconds || defaultConfig.videoSeconds),
|
||||
vquality: normalizedVideo?.resolution.replace(/p$/i, "") || normalizeVideoResolution(node.metadata?.vquality || globalConfig.vquality || defaultConfig.vquality),
|
||||
videoGenerateAudio: videoProfile?.generateAudio.supported ? node.metadata?.generateAudio || globalConfig.videoGenerateAudio || String(videoProfile.generateAudio.default) : "false",
|
||||
videoWatermark: videoProfile?.watermark.supported ? node.metadata?.watermark || globalConfig.videoWatermark || String(videoProfile.watermark.default) : "false",
|
||||
quality: generationDefaults.quality || globalConfig.quality || defaultConfig.quality,
|
||||
size: generationDefaults.size || globalConfig.size || defaultConfig.size,
|
||||
transparentBackground: generationDefaults.transparentBackground || "false",
|
||||
videoSeconds: generationDefaults.videoSeconds || normalizeVideoDuration(globalConfig.videoSeconds || defaultConfig.videoSeconds),
|
||||
vquality: generationDefaults.vquality || normalizeVideoResolution(globalConfig.vquality || defaultConfig.vquality),
|
||||
videoGenerateAudio: videoProfile?.generateAudio.supported ? generationDefaults.videoGenerateAudio || String(videoProfile.generateAudio.default) : "false",
|
||||
videoWatermark: videoProfile?.watermark.supported ? generationDefaults.videoWatermark || String(videoProfile.watermark.default) : "false",
|
||||
audioVoice: node.metadata?.audioVoice || globalConfig.audioVoice || defaultConfig.audioVoice,
|
||||
audioFormat: node.metadata?.audioFormat || globalConfig.audioFormat || defaultConfig.audioFormat,
|
||||
audioSpeed: node.metadata?.audioSpeed || globalConfig.audioSpeed || defaultConfig.audioSpeed,
|
||||
audioInstructions: node.metadata?.audioInstructions || globalConfig.audioInstructions || defaultConfig.audioInstructions,
|
||||
count: normalizedImage?.count || String(node.metadata?.count || (mode === "image" ? globalConfig.canvasImageCount || globalConfig.count : globalConfig.count) || defaultConfig.count),
|
||||
count: generationDefaults.count || String(node.metadata?.count || (mode === "image" ? globalConfig.canvasImageCount || globalConfig.count : globalConfig.count) || defaultConfig.count),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { resolveCanvasGenerationModel } from "@/lib/canvas/canvas-project-genera
|
||||
import { CreditSymbol, requestCreditCost } from "@/constant/credits";
|
||||
import { canvasThemes } from "@/lib/canvas-theme";
|
||||
import { normalizeVideoDuration, normalizeVideoResolution } from "@/lib/video-generation-options";
|
||||
import { modelRequestOptions, resolveCompatibleModel, defaultImageParamsForModel, type ModelRequirements } from "@/lib/model-selection";
|
||||
import { modelRequestOptions, resolveCompatibleModel, resolveModelGenerationDefaults, defaultImageParamsForModel, type ModelRequirements } from "@/lib/model-selection";
|
||||
import { navigateToSettings } from "@/lib/settings-navigation";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
@@ -77,8 +77,20 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
|
||||
characterCount: activeReferences.filter((item) => item.kind === "character").length,
|
||||
},
|
||||
videoOperation: node.metadata?.videoEditOperation,
|
||||
videoSeconds: node.metadata?.seconds || globalConfig.videoSeconds,
|
||||
options: modelRequestOptions({ ...globalConfig, size: node.metadata?.size || globalConfig.size, quality: node.metadata?.quality || globalConfig.quality, count: String(node.metadata?.count || globalConfig.count), videoSeconds: node.metadata?.seconds || globalConfig.videoSeconds, vquality: node.metadata?.vquality || globalConfig.vquality, videoGenerateAudio: node.metadata?.generateAudio || globalConfig.videoGenerateAudio, videoWatermark: node.metadata?.watermark || globalConfig.videoWatermark, audioVoice: node.metadata?.audioVoice || globalConfig.audioVoice, audioFormat: node.metadata?.audioFormat || globalConfig.audioFormat, audioSpeed: node.metadata?.audioSpeed || globalConfig.audioSpeed }, mode),
|
||||
videoSeconds: mode === "video" ? node.metadata?.seconds || globalConfig.videoSeconds : undefined,
|
||||
options: modelRequestOptions({
|
||||
...globalConfig,
|
||||
size: node.metadata?.size || globalConfig.size,
|
||||
quality: node.metadata?.quality || globalConfig.quality,
|
||||
count: String(node.metadata?.count || globalConfig.count),
|
||||
videoSeconds: node.metadata?.seconds || globalConfig.videoSeconds,
|
||||
vquality: node.metadata?.vquality || globalConfig.vquality,
|
||||
videoGenerateAudio: node.metadata?.generateAudio || globalConfig.videoGenerateAudio,
|
||||
videoWatermark: node.metadata?.watermark || globalConfig.videoWatermark,
|
||||
audioVoice: node.metadata?.audioVoice || globalConfig.audioVoice,
|
||||
audioFormat: node.metadata?.audioFormat || globalConfig.audioFormat,
|
||||
audioSpeed: node.metadata?.audioSpeed || globalConfig.audioSpeed,
|
||||
}, mode),
|
||||
};
|
||||
const config = buildNodeConfig(globalConfig, node, mode, requirements);
|
||||
const generationCount = Math.max(1, Math.min(15, Math.floor(Math.abs(Number(config.count)) || 1)));
|
||||
@@ -548,21 +560,50 @@ function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode: Can
|
||||
const fallbackModel = mode === "image" ? defaultConfig.imageModel : mode === "video" ? defaultConfig.videoModel : mode === "audio" ? defaultConfig.audioModel : defaultConfig.textModel;
|
||||
const preferredModel = resolveCanvasGenerationModel(globalConfig, node.metadata?.model, mode) || resolveCanvasGenerationModel(globalConfig, defaultModel, mode) || fallbackModel;
|
||||
const model = resolveCompatibleModel(globalConfig, preferredModel, mode === "image" ? { ...requirements, imageSize: node.metadata?.size || globalConfig.size || defaultConfig.size } : requirements) || preferredModel;
|
||||
const defaults = resolveModelGenerationDefaults(
|
||||
globalConfig,
|
||||
model,
|
||||
mode === "image" ? "image" : mode === "video" ? "video" : undefined,
|
||||
mode === "image"
|
||||
? {
|
||||
size: node.metadata?.size,
|
||||
quality: node.metadata?.quality,
|
||||
transparentBackground: node.metadata?.transparentBackground,
|
||||
count: String(node.metadata?.count || globalConfig.canvasImageCount || globalConfig.count || defaultConfig.count),
|
||||
}
|
||||
: {
|
||||
size: node.metadata?.size,
|
||||
videoSeconds: node.metadata?.seconds,
|
||||
vquality: node.metadata?.vquality,
|
||||
videoGenerateAudio: node.metadata?.generateAudio,
|
||||
videoWatermark: node.metadata?.watermark,
|
||||
},
|
||||
{
|
||||
size: globalConfig.size || defaultConfig.size,
|
||||
quality: globalConfig.quality || defaultConfig.quality,
|
||||
transparentBackground: globalConfig.transparentBackground || defaultConfig.transparentBackground,
|
||||
count: String(globalConfig.canvasImageCount || globalConfig.count || defaultConfig.count),
|
||||
videoSeconds: globalConfig.videoSeconds || defaultConfig.videoSeconds,
|
||||
vquality: globalConfig.vquality || defaultConfig.vquality,
|
||||
videoGenerateAudio: globalConfig.videoGenerateAudio || defaultConfig.videoGenerateAudio,
|
||||
videoWatermark: globalConfig.videoWatermark || defaultConfig.videoWatermark,
|
||||
},
|
||||
);
|
||||
return {
|
||||
...globalConfig,
|
||||
model,
|
||||
quality: node.metadata?.quality || globalConfig.quality || defaultConfig.quality,
|
||||
size: node.metadata?.size || globalConfig.size || defaultConfig.size,
|
||||
transparentBackground: (node.metadata?.transparentBackground || globalConfig.transparentBackground) === "true" ? "true" : "false",
|
||||
videoSeconds: normalizeVideoDuration(node.metadata?.seconds || globalConfig.videoSeconds || defaultConfig.videoSeconds),
|
||||
vquality: normalizeVideoResolution(node.metadata?.vquality || globalConfig.vquality || defaultConfig.vquality),
|
||||
videoGenerateAudio: node.metadata?.generateAudio || globalConfig.videoGenerateAudio || defaultConfig.videoGenerateAudio,
|
||||
videoWatermark: node.metadata?.watermark || globalConfig.videoWatermark || defaultConfig.videoWatermark,
|
||||
quality: defaults.quality || globalConfig.quality || defaultConfig.quality,
|
||||
size: defaults.size || globalConfig.size || defaultConfig.size,
|
||||
transparentBackground: defaults.transparentBackground || "false",
|
||||
videoSeconds: defaults.videoSeconds || normalizeVideoDuration(globalConfig.videoSeconds || defaultConfig.videoSeconds),
|
||||
vquality: defaults.vquality || normalizeVideoResolution(globalConfig.vquality || defaultConfig.vquality),
|
||||
videoGenerateAudio: defaults.videoGenerateAudio || globalConfig.videoGenerateAudio || defaultConfig.videoGenerateAudio,
|
||||
videoWatermark: defaults.videoWatermark || globalConfig.videoWatermark || defaultConfig.videoWatermark,
|
||||
audioVoice: node.metadata?.audioVoice || globalConfig.audioVoice || defaultConfig.audioVoice,
|
||||
audioFormat: node.metadata?.audioFormat || globalConfig.audioFormat || defaultConfig.audioFormat,
|
||||
audioSpeed: node.metadata?.audioSpeed || globalConfig.audioSpeed || defaultConfig.audioSpeed,
|
||||
audioInstructions: node.metadata?.audioInstructions || globalConfig.audioInstructions || defaultConfig.audioInstructions,
|
||||
count: String(node.metadata?.count || (mode === "image" ? globalConfig.canvasImageCount || globalConfig.count : globalConfig.count) || defaultConfig.count),
|
||||
count: defaults.count || String(node.metadata?.count || (mode === "image" ? globalConfig.canvasImageCount || globalConfig.count : globalConfig.count) || defaultConfig.count),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import { resourceIdFromStorageKey } from "@/services/api/resources";
|
||||
import { NODE_DEFAULT_SIZE } from "@/constant/canvas";
|
||||
import { normalizeVideoDuration, normalizeVideoResolution } from "@/lib/video-generation-options";
|
||||
import { isSeedanceVideoConfig } from "@/lib/seedance-video";
|
||||
import { modelCapabilityConfigFor, normalizeImageValue } from "@/lib/model-capabilities";
|
||||
import { modelRequestOptions, resolveCompatibleModel, resolveVideoOperation, type ModelRequirements } from "@/lib/model-selection";
|
||||
import { modelCapabilityConfigFor } from "@/lib/model-capabilities";
|
||||
import { modelRequestOptions, resolveCompatibleModel, resolveModelGenerationDefaults, resolveVideoOperation, type ModelRequirements } from "@/lib/model-selection";
|
||||
import { imageMetadata } from "@/lib/canvas/canvas-generation-task-sync";
|
||||
import { ensureMediaNodeMinimumSize } from "@/lib/canvas/canvas-node-size";
|
||||
import type { CanvasNodeGenerationMode } from "@/components/canvas/canvas-node-prompt-panel";
|
||||
@@ -399,22 +399,46 @@ export function buildGenerationConfig(config: AiConfig, node: CanvasNodeData | u
|
||||
? { ...requirements, options: { ...liveOptions, ...(requirements.options || {}) } }
|
||||
: { capability: mode, options: liveOptions };
|
||||
const model = resolveCompatibleModel(config, preferredModel, imageSize ? { ...baseRequirements, imageSize } : baseRequirements) || preferredModel;
|
||||
const imageProfile = mode === "image" ? modelCapabilityConfigFor(config, model).image! : undefined;
|
||||
const normalizedImage = imageProfile
|
||||
? normalizeImageValue(imageProfile, {
|
||||
quality: requestedConfig.quality,
|
||||
size: requestedConfig.size,
|
||||
transparentBackground: requestedConfig.transparentBackground,
|
||||
count: requestedConfig.count,
|
||||
})
|
||||
: undefined;
|
||||
const generationDefaults = resolveModelGenerationDefaults(
|
||||
config,
|
||||
model,
|
||||
mode === "image" ? "image" : mode === "video" ? "video" : undefined,
|
||||
mode === "image"
|
||||
? {
|
||||
size: node?.metadata?.size,
|
||||
quality: node?.metadata?.quality,
|
||||
transparentBackground: node?.metadata?.transparentBackground,
|
||||
count: requestedConfig.count,
|
||||
}
|
||||
: {
|
||||
size: node?.metadata?.size,
|
||||
videoSeconds: node?.metadata?.seconds,
|
||||
vquality: node?.metadata?.vquality,
|
||||
videoGenerateAudio: node?.metadata?.generateAudio,
|
||||
videoWatermark: node?.metadata?.watermark,
|
||||
},
|
||||
{
|
||||
size: requestedConfig.size,
|
||||
quality: requestedConfig.quality,
|
||||
transparentBackground: requestedConfig.transparentBackground,
|
||||
count: requestedConfig.count,
|
||||
videoSeconds: requestedConfig.videoSeconds,
|
||||
vquality: requestedConfig.vquality,
|
||||
videoGenerateAudio: requestedConfig.videoGenerateAudio,
|
||||
videoWatermark: requestedConfig.videoWatermark,
|
||||
},
|
||||
);
|
||||
return {
|
||||
...requestedConfig,
|
||||
model,
|
||||
quality: normalizedImage?.quality || requestedConfig.quality,
|
||||
size: normalizedImage?.size || requestedConfig.size,
|
||||
transparentBackground: normalizedImage?.transparentBackground || (requestedConfig.transparentBackground === "true" ? "true" : "false"),
|
||||
count: normalizedImage?.count || requestedConfig.count,
|
||||
quality: generationDefaults.quality || requestedConfig.quality,
|
||||
size: generationDefaults.size || requestedConfig.size,
|
||||
transparentBackground: generationDefaults.transparentBackground || (requestedConfig.transparentBackground === "true" ? "true" : "false"),
|
||||
videoSeconds: generationDefaults.videoSeconds || requestedConfig.videoSeconds,
|
||||
vquality: generationDefaults.vquality || requestedConfig.vquality,
|
||||
videoGenerateAudio: generationDefaults.videoGenerateAudio || requestedConfig.videoGenerateAudio,
|
||||
videoWatermark: generationDefaults.videoWatermark || requestedConfig.videoWatermark,
|
||||
count: generationDefaults.count || requestedConfig.count,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defaultImageCapabilityConfig, modelCapabilityConfigFor, STANDARD_IMAGE_SIZE_VALUES, videoDurationAllowed, type ImageCapabilityConfig } from "@/lib/model-capabilities";
|
||||
import { defaultImageCapabilityConfig, modelCapabilityConfigFor, normalizeImageValue, normalizeVideoValue, STANDARD_IMAGE_SIZE_VALUES, videoDurationAllowed, type ImageCapabilityConfig } from "@/lib/model-capabilities";
|
||||
import { modelOptionName, resolveModelChannel, selectableModelsByCapability, type AiConfig, type ModelCapability } from "@/stores/use-config-store";
|
||||
|
||||
export type ModelInputSummary = {
|
||||
@@ -129,7 +129,13 @@ function logicalModelCompatibilityError(spec: NonNullable<NonNullable<AiConfig["
|
||||
}
|
||||
const operation = requirements.capability === "video" && input ? resolveVideoOperation(input, requirements.videoOperation) : requirements.videoOperation;
|
||||
if (operation && spec.operations?.length && !spec.operations.includes(operation)) return "不支持当前生成模式";
|
||||
const options = { ...requirements.options, ...(requirements.videoSeconds ? { videoSeconds: requirements.videoSeconds } : {}), ...(requirements.imageSize ? { size: requirements.imageSize } : {}) };
|
||||
// 图片创作状态也会携带全局默认视频时长;这个字段只对视频模型有意义,
|
||||
// 不能把它拼进图片逻辑模型的能力匹配,否则图片模型会被误判为“不支持当前时长”。
|
||||
const options = {
|
||||
...requirements.options,
|
||||
...(requirements.capability === "video" && requirements.videoSeconds ? { videoSeconds: requirements.videoSeconds } : {}),
|
||||
...(requirements.capability === "image" && requirements.imageSize ? { size: requirements.imageSize } : {}),
|
||||
};
|
||||
for (const [name, value] of Object.entries(options)) {
|
||||
if (value === undefined || value === null || value === "") continue;
|
||||
const constraint = spec.options?.[name];
|
||||
@@ -223,6 +229,51 @@ export function defaultImageParamsForModel(config: AiConfig, model: string): Pic
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export type ModelGenerationDefaults = Pick<AiConfig, "size" | "quality" | "transparentBackground" | "count" | "videoSeconds" | "vquality" | "videoGenerateAudio" | "videoWatermark">;
|
||||
|
||||
export function resolveModelGenerationDefaults(
|
||||
config: AiConfig,
|
||||
model: string,
|
||||
capability: "image" | "video" | undefined,
|
||||
explicit: Partial<ModelGenerationDefaults> = {},
|
||||
fallback: Partial<ModelGenerationDefaults> = {},
|
||||
): Partial<ModelGenerationDefaults> {
|
||||
if (!capability) return {};
|
||||
const channel = resolveModelChannel(config, model);
|
||||
const cost = channel.modelCosts?.find((item) => item.model === modelOptionName(model));
|
||||
const isManagedModel = Boolean(cost?.logicalModelId || cost?.logicalCapabilitySpec);
|
||||
const source = (key: keyof ModelGenerationDefaults) => explicit[key] ?? (isManagedModel ? undefined : fallback[key]);
|
||||
const profile = modelCapabilityConfigFor(config, model);
|
||||
|
||||
if (capability === "image" && profile.image) {
|
||||
const normalized = normalizeImageValue(profile.image, {
|
||||
size: source("size"),
|
||||
quality: source("quality"),
|
||||
count: source("count"),
|
||||
transparentBackground: source("transparentBackground"),
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
|
||||
if (capability === "video" && profile.video) {
|
||||
const normalized = normalizeVideoValue(profile.video, {
|
||||
seconds: source("videoSeconds"),
|
||||
ratio: source("size"),
|
||||
resolution: source("vquality"),
|
||||
});
|
||||
return {
|
||||
videoSeconds: normalized.seconds,
|
||||
size: normalized.ratio,
|
||||
vquality: normalized.resolution.replace(/p$/i, ""),
|
||||
videoGenerateAudio: source("videoGenerateAudio") ?? String(profile.video.generateAudio.default),
|
||||
videoWatermark: source("videoWatermark") ?? String(profile.video.watermark.default),
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export function maxModelInputCapacity(config: AiConfig, capability: "image" | "video", kind: "image" | "video" | "audio") {
|
||||
const options = selectableModelsByCapability(config, capability);
|
||||
if (!options.length) return null;
|
||||
|
||||
@@ -175,7 +175,7 @@ export default function CreatePage() {
|
||||
audioCount: attachments.filter((attachment) => creationAttachmentKind(attachment) === "audio").length,
|
||||
characterCount: 0,
|
||||
},
|
||||
videoSeconds: seconds,
|
||||
videoSeconds: mode === "video" ? seconds : undefined,
|
||||
imageSize: mode === "image" ? ratio : undefined,
|
||||
options: mode === "image"
|
||||
? { size: ratio, quality, count: Number(count), transparentBackground: config.transparentBackground === "true" }
|
||||
@@ -497,7 +497,20 @@ export default function CreatePage() {
|
||||
const controller = new AbortController();
|
||||
const requestLifecycle = beginGenerationConsumer(controller.signal);
|
||||
abortRef.current = controller;
|
||||
const requestConfig = { ...config, model: selectedModel, imageModel: selectedModel, videoModel: selectedModel, textModel: selectedModel, size: ratio, videoSeconds: seconds, quality, vquality: videoQuality, count };
|
||||
const normalizedImage = mode === "image" ? normalizeImageValue(imageProfile, { size: ratio, quality, count }) : undefined;
|
||||
const normalizedVideo = mode === "video" ? normalizeVideoValue(videoProfile, { seconds, ratio, resolution: videoQuality }) : undefined;
|
||||
const requestConfig = {
|
||||
...config,
|
||||
model: selectedModel,
|
||||
imageModel: selectedModel,
|
||||
videoModel: selectedModel,
|
||||
textModel: selectedModel,
|
||||
...(mode === "image"
|
||||
? { size: normalizedImage?.size || ratio, quality: normalizedImage?.quality || quality, count: normalizedImage?.count || count, videoSeconds: config.videoSeconds }
|
||||
: mode === "video"
|
||||
? { size: normalizedVideo?.ratio || ratio, videoSeconds: normalizedVideo?.seconds || seconds, vquality: (normalizedVideo?.resolution || videoQuality).replace(/p$/i, "") }
|
||||
: {}),
|
||||
};
|
||||
try {
|
||||
if (mode === "text") {
|
||||
if (logicalModelIDForConfig(requestConfig)) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, test } from "bun:test";
|
||||
import { canvasConnectionError } from "../src/lib/canvas/canvas-connection-policy";
|
||||
import { assertCanvasImageReferenceLimit, buildGenerationConfig, canvasImageReferenceLimitError, resolveCanvasGenerationModel } from "../src/lib/canvas/canvas-project-generation";
|
||||
import { defaultModelCapabilityConfig } from "../src/lib/model-capabilities";
|
||||
import { groupModelsByDisplayName, modelCompatibilityError, modelGroupReferenceLimits, resolveCompatibleModel } from "../src/lib/model-selection";
|
||||
import { groupModelsByDisplayName, modelCompatibilityError, modelGroupReferenceLimits, resolveCompatibleModel, resolveModelGenerationDefaults } from "../src/lib/model-selection";
|
||||
import { defaultConfig, type AiConfig, type ModelChannel } from "../src/stores/use-config-store";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData } from "../src/types/canvas";
|
||||
|
||||
@@ -53,6 +53,82 @@ function node(id: string, type: CanvasNodeType, generationMode?: "image" | "vide
|
||||
}
|
||||
|
||||
describe("逻辑模型选择", () => {
|
||||
test("图片逻辑模型忽略全局视频时长,不应显示为不支持当前时长", () => {
|
||||
const model = "platform::gpt-image-2";
|
||||
const channel: ModelChannel = {
|
||||
id: "platform",
|
||||
name: "平台模型",
|
||||
baseUrl: "/api",
|
||||
apiKey: "system",
|
||||
apiFormat: "openai",
|
||||
scope: "system",
|
||||
enabled: true,
|
||||
models: ["gpt-image-2"],
|
||||
modelCosts: [{
|
||||
model: "gpt-image-2",
|
||||
displayName: "GPT Image 2",
|
||||
capability: "image",
|
||||
billingMode: "fixed_request",
|
||||
unitPriceMicrocredits: 1,
|
||||
logicalCapabilitySpec: {
|
||||
version: 1,
|
||||
capability: "image",
|
||||
options: { size: { values: ["1:1", "3:2"] } },
|
||||
},
|
||||
}],
|
||||
};
|
||||
const config: AiConfig = {
|
||||
...defaultConfig,
|
||||
channels: [channel],
|
||||
models: [model],
|
||||
imageModels: [model],
|
||||
imageModel: model,
|
||||
model,
|
||||
};
|
||||
|
||||
expect(modelCompatibilityError(config, model, {
|
||||
capability: "image",
|
||||
input: { textCount: 1, imageCount: 0, videoCount: 0, audioCount: 0, characterCount: 0 },
|
||||
videoSeconds: "6",
|
||||
imageSize: "3:2",
|
||||
options: { size: "3:2" },
|
||||
})).toBe("");
|
||||
});
|
||||
|
||||
test("后台图片默认比例优先于旧的全局 1:1 配置", () => {
|
||||
const model = "platform::managed-image";
|
||||
const profile = defaultModelCapabilityConfig(undefined, "managed-image");
|
||||
profile.image!.size = { parameter: "size", values: ["1:1", "16:9"], default: "16:9", allowCustom: false };
|
||||
const config: AiConfig = {
|
||||
...defaultConfig,
|
||||
size: "1:1",
|
||||
channels: [{ id: "platform", name: "平台模型", baseUrl: "/api", apiKey: "system", apiFormat: "openai", scope: "system", models: ["managed-image"], modelCosts: [{ model: "managed-image", capability: "image", billingMode: "fixed_request", unitPriceMicrocredits: 1, logicalModelId: "managed-image", logicalCapabilitySpec: { version: 1, capability: "image" }, capabilityConfig: profile }] }],
|
||||
models: [model],
|
||||
imageModels: [model],
|
||||
imageModel: model,
|
||||
model,
|
||||
};
|
||||
|
||||
expect(resolveModelGenerationDefaults(config, model, "image", {}, { size: "1:1" }).size).toBe("16:9");
|
||||
});
|
||||
|
||||
test("后台视频默认时长优先于旧的全局 6 秒配置", () => {
|
||||
const model = "platform::managed-video";
|
||||
const profile = defaultModelCapabilityConfig(undefined, "managed-video");
|
||||
profile.video!.duration = { selection: "enum", values: [6, 15], default: 15 };
|
||||
const config: AiConfig = {
|
||||
...defaultConfig,
|
||||
videoSeconds: "6",
|
||||
channels: [{ id: "platform", name: "平台模型", baseUrl: "/api", apiKey: "system", apiFormat: "openai", scope: "system", models: ["managed-video"], modelCosts: [{ model: "managed-video", capability: "video", billingMode: "per_second", unitPriceMicrocredits: 1, logicalModelId: "managed-video", logicalCapabilitySpec: { version: 1, capability: "video" }, capabilityConfig: profile }] }],
|
||||
models: [model],
|
||||
videoModels: [model],
|
||||
videoModel: model,
|
||||
model,
|
||||
};
|
||||
|
||||
expect(resolveModelGenerationDefaults(config, model, "video", {}, { videoSeconds: "6" }).videoSeconds).toBe("15");
|
||||
});
|
||||
|
||||
test("后台标注的视频模型不因内部标识缺少视频关键词而回退", () => {
|
||||
const config = policyConfig();
|
||||
const selectedModel = config.videoModels[0]!;
|
||||
|
||||
Reference in New Issue
Block a user