|
|
|
@ -89,9 +89,9 @@ type PointsEstimateResponse = { |
|
|
|
}; |
|
|
|
|
|
|
|
type SeedanceVideoEstimatePayload = { |
|
|
|
model_name: string; |
|
|
|
estimation_type: "seedance_video"; |
|
|
|
seedance_video: { |
|
|
|
modelName: string; |
|
|
|
estimationType: "seedance_video"; |
|
|
|
seedanceVideo: { |
|
|
|
resolution: string; |
|
|
|
aspect_ratio: AspectRatio; |
|
|
|
input_video_duration_seconds: number; |
|
|
|
@ -109,6 +109,7 @@ interface ComposerAdapter<TData extends WorkflowNodeData> { |
|
|
|
|
|
|
|
const ASPECT_RATIOS: AspectRatio[] = ["1:1", "3:4", "4:3", "9:16", "16:9", "21:9"]; |
|
|
|
const RESOLUTIONS: Resolution[] = ["1K", "2K", "4K"]; |
|
|
|
const GPT_IMAGE_2_ALL_RESOLUTIONS: Resolution[] = ["1K", "2K"]; |
|
|
|
const VIDEO_DURATION_QUICK_OPTIONS = [4, 5, 8, 10, 15]; |
|
|
|
const IMAGE_COUNTS: ImageGenerationCount[] = [1, 2, 4]; |
|
|
|
const MAX_REFERENCE_IMAGES = 4; |
|
|
|
@ -171,6 +172,17 @@ function getLegacyImageModel(model: SelectedModel): ModelType { |
|
|
|
return "nano-banana-pro"; |
|
|
|
} |
|
|
|
|
|
|
|
function getImageResolutionOptions(model: SelectedModel): Resolution[] { |
|
|
|
return model.provider === "newapiwg" && model.modelId === "gpt-image-2-all" |
|
|
|
? GPT_IMAGE_2_ALL_RESOLUTIONS |
|
|
|
: RESOLUTIONS; |
|
|
|
} |
|
|
|
|
|
|
|
function normalizeImageResolutionForModel(model: SelectedModel, resolution: string): string { |
|
|
|
const options = getImageResolutionOptions(model); |
|
|
|
return options.includes(resolution as Resolution) ? resolution : options.at(-1) ?? "2K"; |
|
|
|
} |
|
|
|
|
|
|
|
function selectedModelFromProvider(model: ProviderModel): SelectedModel { |
|
|
|
return { |
|
|
|
provider: model.provider, |
|
|
|
@ -306,9 +318,9 @@ function buildSeedanceVideoEstimatePayload( |
|
|
|
inputVideoDurationSeconds: number |
|
|
|
): SeedanceVideoEstimatePayload { |
|
|
|
return { |
|
|
|
model_name: draft.selectedModel.modelId, |
|
|
|
estimation_type: "seedance_video", |
|
|
|
seedance_video: { |
|
|
|
modelName: draft.selectedModel.modelId, |
|
|
|
estimationType: "seedance_video", |
|
|
|
seedanceVideo: { |
|
|
|
resolution: draft.resolution || DEFAULT_VIDEO_RESOLUTION, |
|
|
|
aspect_ratio: draft.aspectRatio || "16:9", |
|
|
|
input_video_duration_seconds: inputVideoDurationSeconds, |
|
|
|
@ -838,11 +850,15 @@ export function GenerationComposer() { |
|
|
|
|
|
|
|
const seedanceEstimateKey = seedanceEstimatePayload |
|
|
|
? JSON.stringify({ |
|
|
|
baseUrl: newApiWGBaseUrl || "default", |
|
|
|
...seedanceEstimatePayload, |
|
|
|
}) |
|
|
|
: null; |
|
|
|
|
|
|
|
const imageResolutionOptions = useMemo( |
|
|
|
() => getImageResolutionOptions(draft.selectedModel), |
|
|
|
[draft.selectedModel.modelId, draft.selectedModel.provider] |
|
|
|
); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
const selectedModel = draft.selectedModel; |
|
|
|
if ( |
|
|
|
@ -965,7 +981,6 @@ export function GenerationComposer() { |
|
|
|
const headers: Record<string, string> = { |
|
|
|
"Content-Type": "application/json", |
|
|
|
}; |
|
|
|
if (newApiWGBaseUrl) headers["X-NewApiWG-Base-URL"] = newApiWGBaseUrl; |
|
|
|
|
|
|
|
const response = await fetch("/api/points/estimate", { |
|
|
|
method: "POST", |
|
|
|
@ -989,7 +1004,7 @@ export function GenerationComposer() { |
|
|
|
return () => { |
|
|
|
cancelled = true; |
|
|
|
}; |
|
|
|
}, [newApiWGBaseUrl, seedanceEstimateKey, seedanceEstimatePayload]); |
|
|
|
}, [seedanceEstimateKey, seedanceEstimatePayload]); |
|
|
|
|
|
|
|
const isVideoStitchNode = context.mode === "process-node" && context.node?.type === "videoStitch"; |
|
|
|
const isEaseCurveNode = context.mode === "process-node" && context.node?.type === "easeCurve"; |
|
|
|
@ -1151,7 +1166,10 @@ export function GenerationComposer() { |
|
|
|
: errorMessage |
|
|
|
? t("composer.retryCurrentNode") |
|
|
|
: nodeEditGenerateTitle; |
|
|
|
const selectedPointAmount = seedanceEstimatedPointAmount ?? getSelectedPointAmount(draft, referenceImages.length); |
|
|
|
const selectedUnitPointAmount = seedanceEstimatedPointAmount ?? getSelectedPointAmount(draft, referenceImages.length); |
|
|
|
const selectedPointAmount = selectedUnitPointAmount !== null && seedanceEstimatedPointAmount === null && activeCapability === "image" |
|
|
|
? selectedUnitPointAmount * draft.imageCount |
|
|
|
: selectedUnitPointAmount; |
|
|
|
const modelPointCostLabel = selectedPointAmount !== null |
|
|
|
? t("composer.pointCost", { count: formatPointAmount(selectedPointAmount) }) |
|
|
|
: null; |
|
|
|
@ -1211,6 +1229,20 @@ export function GenerationComposer() { |
|
|
|
}); |
|
|
|
}, []); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
if (context.capability !== "image" && context.mode !== "empty-create") return; |
|
|
|
const normalizedResolution = normalizeImageResolutionForModel(draft.selectedModel, draft.resolution); |
|
|
|
if (normalizedResolution === draft.resolution) return; |
|
|
|
markDraft({ resolution: normalizedResolution }, ["resolution"]); |
|
|
|
}, [ |
|
|
|
context.capability, |
|
|
|
context.mode, |
|
|
|
draft.resolution, |
|
|
|
draft.selectedModel.modelId, |
|
|
|
draft.selectedModel.provider, |
|
|
|
markDraft, |
|
|
|
]); |
|
|
|
|
|
|
|
const updateVideoQuickDraft = useCallback( |
|
|
|
(patch: Partial<ComposerDraft>, fields: DraftField[]) => { |
|
|
|
const activeContext = contextRef.current; |
|
|
|
@ -1279,6 +1311,9 @@ export function GenerationComposer() { |
|
|
|
(model: ProviderModel) => { |
|
|
|
const nextModel = selectedModelFromProvider(model); |
|
|
|
const activeContext = contextRef.current; |
|
|
|
const nextResolution = modelSupportsCapability(nextModel, "video") |
|
|
|
? DEFAULT_VIDEO_RESOLUTION |
|
|
|
: normalizeImageResolutionForModel(nextModel, draftRef.current.resolution); |
|
|
|
|
|
|
|
if (activeContext.mode === "node-edit") { |
|
|
|
const activeAdapter = getAdapter(activeContext.nodeType); |
|
|
|
@ -1289,7 +1324,7 @@ export function GenerationComposer() { |
|
|
|
{ |
|
|
|
...draftRef.current, |
|
|
|
selectedModel: nextModel, |
|
|
|
resolution: modelSupportsCapability(nextModel, "video") ? DEFAULT_VIDEO_RESOLUTION : draftRef.current.resolution, |
|
|
|
resolution: nextResolution, |
|
|
|
durationSeconds: modelSupportsCapability(nextModel, "video") |
|
|
|
? DEFAULT_VIDEO_DURATION_SECONDS |
|
|
|
: draftRef.current.durationSeconds, |
|
|
|
@ -1301,7 +1336,7 @@ export function GenerationComposer() { |
|
|
|
setDraft((current) => ({ |
|
|
|
...current, |
|
|
|
selectedModel: nextModel, |
|
|
|
resolution: modelSupportsCapability(nextModel, "video") ? DEFAULT_VIDEO_RESOLUTION : current.resolution, |
|
|
|
resolution: nextResolution, |
|
|
|
durationSeconds: modelSupportsCapability(nextModel, "video") |
|
|
|
? DEFAULT_VIDEO_DURATION_SECONDS |
|
|
|
: current.durationSeconds, |
|
|
|
@ -1316,7 +1351,9 @@ export function GenerationComposer() { |
|
|
|
setDraft((current) => ({ |
|
|
|
...current, |
|
|
|
selectedModel: nextModel, |
|
|
|
resolution: modelSupportsCapability(nextModel, "video") ? DEFAULT_VIDEO_RESOLUTION : current.resolution, |
|
|
|
resolution: modelSupportsCapability(nextModel, "video") |
|
|
|
? DEFAULT_VIDEO_RESOLUTION |
|
|
|
: normalizeImageResolutionForModel(nextModel, current.resolution), |
|
|
|
durationSeconds: modelSupportsCapability(nextModel, "video") |
|
|
|
? DEFAULT_VIDEO_DURATION_SECONDS |
|
|
|
: current.durationSeconds, |
|
|
|
@ -2035,11 +2072,11 @@ export function GenerationComposer() { |
|
|
|
</select> |
|
|
|
|
|
|
|
<select |
|
|
|
value={draft.resolution} |
|
|
|
value={normalizeImageResolutionForModel(draft.selectedModel, draft.resolution)} |
|
|
|
onChange={(event) => markDraft({ resolution: event.target.value as Resolution }, ["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" |
|
|
|
> |
|
|
|
{RESOLUTIONS.map((item) => ( |
|
|
|
{imageResolutionOptions.map((item) => ( |
|
|
|
<option key={item} value={item} className="bg-neutral-900"> |
|
|
|
{item} |
|
|
|
</option> |
|
|
|
|