Compare commits

..

No commits in common. "71997086e13ff6909d927274476cd02e7baa2c29" and "f75f77105dfe29cf436fda4e5bfc602d7b5f960c" have entirely different histories.

22 changed files with 92 additions and 774 deletions

View File

@ -368,7 +368,6 @@ type ImageGenConfig = {
creditCostPerVideo?: number; // 每条视频积分消耗
videoDurationOptions?: number[]; // 视频秒数选项
creditCostPerVideoByDuration?: Record<string, number>; // 各秒数积分
creditCostPerVideoByResolution?: Record<string, number>; // 各分辨率积分
enableLoadingPlaceholder?: boolean; // 是否启用加载占位图
loadingColor?: string; // 占位图主题色,如 "purple", "blue", "green"
};
@ -388,7 +387,6 @@ function createDefaultImageGenConfig(): ImageGenConfig {
creditCostPerVideo: 0,
videoDurationOptions: [],
creditCostPerVideoByDuration: {},
creditCostPerVideoByResolution: {},
enableLoadingPlaceholder: true,
loadingColor: "purple",
};
@ -2912,28 +2910,6 @@ export function AdminSettingsClient() {
});
};
const updateEnhanceProScaleFactorCost = (
factorKey: string,
nextCost: number
) => {
setSiteSettings((prev) => {
const config = normalizePointsSystemConfig(prev.pointsSystemConfig);
return {
...prev,
pointsSystemConfig: {
...config,
enhanceProPricing: {
...config.enhanceProPricing,
scaleFactorCosts: {
...config.enhanceProPricing.scaleFactorCosts,
[factorKey]: Math.max(0, Math.round(nextCost) || 0),
},
},
},
};
});
};
const updateMembershipConfigState = (
updater: (
config: ReturnType<typeof normalizeMembershipConfig>
@ -5590,7 +5566,7 @@ export function AdminSettingsClient() {
id={`enhance-pro-preset-scale-${preset.id}`}
type="number"
min="1"
max={enhanceProConfig.maxUpscaleFactor}
max="32"
step="0.5"
value={preset.imageScalingFactor}
onChange={(e) =>
@ -5598,7 +5574,7 @@ export function AdminSettingsClient() {
index,
"imageScalingFactor",
Math.min(
enhanceProConfig.maxUpscaleFactor,
32,
Math.max(1, Number.parseFloat(e.target.value) || 1)
)
)
@ -8232,60 +8208,6 @@ export function AdminSettingsClient() {
}
/>
</div>
<div className="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2">
<div className="space-y-1">
<Label
className="text-xs text-muted-foreground"
htmlFor="points3dWorldMarble11Cost"
>
Marble 1.1 积分
</Label>
<Input
id="points3dWorldMarble11Cost"
min={0}
type="number"
value={pointsSystemConfig.creditCostPer3DWorldMarble11}
onChange={(e) =>
setSiteSettings({
...siteSettings,
pointsSystemConfig: {
...pointsSystemConfig,
creditCostPer3DWorldMarble11: Math.max(
0,
Number.parseInt(e.target.value || "0", 10) || 0
),
},
})
}
/>
</div>
<div className="space-y-1">
<Label
className="text-xs text-muted-foreground"
htmlFor="points3dWorldMarble11PlusCost"
>
Marble 1.1 Plus 积分
</Label>
<Input
id="points3dWorldMarble11PlusCost"
min={0}
type="number"
value={pointsSystemConfig.creditCostPer3DWorldMarble11Plus}
onChange={(e) =>
setSiteSettings({
...siteSettings,
pointsSystemConfig: {
...pointsSystemConfig,
creditCostPer3DWorldMarble11Plus: Math.max(
0,
Number.parseInt(e.target.value || "0", 10) || 0
),
},
})
}
/>
</div>
</div>
</div>
</div>
<div className="rounded-lg border p-4 space-y-4">
@ -8320,7 +8242,7 @@ export function AdminSettingsClient() {
<div className="space-y-1">
<Label>计费模式</Label>
<div className="text-xs text-muted-foreground">
可按放大倍率、分辨率档位或固定单价计算本地积分。
固定单价适合先快速上线,分辨率档位更适合贴近增强成本。
</div>
</div>
<div className="mt-2 w-full max-w-xs">
@ -8335,9 +8257,7 @@ export function AdminSettingsClient() {
chargeMode:
value === "fixed"
? "fixed"
: value === "resolution_bucket"
? "resolution_bucket"
: "scale_factor",
: "resolution_bucket",
},
},
})
@ -8348,9 +8268,6 @@ export function AdminSettingsClient() {
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="scale_factor">
放大倍率
</SelectItem>
<SelectItem value="resolution_bucket">
分辨率档位
</SelectItem>
@ -8395,66 +8312,6 @@ export function AdminSettingsClient() {
</div>
</div>
<div className="flex items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<Label>只在启用放大时扣积分</Label>
<div className="text-xs text-muted-foreground">
开启后用户关闭“启用放大”时不扣除图像增强 Pro 积分。
</div>
</div>
<Switch
checked={
pointsSystemConfig.enhanceProPricing
.chargeOnlyWhenUpscaling !== false
}
onCheckedChange={(checked) =>
setSiteSettings({
...siteSettings,
pointsSystemConfig: {
...pointsSystemConfig,
enhanceProPricing: {
...pointsSystemConfig.enhanceProPricing,
chargeOnlyWhenUpscaling: checked,
},
},
})
}
/>
</div>
<div className="space-y-3 rounded-lg border p-3">
<div className="space-y-1">
<Label>放大倍率积分</Label>
<div className="text-xs text-muted-foreground">
“放大倍率”计费模式下使用;小数倍率会匹配大于等于该倍率的最近档位。
</div>
</div>
<div className="grid grid-cols-2 gap-3 md:grid-cols-5">
{Object.entries(
pointsSystemConfig.enhanceProPricing.scaleFactorCosts
)
.sort(([left], [right]) => Number(left) - Number(right))
.map(([factorKey, cost]) => (
<div key={factorKey} className="space-y-1">
<Label className="text-xs text-muted-foreground">
{factorKey}x
</Label>
<Input
min={0}
type="number"
value={cost}
onChange={(e) =>
updateEnhanceProScaleFactorCost(
factorKey,
Number.parseInt(e.target.value || "0", 10) || 0
)
}
/>
</div>
))}
</div>
</div>
<div className="flex items-center justify-between rounded-lg border p-3">
<div className="space-y-0.5">
<Label>按候选结果数量线性乘倍</Label>
@ -8628,7 +8485,7 @@ export function AdminSettingsClient() {
</div>
<div className="rounded-lg border border-dashed p-3 text-xs text-muted-foreground">
建议算法:按放大倍率或输出分辨率取基础积分,再乘以候选数量和模型倍率,最终向上取整。
建议算法:按输出分辨率档位取基础积分,再乘以候选数量和模型倍率,最终向上取整。
</div>
</div>
<div className="space-y-2">
@ -12073,62 +11930,6 @@ export function AdminSettingsClient() {
当前还没有配置秒数档位,将按“每条视频积分消耗”统一扣费。
</p>
)}
<div className="space-y-3 border-t pt-4">
<div className="text-sm font-medium">
各分辨率积分消耗
</div>
<div className="grid grid-cols-3 gap-4">
{(["480p", "720p", "1080p"] as const).map(
(resolutionKey) => (
<div
key={resolutionKey}
className="space-y-2"
>
<Label>{resolutionKey} 积分</Label>
<Input
type="number"
min={0}
step={1}
value={
modelForm.imageGenConfig
?.creditCostPerVideoByResolution?.[
resolutionKey
] ??
modelForm.imageGenConfig
?.creditCostPerVideo ??
0
}
onChange={(e) => {
const nextValue = Math.max(
0,
Number.parseInt(
e.target.value,
10
) || 0
);
setModelForm({
...modelForm,
imageGenConfig: {
...modelForm.imageGenConfig,
creditCostPerVideoByResolution: {
...(modelForm.imageGenConfig
?.creditCostPerVideoByResolution ||
{}),
[resolutionKey]: nextValue,
},
},
});
}}
/>
</div>
)
)}
</div>
<p className="text-xs text-muted-foreground">
配置后会优先按用户选择的视频分辨率扣积分;未命中再回退到秒数或统一价格。
</p>
</div>
</div>
)}
@ -15205,7 +15006,7 @@ export function AdminSettingsClient() {
</div>
</div>
<div className="grid gap-4 md:grid-cols-3">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="enhanceProMaxBatchSize">最大候选图数量</Label>
<Select
@ -15236,39 +15037,6 @@ export function AdminSettingsClient() {
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="enhanceProMaxUpscaleFactor">
最大放大倍数
</Label>
<Input
id="enhanceProMaxUpscaleFactor"
type="number"
min="1"
max="32"
step="0.5"
value={enhanceProConfig.maxUpscaleFactor}
onChange={(e) => {
const nextMaxUpscaleFactor = Math.min(
32,
Math.max(1, Number.parseFloat(e.target.value) || 4)
);
setEnhanceProConfig((prev) => ({
...prev,
maxUpscaleFactor: nextMaxUpscaleFactor,
presets: prev.presets.map((preset) => ({
...preset,
imageScalingFactor: Math.min(
Math.max(1, preset.imageScalingFactor || 1),
nextMaxUpscaleFactor
),
})),
}));
}}
/>
<p className="text-xs text-muted-foreground">
默认建议 2-4 倍;需要时仍可放宽到 Krea 支持的更高倍数。
</p>
</div>
<div className="space-y-2">
<Label htmlFor="enhanceProMaxOutputEdge">最大输出长边</Label>
<Input

View File

@ -4087,17 +4087,13 @@ const MODEL_UUID_REGEX =
function getModelCreditCost(
modelConfig: any,
target: "image" | "video",
options?: { duration?: number | null; resolution?: string | null }
options?: { duration?: number | null }
): number {
const config = (modelConfig as any)?.imageGenConfig || {};
const sourceValue =
target === "video"
? (() => {
const resolvedVideoCost = getVideoCreditCostFromConfig(
config,
options?.duration,
options?.resolution
);
const resolvedVideoCost = getVideoCreditCostFromConfig(config, options?.duration);
if (resolvedVideoCost > 0) return resolvedVideoCost;
return config.creditCostPerVideo ?? config.creditCostPerImage ?? 0;
})()
@ -4126,7 +4122,6 @@ async function consumeGeneratedVideoPoints(params: {
messageId: string;
modelRefs: Array<string | null | undefined>;
duration?: number | null;
resolution?: string | null;
}) {
try {
const pointsConfig = await getPointsSystemConfig();
@ -4144,21 +4139,13 @@ async function consumeGeneratedVideoPoints(params: {
const resolved = await resolveBillingModelByReference(ref);
if (!resolved) continue;
billingModel = resolved;
if (
getModelCreditCost(resolved, "video", {
duration: params.duration,
resolution: params.resolution,
}) > 0
) {
break;
}
if (getModelCreditCost(resolved, "video", { duration: params.duration }) > 0) break;
}
if (!billingModel) return;
const unitCost = getModelCreditCost(billingModel, "video", {
duration: params.duration,
resolution: params.resolution,
});
if (unitCost <= 0) return;
@ -4169,7 +4156,7 @@ async function consumeGeneratedVideoPoints(params: {
modelId: billingModel.id,
chatId: params.chatId,
messageId: params.messageId,
note: `模型(${billingModel.displayName || billingModel.modelId || billingModel.id}) 生视频 1 条${params.duration ? `(${params.duration} 秒)` : ""}${params.resolution ? `(${params.resolution})` : ""}`,
note: `模型(${billingModel.displayName || billingModel.modelId || billingModel.id}) 生视频 1 条${params.duration ? `(${params.duration} 秒)` : ""}`,
});
if (!consumeResult.ok) {
@ -6808,9 +6795,6 @@ export async function POST(request: Request) {
messageId: finalMessageId,
modelRefs: executionResult.modelRefs,
duration: Number((finalVideoMetadata as any)?.duration) || undefined,
resolution:
String((finalVideoMetadata as any)?.resolution || "").trim() ||
undefined,
});
scheduleVideoArchive({
@ -8115,9 +8099,6 @@ export async function POST(request: Request) {
messageId: assistantMessageId,
modelRefs: [result.modelRef || presetModelId],
duration: Number((result.metadata as any)?.duration) || undefined,
resolution:
String((result.metadata as any)?.resolution || "").trim() ||
undefined,
});
scheduleVideoArchive({
@ -8628,12 +8609,6 @@ export async function POST(request: Request) {
Number((result.metadata as any)?.duration) ||
Number(effectiveVideoParams?.duration) ||
undefined,
resolution:
String(
(result.metadata as any)?.resolution ||
effectiveVideoParams?.resolution ||
""
).trim() || undefined,
});
if (videoUrl) {
@ -10730,9 +10705,6 @@ export async function POST(request: Request) {
messageId,
modelRefs: billedModelRefs,
duration: Number(effectiveVideoParams?.duration) || undefined,
resolution:
String(effectiveVideoParams?.resolution || "").trim() ||
undefined,
});
}
@ -11726,7 +11698,6 @@ export async function POST(request: Request) {
messageId: videoMessageId,
modelRefs: [finalVideoModelData.id, providerModelData.id, finalModelId],
duration: Number(videoOptions.duration) || undefined,
resolution: String(videoOptions.resolution || "").trim() || undefined,
});
dataStream.write({

View File

@ -18,7 +18,6 @@ import {
import {
buildVideoDurationCostMap,
getConfiguredVideoDurationOptions,
normalizeVideoResolutionCostMap,
} from "@/lib/ai/video-model-config";
// 🆕 图片生成模型特性配置 Schema
@ -36,7 +35,6 @@ const imageGenConfigSchema = z.object({
creditCostPerVideo: z.number().min(0).max(100000).optional(),
videoDurationOptions: z.array(z.number().min(1).max(600)).max(20).optional(),
creditCostPerVideoByDuration: z.record(z.string(), z.number().min(0).max(100000)).optional(),
creditCostPerVideoByResolution: z.record(z.string(), z.number().min(0).max(100000)).optional(),
enableLoadingPlaceholder: z.boolean().optional(),
loadingColor: z.string().optional(),
}).optional();
@ -55,9 +53,6 @@ function normalizeImageGenConfig(config: z.infer<typeof imageGenConfigSchema>) {
...config,
videoDurationOptions: videoDurationOptions.length ? videoDurationOptions : undefined,
creditCostPerVideoByDuration,
creditCostPerVideoByResolution: normalizeVideoResolutionCostMap(
config.creditCostPerVideoByResolution
),
};
}

View File

@ -7,7 +7,6 @@ import {
} from "@/lib/db/queries";
import { resolveThreeDModelRealId } from "@/lib/model3d/model-alias-config";
import { getThreeDModelAliasConfigForRuntime } from "@/lib/model3d/model-alias-config-server";
import { get3DWorldCreditCost } from "@/lib/points/config";
import { WorldLabsApiError, worldLabsRequest } from "@/lib/worldlabs/client";
import {
disableWorldLabsProvider,
@ -156,17 +155,12 @@ export async function POST(request: Request) {
return NextResponse.json({ success: false, error: "缺少有效的 mode" }, { status: 400 });
}
const modelAliasConfig = await getThreeDModelAliasConfigForRuntime();
const resolvedWorldModel = resolveThreeDModelRealId(
readOptionalTrimmedText(body.model),
modelAliasConfig.worldModels,
);
const pointsConfig = await getPointsSystemConfig();
const shouldChargePoints = shouldCharge3DPoints(mode);
const unitCost = get3DWorldCreditCost(
pointsConfig,
resolvedWorldModel || "marble-1.1",
);
const unitCost = Number.isFinite(Number(pointsConfig.creditCostPer3DWorld))
? Math.max(0, Math.round(Number(pointsConfig.creditCostPer3DWorld)))
: 0;
if (
shouldChargePoints &&
@ -199,6 +193,10 @@ export async function POST(request: Request) {
readOptionalTrimmedText(body.displayName) ||
readOptionalTrimmedText(body.prompt) ||
`World ${new Date().toLocaleString("zh-CN")}`;
const resolvedWorldModel = resolveThreeDModelRealId(
readOptionalTrimmedText(body.model),
modelAliasConfig.worldModels,
);
const worldPrompt = buildWorldPrompt(body, mode);
const candidates = preferredProviderId
@ -268,8 +266,8 @@ export async function POST(request: Request) {
userId: session.user.id,
amount: unitCost,
reason: "world3d_generation",
modelId: `worldlabs-3d:${resolvedWorldModel || "marble-1.1"}`,
note: `3D世界生成消耗积分(${resolvedWorldModel || "marble-1.1"})`,
modelId: "worldlabs-3d",
note: "3D世界生成消耗积分",
});
} catch (error) {
console.error("[Points] Failed to consume World Labs 3D points:", error);

View File

@ -313,12 +313,7 @@ async function createEnhanceProJob(params: {
crop_to_fill: normalizeBoolean(params.requestBody.crop_to_fill, false),
model: params.requestBody.model || params.config.defaultModel,
upscaling_activated: normalizeBoolean(params.requestBody.upscaling_activated, false),
image_scaling_factor: clampNumber(
params.requestBody.image_scaling_factor,
1,
params.config.maxUpscaleFactor,
1,
),
image_scaling_factor: clampNumber(params.requestBody.image_scaling_factor, 1, 32, 1),
creativity: clampInteger(params.requestBody.creativity, 1, 6, 3),
texture: clampInteger(params.requestBody.texture, 1, 5, 3),
sharpen: clampNumber(params.requestBody.sharpen, 0, 1, 0.5),
@ -652,21 +647,12 @@ export async function POST(request: NextRequest) {
body.model === "Redefine"
? body.model
: config.defaultModel;
const upscalingActivated = body.upscaling_activated === true;
const imageScalingFactor = clampNumber(
body.image_scaling_factor,
1,
config.maxUpscaleFactor,
1,
);
const pointsConfig = await getPointsSystemConfig();
const pointsEstimate = calculateEnhanceProPointsEstimate(pointsConfig, {
width: clampedDimensions.width,
height: clampedDimensions.height,
batchSize,
model: selectedModel,
upscalingActivated,
scaleFactor: imageScalingFactor,
});
if (pointsEstimate.chargeable && pointsConfig.blockOnInsufficient !== false) {
@ -697,8 +683,6 @@ export async function POST(request: NextRequest) {
...body,
batchSize,
model: selectedModel,
upscaling_activated: upscalingActivated,
image_scaling_factor: imageScalingFactor,
},
sourceImageUrl: sourceInput.sourceImageUrl,
width: clampedDimensions.width,

View File

@ -9764,7 +9764,14 @@ export default function CanvasEditorPage({ params }: PageProps) {
? VIDEO_CANVAS_TOOLBAR_BUTTON_CLASS
: "h-9 w-9 rounded-xl"
)}
onClick={() => router.push(`/canvas/${id}/realtime?mode=enhance-pro`)}
disabled={!selectedImageNode}
onClick={() => {
if (!selectedImageNode) {
toast.info("请先选中一张图片");
return;
}
router.push(`/canvas/${id}/realtime?mode=enhance-pro`);
}}
size="icon"
variant="ghost"
>
@ -9774,7 +9781,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
<TooltipContent side="top">
{selectedImageNode
? "图像增强 Pro"
: "进入图像增强 Pro 后上传图片"}
: "先选中图片后进入图像增强 Pro"}
</TooltipContent>
</Tooltip>
) : null}

View File

@ -40,7 +40,6 @@ export default function CanvasRealtimePage() {
const handleApplyResult = useCallback(
async ({
imageUrl,
sourceImageUrl,
prompt,
title,
model,
@ -48,7 +47,6 @@ export default function CanvasRealtimePage() {
height,
}: {
imageUrl: string;
sourceImageUrl?: string;
prompt: string;
title?: string;
model?: string;
@ -76,7 +74,6 @@ export default function CanvasRealtimePage() {
const targetHeight = hasNaturalSize
? Math.max(140, Math.round(safeHeight * scale))
: fallbackSize;
const compareSourceUrl = String(sourceImageUrl || "").trim();
const x =
(stageWidth * 0.45 - viewportX) / viewportZoom - targetWidth / 2;
@ -89,16 +86,10 @@ export default function CanvasRealtimePage() {
size: { width: targetWidth, height: targetHeight },
data: {
src: imageUrl,
originalSrc: compareSourceUrl || undefined,
status: "completed",
title: title || "实时画布结果",
prompt,
model: model || "bfl/flux-2",
metadata: compareSourceUrl
? {
compareSourceUrl,
}
: undefined,
},
connections: [],
});

View File

@ -70,7 +70,6 @@ import {
isAgentPsdImageLayerSlot,
isAgentPsdTextLayerSlot,
parseAgentPsdTemplates,
resolveAgentPsdTextLayerEffectiveAlign,
} from "@/lib/agents/psd-template-config";
import {
defaultAgentPsdTemplateSecondaryCategoryConfig,
@ -993,11 +992,9 @@ function getLayerFrameStyle(
function getResolvedTextPreview(
layer: AgentPsdTemplateTextLayerSlot,
value?: string
value: string
) {
return typeof value === "string"
? value
: layer.placeholder || layer.layerName;
return value || layer.placeholder || layer.layerName;
}
function getResolvedTextLayerColor(
@ -1026,11 +1023,10 @@ function getTextLayerRenderFrame(
});
const width = Math.max(layer.width, resolvedLayout.contentWidth);
const height = Math.max(layer.height, resolvedLayout.contentHeight);
const textAlign = resolveAgentPsdTextLayerEffectiveAlign(layer);
const offsetX =
textAlign === "right"
layer.textAlign === "right"
? layer.width - width
: textAlign === "center"
: layer.textAlign === "center"
? (layer.width - width) / 2
: 0;
@ -1050,17 +1046,16 @@ function renderTextLayerPreviewContent(
) {
const resolvedText = getResolvedTextPreview(layer, value);
const resolvedLayout = getTextLayerRenderFrame(layer, resolvedText);
const textAlign = resolveAgentPsdTextLayerEffectiveAlign(layer);
const textAnchor =
textAlign === "center"
layer.textAlign === "center"
? "middle"
: textAlign === "right"
: layer.textAlign === "right"
? "end"
: "start";
const textX =
textAlign === "center"
layer.textAlign === "center"
? resolvedLayout.width / 2
: textAlign === "right"
: layer.textAlign === "right"
? resolvedLayout.width - layer.paddingX
: layer.paddingX;
@ -3048,7 +3043,7 @@ export function AgentPsdTemplateEditorDialog({
}
const value =
effectiveTextAssignments[layer.id] ?? "";
effectiveTextAssignments[layer.id] || "";
const resolvedTextColor =
effectiveTextColorAssignments[layer.id] ||
layer.textColor;
@ -3081,7 +3076,7 @@ export function AgentPsdTemplateEditorDialog({
const textFrame = getTextLayerRenderFrame(
layer,
value
value || layer.placeholder || layer.layerName
);
return (
@ -3097,7 +3092,7 @@ export function AgentPsdTemplateEditorDialog({
>
{renderTextLayerPreviewContent(
layer,
value,
value || layer.placeholder || layer.layerName,
resolvedTextColor
)}
</div>
@ -3248,7 +3243,7 @@ export function AgentPsdTemplateEditorDialog({
isSelected
);
const currentValue =
effectiveTextAssignments[layer.id] ?? "";
effectiveTextAssignments[layer.id] || "";
const resolvedTextColor =
effectiveTextColorAssignments[layer.id] ||
layer.textColor;
@ -3501,7 +3496,7 @@ export function AgentPsdTemplateEditorDialog({
}
rows={4}
value={
effectiveTextAssignments[selectedTextLayer.id] ?? ""
effectiveTextAssignments[selectedTextLayer.id] || ""
}
/>
<div className="flex flex-wrap gap-x-4 gap-y-2 rounded-xl bg-white/[0.02] px-4 py-3 text-white/60 text-xs shadow-inner">

View File

@ -171,7 +171,8 @@ export function AgentWorkflowMaterialPicker({
recommendedImageIds.every((id) => selectedSet.has(id));
const isAutoSelectedResult =
Boolean(hasSystemRecommendedSelection) && !hasManualSelectionInteraction;
const canTogglePreviewMode = totalImageCount > 0;
const canTogglePreviewMode =
totalImageCount > 0 && selectedGalleryCount > 0 && selectedGalleryCount < totalImageCount;
const visibleImages = useMemo(() => {
if (!galleryImages.length) {
return [];
@ -200,8 +201,8 @@ export function AgentWorkflowMaterialPicker({
if (hasManualSelectionInteraction) {
return;
}
setPreviewDisplayMode("all");
}, [hasManualSelectionInteraction, previewResetKey]);
setPreviewDisplayMode(hasSystemRecommendedSelection ? "selected" : "all");
}, [hasManualSelectionInteraction, hasSystemRecommendedSelection, previewResetKey]);
const hasFetchSuccess = !disabled && !isLoading && !errorText && Boolean(result);

View File

@ -68,7 +68,6 @@ type EnhanceProClientConfig = {
defaultOutputFormat: EnhanceProOutputFormat;
allowBatchSize: boolean;
maxBatchSize: number;
maxUpscaleFactor: number;
maxOutputEdge: number;
enableAdvancedControls: boolean;
presets: EnhanceProPreset[];
@ -110,7 +109,6 @@ const FALLBACK_CONFIG: EnhanceProClientConfig = {
defaultOutputFormat: "jpg",
allowBatchSize: true,
maxBatchSize: 4,
maxUpscaleFactor: 4,
maxOutputEdge: 4096,
enableAdvancedControls: true,
presets: defaultEnhanceProPresets(),
@ -246,11 +244,7 @@ function applyPresetToForm(
crop_to_fill: preset.cropToFill,
model: preset.model,
upscaling_activated: preset.upscalingActivated,
image_scaling_factor: clampNumber(
preset.imageScalingFactor,
1,
config.maxUpscaleFactor
),
image_scaling_factor: clampNumber(preset.imageScalingFactor, 1, 32),
creativity: clampNumber(Math.round(preset.creativity), 1, 6),
texture: clampNumber(Math.round(preset.texture), 1, 5),
sharpen: clampNumber(preset.sharpen, 0, 1),
@ -368,7 +362,6 @@ interface EnhanceProWorkspaceProps {
onRequestAddImage?: () => void;
onApplyResult: (payload: {
imageUrl: string;
sourceImageUrl?: string;
prompt: string;
title?: string;
model?: string;
@ -457,19 +450,9 @@ export function EnhanceProWorkspace({
height: formState.height,
batchSize: formState.batchSize,
model: formState.model,
upscalingActivated: formState.upscaling_activated,
scaleFactor: formState.image_scaling_factor,
}
),
[
formState.batchSize,
formState.height,
formState.image_scaling_factor,
formState.model,
formState.upscaling_activated,
formState.width,
pointsConfig,
]
[formState.batchSize, formState.height, formState.model, formState.width, pointsConfig]
);
useEffect(() => {
@ -595,7 +578,7 @@ export function EnhanceProWorkspace({
image_scaling_factor: clampNumber(
formState.image_scaling_factor,
1,
config.maxUpscaleFactor
32
),
creativity: clampNumber(Math.round(formState.creativity), 1, 6),
texture: clampNumber(Math.round(formState.texture), 1, 5),
@ -630,7 +613,6 @@ export function EnhanceProWorkspace({
}
onApplyResult({
imageUrl: selectedGeneratedImage.originalUrl,
sourceImageUrl: sourceImageUrl || undefined,
prompt: formState.prompt.trim() || "图像增强 Pro",
title: "图像增强 Pro 结果",
model: selectedPreset?.model || config.defaultModel,
@ -757,14 +739,14 @@ export function EnhanceProWorkspace({
<div className="relative flex flex-col items-center justify-center gap-3 px-6 text-center">
<Button
className={cn(
"h-16 rounded-[22px] px-10 text-[15px] font-semibold tracking-[-0.02em] sm:h-20 sm:px-12 sm:text-lg",
"h-14 rounded-[22px] px-8 text-[15px] font-semibold tracking-[-0.02em]",
PRIMARY_ACTION_BUTTON_CLASS
)}
disabled={!onRequestAddImage}
onClick={onRequestAddImage}
type="button"
>
<ImageIcon className="mr-2 h-5 w-5 sm:h-6 sm:w-6" />
<ImageIcon className="mr-2 h-5 w-5" />
添加图片
</Button>
<div className="text-[#666666] text-[13px] leading-6">
@ -822,9 +804,6 @@ export function EnhanceProWorkspace({
{pointsEstimate.chargeMode === "resolution_bucket" && pointsEstimate.bucketName
? ` · ${pointsEstimate.bucketName}`
: ""}
{pointsEstimate.chargeMode === "scale_factor"
? ` · ${pointsEstimate.scaleFactor}x`
: ""}
</div>
) : null}
<div className="pointer-events-auto inline-flex items-center justify-center gap-2 rounded-[32px] border border-[#e5e5e5] bg-white p-2 shadow-[0_0_0_1px_rgba(0,0,0,0.03),0_18px_38px_rgba(0,0,0,0.08)] backdrop-blur-xl">
@ -1151,7 +1130,7 @@ export function EnhanceProWorkspace({
<Input
className={COMPACT_FIELD_CLASS}
id="enhance-workspace-scale"
max={config.maxUpscaleFactor}
max="32"
min="1"
onChange={(event) =>
handleNumericField(
@ -1159,7 +1138,7 @@ export function EnhanceProWorkspace({
clampNumber(
Number.parseFloat(event.target.value) || 1,
1,
config.maxUpscaleFactor
32
)
)
}

View File

@ -148,7 +148,6 @@ type RealtimeCanvasDialogProps = {
initialMode?: RealtimeWorkspaceMode;
onApplyResult: (payload: {
imageUrl: string;
sourceImageUrl?: string;
prompt: string;
title?: string;
model?: string;
@ -2957,7 +2956,6 @@ export function RealtimeCanvasDialog({
const handleApplyEnhanceResult = useCallback(
(payload: {
imageUrl: string;
sourceImageUrl?: string;
prompt: string;
title?: string;
model?: string;

View File

@ -38,7 +38,6 @@ type EnhanceProClientConfig = {
defaultOutputFormat: EnhanceProOutputFormat;
allowBatchSize: boolean;
maxBatchSize: number;
maxUpscaleFactor: number;
maxOutputEdge: number;
enableAdvancedControls: boolean;
presets: EnhanceProPreset[];
@ -74,7 +73,6 @@ const FALLBACK_CONFIG: EnhanceProClientConfig = {
defaultOutputFormat: "jpg",
allowBatchSize: true,
maxBatchSize: 4,
maxUpscaleFactor: 4,
maxOutputEdge: 4096,
enableAdvancedControls: true,
presets: defaultEnhanceProPresets(),
@ -153,11 +151,7 @@ function applyPresetToForm(
crop_to_fill: preset.cropToFill,
model: preset.model,
upscaling_activated: preset.upscalingActivated,
image_scaling_factor: clampNumber(
preset.imageScalingFactor,
1,
config.maxUpscaleFactor
),
image_scaling_factor: clampNumber(preset.imageScalingFactor, 1, 32),
creativity: clampNumber(Math.round(preset.creativity), 1, 6),
texture: clampNumber(Math.round(preset.texture), 1, 5),
sharpen: clampNumber(preset.sharpen, 0, 1),
@ -280,11 +274,7 @@ export function EnhanceProDialog({
crop_to_fill: formState.crop_to_fill,
model: formState.model,
upscaling_activated: formState.upscaling_activated,
image_scaling_factor: clampNumber(
formState.image_scaling_factor,
1,
config.maxUpscaleFactor
),
image_scaling_factor: clampNumber(formState.image_scaling_factor, 1, 32),
creativity: clampNumber(Math.round(formState.creativity), 1, 6),
texture: clampNumber(Math.round(formState.texture), 1, 5),
sharpen: clampNumber(formState.sharpen, 0, 1),
@ -559,17 +549,13 @@ export function EnhanceProDialog({
id="enhance-pro-image-scaling"
type="number"
min="1"
max={config.maxUpscaleFactor}
max="32"
step="0.5"
value={formState.image_scaling_factor}
onChange={(event) =>
handleNumericField(
"image_scaling_factor",
clampNumber(
Number.parseFloat(event.target.value) || 1,
1,
config.maxUpscaleFactor
),
clampNumber(Number.parseFloat(event.target.value) || 1, 1, 32),
)
}
/>

View File

@ -95,12 +95,9 @@ export async function executeAgentPsdTemplateRenderRequest(
}
if (isAgentPsdTextLayerSlot(layer)) {
if (
Object.prototype.hasOwnProperty.call(body.textAssignments, layer.id)
) {
textAssignments[layer.id] = String(
body.textAssignments[layer.id] ?? ""
).trim();
const nextText = String(body.textAssignments[layer.id] || layer.defaultText || "").trim();
if (nextText) {
textAssignments[layer.id] = nextText;
}
textColorAssignments[layer.id] = normalizeHexColor(
body.textColorAssignments[layer.id] || layer.textColor,
@ -115,8 +112,7 @@ export async function executeAgentPsdTemplateRenderRequest(
layer.isRequired &&
(isAgentPsdImageLayerSlot(layer)
? !imageAssignments[layer.id]
: !Object.prototype.hasOwnProperty.call(textAssignments, layer.id) &&
!String(layer.defaultText || "").trim())
: !textAssignments[layer.id])
);
if (missingRequiredLayer) {
throw new Error(`缺少必传素材:${missingRequiredLayer.layerName}`);

View File

@ -289,27 +289,6 @@ export function normalizeAgentPsdTextAlign(value: unknown): AgentPsdTextAlign {
return "left";
}
export function resolveAgentPsdTextLayerEffectiveAlign(
layer: Pick<
AgentPsdTemplateTextLayerSlot,
"paddingX" | "styleSource" | "textAlign" | "width"
>
): AgentPsdTextAlign {
const textAlign = normalizeAgentPsdTextAlign(layer.textAlign);
const paddingX = normalizePositiveInteger(layer.paddingX);
const width = normalizePositiveInteger(layer.width);
if (
layer.styleSource !== "custom" &&
textAlign === "center" &&
paddingX <= Math.max(2, Math.round(width * 0.02))
) {
return "left";
}
return textAlign;
}
export function normalizeAgentPsdTextStyleSource(value: unknown): AgentPsdTextStyleSource {
return normalizeText(value) === "custom" ? "custom" : "auto";
}

View File

@ -6,7 +6,6 @@ import {
hasAgentPsdTemplateStoredLayerSource,
isAgentPsdImageLayerSlot,
isAgentPsdTextLayerSlot,
resolveAgentPsdTextLayerEffectiveAlign,
type AgentPsdTemplate,
} from "@/lib/agents/psd-template-config";
import type { AgentPsdFont } from "@/lib/agents/psd-font-config";
@ -528,30 +527,17 @@ export async function renderAgentPsdTemplate(params: {
}
if (isAgentPsdTextLayerSlot(layer)) {
const hasTextAssignment = Object.prototype.hasOwnProperty.call(
textAssignments,
layer.id
);
const text = String(textAssignments[layer.id] || layer.defaultText || "").trim();
const defaultText = String(layer.defaultText || "").trim();
const text = String(
hasTextAssignment ? textAssignments[layer.id] ?? "" : defaultText
).trim();
const textColor = normalizeHexColor(
textColorAssignments[layer.id] || layer.textColor,
layer.textColor
);
const hasColorChange = normalizeHexColor(textColor, layer.textColor) !== normalizeHexColor(layer.textColor);
if (!text) {
if (!hasTextAssignment) {
await appendStoredLayer(layer);
}
continue;
}
if (text === defaultText && !hasColorChange) {
if (!text || (text === defaultText && !hasColorChange)) {
await appendStoredLayer(layer);
continue;
}
const textAlign = resolveAgentPsdTextLayerEffectiveAlign(layer);
const font = layer.fontId ? fontMap.get(layer.fontId) : undefined;
const fontBuffer = font?.fileUrl
@ -601,7 +587,7 @@ export async function renderAgentPsdTemplate(params: {
lineHeight: layer.lineHeight,
letterSpacing: layer.letterSpacing,
textColor,
textAlign,
textAlign: layer.textAlign,
paddingX: layer.paddingX,
paddingY: layer.paddingY,
maxLines: layer.maxLines,
@ -626,7 +612,7 @@ export async function renderAgentPsdTemplate(params: {
layerHeight: layer.height,
contentWidth: overlayLayout.contentWidth,
contentHeight: overlayLayout.contentHeight,
textAlign,
textAlign: layer.textAlign,
topOffset: -overlayLayout.visualOffsetY,
});

View File

@ -272,12 +272,9 @@ export function analyzeTextLayerPixels(params: {
const rightGap = Math.max(0, width - maxX - 1);
const centerTolerance = Math.max(4, width * 0.06);
const edgeTolerance = Math.max(6, width * 0.08);
const hasReliableCenterPadding =
leftGap >= edgeTolerance && rightGap >= edgeTolerance;
const contentCenterX = (minX + maxX) / 2;
const stageCenterX = (width - 1) / 2;
const inferredTextAlign: AgentPsdTextAlign =
hasReliableCenterPadding &&
Math.abs(contentCenterX - stageCenterX) <= centerTolerance &&
Math.abs(leftGap - rightGap) <= edgeTolerance
? "center"

View File

@ -78,9 +78,6 @@ function createImageConfig(fields: string[]) {
}
function createVideoDurationOptions(modelId: string) {
if (modelId.includes("/bytedance/seedance-2")) {
return Array.from({ length: 12 }, (_, index) => index + 4);
}
if (modelId.includes("/minimax/hailuo-02") || modelId.includes("/minimax/hailuo-2.3")) {
return [6, 10];
}
@ -117,18 +114,13 @@ function createVideoCapabilities(modelId: string, fields: string[]) {
const durationOptions = fields.includes("duration") ? createVideoDurationOptions(modelId) : [5];
const resolutionOptions = fields.includes("resolution")
? modelId.includes("/bytedance/seedance-2")
? ["480p", "720p"]
: (modelId.includes("/minimax/") ? ["720p", "1080p"] : ["480p", "720p", "1080p"])
? (modelId.includes("/minimax/") ? ["720p", "1080p"] : ["480p", "720p", "1080p"])
: ["720p"];
const ratioOptions = modelId.includes("/bytedance/seedance-2")
? ["16:9", "9:16"]
: ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"];
return {
supportedModes: Array.from(supportedModes),
supportedResolutions: resolutionOptions,
supportedRatios: ratioOptions,
supportedRatios: ["16:9", "9:16", "1:1", "4:3", "3:4", "21:9"],
durationRange: {
min: Math.min(...durationOptions),
max: Math.max(...durationOptions),
@ -215,7 +207,6 @@ function videoPreset(
creditCostPerVideo: 0,
videoDurationOptions: durationOptions,
creditCostPerVideoByDuration: {},
creditCostPerVideoByResolution: {},
enableLoadingPlaceholder: true,
loadingColor: "blue",
},

View File

@ -325,9 +325,6 @@ function normalizeBatchSize(value?: number) {
function normalizeVideoDuration(duration: unknown, endpoint: string) {
const value = Number.isFinite(Number(duration)) ? Math.round(Number(duration)) : 5;
if (endpoint.includes("/bytedance/seedance-2")) {
return clamp(value || 5, 4, 15);
}
if (endpoint.includes("/minimax/hailuo-02") || endpoint.includes("/minimax/hailuo-2.3")) {
return clamp(value || 6, 6, 10);
}
@ -348,9 +345,6 @@ function normalizeVideoDuration(duration: unknown, endpoint: string) {
function normalizeVideoResolution(resolution: unknown, endpoint: string) {
const normalized = normalizeText(String(resolution || "")).toLowerCase();
if (endpoint.includes("/bytedance/seedance-2")) {
return normalized === "480p" ? "480p" : "720p";
}
if (endpoint.includes("/minimax/")) {
if (normalized === "1080p") return "1080p";
if (normalized === "512p" || normalized === "480p") return "512p";
@ -362,9 +356,6 @@ function normalizeVideoResolution(resolution: unknown, endpoint: string) {
function normalizeVideoAspectRatio(ratio: unknown, endpoint: string) {
const normalized = normalizeAspectRatio(String(ratio || ""), "16:9");
if (endpoint.includes("/bytedance/seedance-2")) {
return normalized === "9:16" ? "9:16" : "16:9";
}
if (endpoint.includes("/openai/sora-2")) {
return normalized === "9:16" ? "9:16" : "16:9";
}
@ -517,79 +508,6 @@ async function downloadRemoteAsset(url: string) {
}
}
function stringifyForLog(value: unknown) {
try {
return JSON.stringify(value, null, 2).slice(0, 4000);
} catch {
return String(value).slice(0, 4000);
}
}
function isLikelyKreaHostedUrl(url: string) {
try {
const hostname = new URL(url).hostname.toLowerCase();
return hostname === "krea.ai" || hostname.endsWith(".krea.ai");
} catch {
return false;
}
}
async function uploadKreaReferenceAsset(
baseURL: string,
apiKey: string,
sourceUrl: string
) {
const normalized = normalizeText(sourceUrl);
if (!/^https?:\/\//i.test(normalized) || isLikelyKreaHostedUrl(normalized)) {
return normalized;
}
try {
const { buffer, contentType } = await downloadRemoteAsset(normalized);
const mimeType = contentType || "application/octet-stream";
if (!/^(image|video)\//i.test(mimeType)) return normalized;
const form = new FormData();
const blob = new Blob([buffer], { type: mimeType });
form.append("file", blob, generateFilename("krea-reference", detectExtensionFromContentType(mimeType)));
form.append("description", "Reference asset uploaded for Krea API generation");
const response = await fetch(joinEndpoint(baseURL, "/assets"), {
method: "POST",
headers: {
Authorization: `Bearer ${normalizeKreaApiKey(apiKey)}`,
Accept: "application/json",
},
body: form,
});
const rawText = await response.text();
let payload: any = null;
try {
payload = rawText ? JSON.parse(rawText) : null;
} catch {
payload = rawText;
}
if (!response.ok) {
console.warn("[Krea] Reference asset upload failed, falling back to source URL:", {
status: response.status,
sourceUrl: summarizeKreaUrl(normalized),
response: stringifyForLog(payload || rawText),
});
return normalized;
}
const assetUrl = normalizeText(payload?.image_url || payload?.url || payload?.asset_url);
return assetUrl || normalized;
} catch (error) {
console.warn("[Krea] Reference asset upload failed, falling back to source URL:", {
sourceUrl: summarizeKreaUrl(normalized),
error,
});
return normalized;
}
}
async function inferReferenceAspectRatioFromImageUrl(url?: string | null) {
const normalized = normalizeText(url);
if (!normalized || !/^https?:\/\//i.test(normalized)) return undefined;
@ -719,7 +637,7 @@ async function requestKreaJob(
endpoint,
status: response.status,
requestFields: Object.keys(body),
response: stringifyForLog(payload || rawText),
response: payload,
});
const message =
typeof payload?.error === "string"
@ -737,11 +655,6 @@ async function requestKreaJob(
if (!jobId) {
throw new Error("Krea 未返回任务 ID");
}
console.log("[Krea] Job accepted:", {
endpoint,
jobId,
status: normalizeText(payload?.status) || null,
});
return { ...payload, job_id: jobId };
}
@ -785,12 +698,6 @@ async function waitForKreaJob(
return payload;
}
if (status === "failed" || status === "cancelled") {
console.error("[Krea] Job failed:", {
jobId,
kind,
status,
response: stringifyForLog(payload),
});
throw new Error(String(payload?.error || payload?.message || "Krea 任务失败"));
}
@ -841,15 +748,6 @@ function buildKreaBody(params: {
const required = new Set(requiredFields);
const body: Record<string, any> = {};
const isVideo = kind === "video";
const videoMode =
isVideo
? videoParams?.mode ||
(imageUrls.length > 1
? "first-last-frame"
: imageUrls.length > 0
? "first-frame"
: "text-to-video")
: undefined;
const hasImageReference =
kind !== "video" && imageUrls.length > 0 && hasKreaSourceImageField(fields);
const requestedImageAspectRatio = normalizeText(imageParams?.aspectRatio).toLowerCase();
@ -943,26 +841,14 @@ function buildKreaBody(params: {
body.characterReferenceImages = imageUrls.slice(0, 4);
}
if (
fields.has("referenceImages") &&
imageUrls.length > 0 &&
(!isVideo || videoMode === "reference-images")
) {
if (fields.has("referenceImages") && imageUrls.length > 0) {
body.referenceImages = endpoint.includes("/image/runway/gen-4-image")
? buildTaggedReferences(imageUrls, "ref", 3)
: imageUrls.slice(0, endpoint.includes("/seedance-2") ? 9 : 4);
}
if (
fields.has("startImage") &&
imageUrls[0] &&
(!isVideo || videoMode === "first-frame" || videoMode === "first-last-frame")
) {
body.startImage = imageUrls[0];
}
if (fields.has("endImage") && imageUrls[1] && (!isVideo || videoMode === "first-last-frame")) {
body.endImage = imageUrls[1];
}
if (fields.has("startImage") && imageUrls[0]) body.startImage = imageUrls[0];
if (fields.has("endImage") && imageUrls[1]) body.endImage = imageUrls[1];
if (fields.has("init_image_url") && imageUrls[0]) body.init_image_url = imageUrls[0];
if (fields.has("references") && imageUrls[0]) {
body.references = [{ type: "image", uri: imageUrls[0] }];
@ -1029,11 +915,6 @@ export function createKrea({ apiKey, baseURL }: KreaConfig) {
const normalizedVideoUrls = (
await Promise.all(videoUrls.map((url) => normalizeAssetUrl(url)))
).filter(Boolean);
const kreaImageUrls = (
await Promise.all(
normalizedImageUrls.map((url) => uploadKreaReferenceAsset(baseURL, apiKey, url))
)
).filter(Boolean);
const requestFields =
preset?.requestFields ||
(kind === "video"
@ -1052,7 +933,7 @@ export function createKrea({ apiKey, baseURL }: KreaConfig) {
endpoint,
kind,
prompt,
imageUrls: kreaImageUrls,
imageUrls: normalizedImageUrls,
requestFields,
});
const { body, imageSize } = buildKreaBody({
@ -1061,7 +942,7 @@ export function createKrea({ apiKey, baseURL }: KreaConfig) {
requestFields,
requiredFields,
prompt: promptForKrea,
imageUrls: kreaImageUrls,
imageUrls: normalizedImageUrls,
videoUrls: normalizedVideoUrls,
referenceAspectRatio,
imageParams: options.imageParams,

View File

@ -1,11 +1,9 @@
export type VideoDurationCostMap = Record<string, number>;
export type VideoResolutionCostMap = Record<string, number>;
export type VideoImageGenConfigLike = {
creditCostPerVideo?: number | null;
videoDurationOptions?: number[] | null;
creditCostPerVideoByDuration?: Record<string, number> | null;
creditCostPerVideoByResolution?: Record<string, number> | null;
};
function normalizePositiveInteger(value: unknown) {
@ -56,42 +54,6 @@ export function normalizeVideoDurationCostMap(
return Object.fromEntries(entries);
}
export function normalizeVideoResolutionKey(value: unknown) {
const normalized = String(value || "").trim().toLowerCase();
if (!normalized) return null;
if (normalized === "480p" || normalized === "480") return "480p";
if (normalized === "720p" || normalized === "720") return "720p";
if (normalized === "1080p" || normalized === "1080") return "1080p";
if (normalized === "1k") return "1K";
if (normalized === "2k") return "2K";
if (normalized === "4k") return "4K";
return null;
}
export function normalizeVideoResolutionCostMap(
value: unknown
): VideoResolutionCostMap | undefined {
if (!value || typeof value !== "object") return undefined;
const entries: Array<readonly [string, number]> = [];
for (const [resolutionKey, costValue] of Object.entries(
value as Record<string, unknown>
)) {
const normalizedResolution = normalizeVideoResolutionKey(resolutionKey);
const normalizedCost = Number(costValue);
if (!normalizedResolution || !Number.isFinite(normalizedCost)) {
continue;
}
entries.push([
normalizedResolution,
Math.max(0, Math.round(normalizedCost)),
] as const);
}
if (entries.length === 0) return undefined;
return Object.fromEntries(entries);
}
export function getConfiguredVideoDurationOptions(
config: VideoImageGenConfigLike | null | undefined
): number[] {
@ -123,20 +85,9 @@ export function buildVideoDurationCostMap(params: {
export function getVideoCreditCostFromConfig(
config: VideoImageGenConfigLike | null | undefined,
duration?: number | null,
resolution?: string | null
duration?: number | null
): number {
const normalizedDuration = normalizePositiveInteger(duration);
const normalizedResolution = normalizeVideoResolutionKey(resolution);
const resolutionCostMap = normalizeVideoResolutionCostMap(
config?.creditCostPerVideoByResolution
);
if (normalizedResolution && resolutionCostMap) {
if (Object.prototype.hasOwnProperty.call(resolutionCostMap, normalizedResolution)) {
return resolutionCostMap[normalizedResolution] || 0;
}
}
const durationOptions = getConfiguredVideoDurationOptions(config);
const durationCostMap = normalizeVideoDurationCostMap(
config?.creditCostPerVideoByDuration,

View File

@ -3925,7 +3925,6 @@ interface ImageGenConfig {
creditCostPerVideo?: number;
videoDurationOptions?: number[];
creditCostPerVideoByDuration?: Record<string, number>;
creditCostPerVideoByResolution?: Record<string, number>;
enableLoadingPlaceholder?: boolean;
loadingColor?: string;
}

View File

@ -49,7 +49,6 @@ export type EnhanceProKreaConfig = {
defaultOutputFormat: EnhanceProOutputFormat;
allowBatchSize: boolean;
maxBatchSize: number;
maxUpscaleFactor: number;
maxOutputEdge: number;
enableAdvancedControls: boolean;
presets: EnhanceProPreset[];
@ -196,10 +195,7 @@ function normalizeEnhanceProPreset(
index: number,
configDefaults: Pick<
EnhanceProKreaConfig,
| "defaultModel"
| "defaultOutputFormat"
| "maxBatchSize"
| "maxUpscaleFactor"
"defaultModel" | "defaultOutputFormat" | "maxBatchSize"
>,
): EnhanceProPreset {
const defaults = defaultEnhanceProPresets()[index] || defaultEnhanceProPresets()[0];
@ -275,7 +271,7 @@ function normalizeEnhanceProPreset(
imageScalingFactor: clampFloat(
raw.imageScalingFactor,
1,
configDefaults.maxUpscaleFactor,
32,
defaults.imageScalingFactor,
),
creativity: clampInteger(raw.creativity, 1, 6, defaults.creativity),
@ -297,7 +293,6 @@ export function defaultEnhanceProKreaConfig(): EnhanceProKreaConfig {
defaultOutputFormat: ENHANCE_PRO_KREA_DEFAULT_OUTPUT_FORMAT,
allowBatchSize: true,
maxBatchSize: 4,
maxUpscaleFactor: 4,
maxOutputEdge: 4096,
enableAdvancedControls: true,
presets: defaultEnhanceProPresets(),
@ -316,22 +311,15 @@ export function normalizeEnhanceProKreaConfig(
const defaultModel = String(raw.defaultModel || "").trim();
const defaultOutputFormat = String(raw.defaultOutputFormat || "").trim();
const maxUpscaleFactor = clampFloat(
raw.maxUpscaleFactor,
1,
32,
defaults.maxUpscaleFactor,
);
const normalizedPresets = (
Array.isArray(raw.presets) ? raw.presets : defaultEnhanceProPresets()
).map((item, index) =>
normalizeEnhanceProPreset(item, index, {
defaultModel: defaults.defaultModel,
defaultOutputFormat: defaults.defaultOutputFormat,
maxBatchSize: clampInteger(raw.maxBatchSize, 1, 4, defaults.maxBatchSize),
maxUpscaleFactor,
}),
);
const normalizedPresets = Array.isArray(raw.presets)
? raw.presets.map((item, index) =>
normalizeEnhanceProPreset(item, index, {
defaultModel: defaults.defaultModel,
defaultOutputFormat: defaults.defaultOutputFormat,
maxBatchSize: clampInteger(raw.maxBatchSize, 1, 4, defaults.maxBatchSize),
}),
)
: defaultEnhanceProPresets();
const presetWithDefault = normalizedPresets.some((preset) => preset.isDefault)
? normalizedPresets
: normalizedPresets.map((preset, index) => ({
@ -354,8 +342,7 @@ export function normalizeEnhanceProKreaConfig(
defaultModel:
defaultModel === "Recovery" ||
defaultModel === "Recovery V2" ||
defaultModel === "Reimagine" ||
defaultModel === "Redefine"
defaultModel === "Reimagine"
? defaultModel
: defaults.defaultModel,
defaultOutputFormat:
@ -366,7 +353,6 @@ export function normalizeEnhanceProKreaConfig(
: defaults.defaultOutputFormat,
allowBatchSize: raw.allowBatchSize !== false,
maxBatchSize: clampInteger(raw.maxBatchSize, 1, 4, defaults.maxBatchSize),
maxUpscaleFactor,
maxOutputEdge: clampInteger(
raw.maxOutputEdge,
512,
@ -390,7 +376,6 @@ export function sanitizeEnhanceProKreaConfigForClient(
defaultOutputFormat: config.defaultOutputFormat,
allowBatchSize: config.allowBatchSize,
maxBatchSize: config.maxBatchSize,
maxUpscaleFactor: config.maxUpscaleFactor,
maxOutputEdge: config.maxOutputEdge,
enableAdvancedControls: config.enableAdvancedControls,
presets: config.presets,

View File

@ -3,10 +3,7 @@ import type { EnhanceProKreaModel } from "@/lib/enhance-pro/config";
export const POINTS_SYSTEM_CONFIG_KEY = "points_system_config";
export const POINTS_PACKAGES_KEY = "points_packages";
export type EnhanceProPointsChargeMode =
| "fixed"
| "resolution_bucket"
| "scale_factor";
export type EnhanceProPointsChargeMode = "fixed" | "resolution_bucket";
export type EnhanceProPointsResolutionBucket = {
id: string;
@ -19,9 +16,7 @@ export type EnhanceProPointsPricingConfig = {
enabled: boolean;
chargeMode: EnhanceProPointsChargeMode;
fixedCost: number;
chargeOnlyWhenUpscaling: boolean;
batchSizeMultiplierEnabled: boolean;
scaleFactorCosts: Record<string, number>;
resolutionBuckets: EnhanceProPointsResolutionBucket[];
modelMultipliers: Record<EnhanceProKreaModel, number>;
};
@ -31,8 +26,6 @@ export type PointsSystemConfig = {
blockOnInsufficient: boolean;
creditCostPer3DModel: number;
creditCostPer3DWorld: number;
creditCostPer3DWorldMarble11: number;
creditCostPer3DWorldMarble11Plus: number;
enhanceProPricing: EnhanceProPointsPricingConfig;
};
@ -50,21 +43,11 @@ export const defaultPointsSystemConfig: PointsSystemConfig = {
blockOnInsufficient: true,
creditCostPer3DModel: 0,
creditCostPer3DWorld: 0,
creditCostPer3DWorldMarble11: 0,
creditCostPer3DWorldMarble11Plus: 0,
enhanceProPricing: {
enabled: false,
chargeMode: "scale_factor",
chargeMode: "resolution_bucket",
fixedCost: 45,
chargeOnlyWhenUpscaling: true,
batchSizeMultiplierEnabled: true,
scaleFactorCosts: {
"1": 0,
"1.5": 15,
"2": 20,
"3": 35,
"4": 45,
},
resolutionBuckets: [
{
id: "enhance-pro-1mp",
@ -146,46 +129,6 @@ function normalizeMegapixels(input: unknown, fallback: number) {
: fallback;
}
function normalizeScaleFactor(input: unknown, fallback: number) {
const parsedValue = Number(input);
return Number.isFinite(parsedValue) && parsedValue > 0
? Math.min(32, Math.max(1, Number(parsedValue.toFixed(2))))
: fallback;
}
function tryParseScaleFactor(input: unknown) {
const parsedValue = Number(input);
if (!Number.isFinite(parsedValue) || parsedValue <= 0) return null;
return Math.min(32, Math.max(1, Number(parsedValue.toFixed(2))));
}
export function formatEnhanceProScaleFactorKey(input: unknown) {
const normalized = normalizeScaleFactor(input, 1);
return Number(normalized.toFixed(2)).toString();
}
function normalizeEnhanceProScaleFactorCosts(value: unknown) {
const defaults = defaultPointsSystemConfig.enhanceProPricing.scaleFactorCosts;
if (!value || typeof value !== "object" || Array.isArray(value)) {
return defaults;
}
const normalizedEntries = Object.entries(value as Record<string, unknown>)
.map(([factorKey, costValue]) => {
const factor = tryParseScaleFactor(factorKey);
if (factor === null) return null;
return [formatEnhanceProScaleFactorKey(factor), normalizeCost(costValue)] as const;
})
.filter((entry): entry is readonly [string, number] => Boolean(entry))
.sort((left, right) => Number(left[0]) - Number(right[0]));
if (normalizedEntries.length === 0) {
return defaults;
}
return Object.fromEntries(normalizedEntries);
}
function normalizeEnhanceProResolutionBucket(
value: unknown,
index: number,
@ -243,22 +186,12 @@ export function normalizeEnhanceProPointsPricingConfig(
raw.modelMultipliers && typeof raw.modelMultipliers === "object"
? raw.modelMultipliers
: defaults.modelMultipliers;
const chargeMode: EnhanceProPointsChargeMode =
raw.chargeMode === "fixed" ||
raw.chargeMode === "resolution_bucket" ||
raw.chargeMode === "scale_factor"
? raw.chargeMode
: defaults.chargeMode;
return {
enabled: raw.enabled === true,
chargeMode,
chargeMode: raw.chargeMode === "fixed" ? "fixed" : "resolution_bucket",
fixedCost: normalizeCost(raw.fixedCost ?? legacyFixedCost ?? defaults.fixedCost),
chargeOnlyWhenUpscaling: raw.chargeOnlyWhenUpscaling !== false,
batchSizeMultiplierEnabled: raw.batchSizeMultiplierEnabled !== false,
scaleFactorCosts: normalizeEnhanceProScaleFactorCosts(
raw.scaleFactorCosts
),
resolutionBuckets:
resolutionBuckets.length > 0 ? resolutionBuckets : defaults.resolutionBuckets,
modelMultipliers: {
@ -298,8 +231,6 @@ export function calculateEnhanceProPointsEstimate(
height: number;
batchSize?: number;
model?: EnhanceProKreaModel;
upscalingActivated?: boolean;
scaleFactor?: number;
}
) {
const pricing = normalizeEnhanceProPointsPricingConfig(
@ -314,8 +245,6 @@ export function calculateEnhanceProPointsEstimate(
: "Redefine";
const megapixels = getEnhanceProResolutionMegapixels(params.width, params.height);
const batchSize = Math.max(1, Math.round(Number(params.batchSize) || 1));
const upscalingActivated = params.upscalingActivated === true;
const scaleFactor = normalizeScaleFactor(params.scaleFactor ?? 1, 1);
const batchMultiplier = pricing.batchSizeMultiplierEnabled ? batchSize : 1;
const modelMultiplier =
pricing.modelMultipliers[model] || defaultPointsSystemConfig.enhanceProPricing.modelMultipliers[model];
@ -325,32 +254,10 @@ export function calculateEnhanceProPointsEstimate(
pricing.resolutionBuckets[pricing.resolutionBuckets.length - 1] ||
null
: null;
const scaleFactorCosts = normalizeEnhanceProScaleFactorCosts(
pricing.scaleFactorCosts
);
const scaleFactorCostEntries = Object.entries(scaleFactorCosts)
.map(([factorKey, cost]) => ({
factorKey,
factor: Number(factorKey),
cost,
}))
.filter((item) => Number.isFinite(item.factor))
.sort((left, right) => left.factor - right.factor);
const scaleFactorCost =
scaleFactorCostEntries.find(
(item) => Math.abs(item.factor - scaleFactor) < 0.001
) ||
scaleFactorCostEntries.find((item) => scaleFactor <= item.factor) ||
scaleFactorCostEntries[scaleFactorCostEntries.length - 1] ||
null;
const baseCost =
pricing.chargeOnlyWhenUpscaling && !upscalingActivated
? 0
: pricing.chargeMode === "fixed"
? pricing.fixedCost
: pricing.chargeMode === "scale_factor"
? Math.max(0, Number(scaleFactorCost?.cost || 0))
: Math.max(0, Number(bucket?.cost || 0));
pricing.chargeMode === "fixed"
? pricing.fixedCost
: Math.max(0, Number(bucket?.cost || 0));
const rawCost = baseCost * batchMultiplier * modelMultiplier;
const cost = baseCost > 0 ? Math.max(1, Math.ceil(rawCost)) : 0;
const enabled = pointsConfig.enabled === true && pricing.enabled === true;
@ -359,9 +266,6 @@ export function calculateEnhanceProPointsEstimate(
enabled,
chargeable: enabled && cost > 0,
chargeMode: pricing.chargeMode,
chargeOnlyWhenUpscaling: pricing.chargeOnlyWhenUpscaling,
upscalingActivated,
scaleFactor,
megapixels,
batchSize,
batchMultiplier,
@ -369,26 +273,11 @@ export function calculateEnhanceProPointsEstimate(
modelMultiplier,
baseCost,
cost,
scaleFactorCostKey: scaleFactorCost?.factorKey || null,
bucketId: bucket?.id || null,
bucketName: bucket?.name || null,
};
}
export function get3DWorldCreditCost(
pointsConfig: PointsSystemConfig,
modelId?: string | null
) {
const normalizedModelId = String(modelId || "").trim().toLowerCase();
if (normalizedModelId === "marble-1.1-plus") {
return normalizeCost(pointsConfig.creditCostPer3DWorldMarble11Plus);
}
if (normalizedModelId === "marble-1.1") {
return normalizeCost(pointsConfig.creditCostPer3DWorldMarble11);
}
return normalizeCost(pointsConfig.creditCostPer3DWorld);
}
export function normalizePointsSystemConfig(value: unknown): PointsSystemConfig {
if (!value || typeof value !== "object") {
return defaultPointsSystemConfig;
@ -399,9 +288,6 @@ export function normalizePointsSystemConfig(value: unknown): PointsSystemConfig
creditCostPerEnhancePro?: number;
};
const legacy3DCost = normalizeCost(raw.creditCostPer3D);
const generic3DWorldCost = normalizeCost(
raw.creditCostPer3DWorld ?? legacy3DCost
);
const legacyEnhanceProCost = normalizeCost(raw.creditCostPerEnhancePro);
return {
enabled: raw.enabled === true,
@ -412,12 +298,6 @@ export function normalizePointsSystemConfig(value: unknown): PointsSystemConfig
creditCostPer3DWorld: normalizeCost(
raw.creditCostPer3DWorld ?? legacy3DCost
),
creditCostPer3DWorldMarble11: normalizeCost(
raw.creditCostPer3DWorldMarble11 ?? generic3DWorldCost
),
creditCostPer3DWorldMarble11Plus: normalizeCost(
raw.creditCostPer3DWorldMarble11Plus ?? generic3DWorldCost
),
enhanceProPricing: normalizeEnhanceProPointsPricingConfig(
raw.enhanceProPricing,
legacyEnhanceProCost