feat: refine krea enhance pro billing
This commit is contained in:
parent
f75f77105d
commit
8e51697800
@ -368,6 +368,7 @@ type ImageGenConfig = {
|
|||||||
creditCostPerVideo?: number; // 每条视频积分消耗
|
creditCostPerVideo?: number; // 每条视频积分消耗
|
||||||
videoDurationOptions?: number[]; // 视频秒数选项
|
videoDurationOptions?: number[]; // 视频秒数选项
|
||||||
creditCostPerVideoByDuration?: Record<string, number>; // 各秒数积分
|
creditCostPerVideoByDuration?: Record<string, number>; // 各秒数积分
|
||||||
|
creditCostPerVideoByResolution?: Record<string, number>; // 各分辨率积分
|
||||||
enableLoadingPlaceholder?: boolean; // 是否启用加载占位图
|
enableLoadingPlaceholder?: boolean; // 是否启用加载占位图
|
||||||
loadingColor?: string; // 占位图主题色,如 "purple", "blue", "green"
|
loadingColor?: string; // 占位图主题色,如 "purple", "blue", "green"
|
||||||
};
|
};
|
||||||
@ -387,6 +388,7 @@ function createDefaultImageGenConfig(): ImageGenConfig {
|
|||||||
creditCostPerVideo: 0,
|
creditCostPerVideo: 0,
|
||||||
videoDurationOptions: [],
|
videoDurationOptions: [],
|
||||||
creditCostPerVideoByDuration: {},
|
creditCostPerVideoByDuration: {},
|
||||||
|
creditCostPerVideoByResolution: {},
|
||||||
enableLoadingPlaceholder: true,
|
enableLoadingPlaceholder: true,
|
||||||
loadingColor: "purple",
|
loadingColor: "purple",
|
||||||
};
|
};
|
||||||
@ -2910,6 +2912,28 @@ 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 = (
|
const updateMembershipConfigState = (
|
||||||
updater: (
|
updater: (
|
||||||
config: ReturnType<typeof normalizeMembershipConfig>
|
config: ReturnType<typeof normalizeMembershipConfig>
|
||||||
@ -5566,7 +5590,7 @@ export function AdminSettingsClient() {
|
|||||||
id={`enhance-pro-preset-scale-${preset.id}`}
|
id={`enhance-pro-preset-scale-${preset.id}`}
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="32"
|
max={enhanceProConfig.maxUpscaleFactor}
|
||||||
step="0.5"
|
step="0.5"
|
||||||
value={preset.imageScalingFactor}
|
value={preset.imageScalingFactor}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@ -5574,7 +5598,7 @@ export function AdminSettingsClient() {
|
|||||||
index,
|
index,
|
||||||
"imageScalingFactor",
|
"imageScalingFactor",
|
||||||
Math.min(
|
Math.min(
|
||||||
32,
|
enhanceProConfig.maxUpscaleFactor,
|
||||||
Math.max(1, Number.parseFloat(e.target.value) || 1)
|
Math.max(1, Number.parseFloat(e.target.value) || 1)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -8208,6 +8232,60 @@ export function AdminSettingsClient() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
<div className="rounded-lg border p-4 space-y-4">
|
<div className="rounded-lg border p-4 space-y-4">
|
||||||
@ -8242,7 +8320,7 @@ export function AdminSettingsClient() {
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label>计费模式</Label>
|
<Label>计费模式</Label>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
固定单价适合先快速上线,分辨率档位更适合贴近增强成本。
|
可按放大倍率、分辨率档位或固定单价计算本地积分。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 w-full max-w-xs">
|
<div className="mt-2 w-full max-w-xs">
|
||||||
@ -8257,7 +8335,9 @@ export function AdminSettingsClient() {
|
|||||||
chargeMode:
|
chargeMode:
|
||||||
value === "fixed"
|
value === "fixed"
|
||||||
? "fixed"
|
? "fixed"
|
||||||
: "resolution_bucket",
|
: value === "resolution_bucket"
|
||||||
|
? "resolution_bucket"
|
||||||
|
: "scale_factor",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -8268,6 +8348,9 @@ export function AdminSettingsClient() {
|
|||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
<SelectItem value="scale_factor">
|
||||||
|
放大倍率
|
||||||
|
</SelectItem>
|
||||||
<SelectItem value="resolution_bucket">
|
<SelectItem value="resolution_bucket">
|
||||||
分辨率档位
|
分辨率档位
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@ -8312,6 +8395,66 @@ export function AdminSettingsClient() {
|
|||||||
</div>
|
</div>
|
||||||
</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="flex items-center justify-between rounded-lg border p-3">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<Label>按候选结果数量线性乘倍</Label>
|
<Label>按候选结果数量线性乘倍</Label>
|
||||||
@ -8485,7 +8628,7 @@ export function AdminSettingsClient() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="rounded-lg border border-dashed p-3 text-xs text-muted-foreground">
|
<div className="rounded-lg border border-dashed p-3 text-xs text-muted-foreground">
|
||||||
建议算法:按输出分辨率档位取基础积分,再乘以候选数量和模型倍率,最终向上取整。
|
建议算法:按放大倍率或输出分辨率取基础积分,再乘以候选数量和模型倍率,最终向上取整。
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@ -11930,6 +12073,62 @@ export function AdminSettingsClient() {
|
|||||||
当前还没有配置秒数档位,将按“每条视频积分消耗”统一扣费。
|
当前还没有配置秒数档位,将按“每条视频积分消耗”统一扣费。
|
||||||
</p>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -15006,7 +15205,7 @@ export function AdminSettingsClient() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="enhanceProMaxBatchSize">最大候选图数量</Label>
|
<Label htmlFor="enhanceProMaxBatchSize">最大候选图数量</Label>
|
||||||
<Select
|
<Select
|
||||||
@ -15037,6 +15236,39 @@ export function AdminSettingsClient() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</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">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="enhanceProMaxOutputEdge">最大输出长边</Label>
|
<Label htmlFor="enhanceProMaxOutputEdge">最大输出长边</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@ -4087,13 +4087,17 @@ const MODEL_UUID_REGEX =
|
|||||||
function getModelCreditCost(
|
function getModelCreditCost(
|
||||||
modelConfig: any,
|
modelConfig: any,
|
||||||
target: "image" | "video",
|
target: "image" | "video",
|
||||||
options?: { duration?: number | null }
|
options?: { duration?: number | null; resolution?: string | null }
|
||||||
): number {
|
): number {
|
||||||
const config = (modelConfig as any)?.imageGenConfig || {};
|
const config = (modelConfig as any)?.imageGenConfig || {};
|
||||||
const sourceValue =
|
const sourceValue =
|
||||||
target === "video"
|
target === "video"
|
||||||
? (() => {
|
? (() => {
|
||||||
const resolvedVideoCost = getVideoCreditCostFromConfig(config, options?.duration);
|
const resolvedVideoCost = getVideoCreditCostFromConfig(
|
||||||
|
config,
|
||||||
|
options?.duration,
|
||||||
|
options?.resolution
|
||||||
|
);
|
||||||
if (resolvedVideoCost > 0) return resolvedVideoCost;
|
if (resolvedVideoCost > 0) return resolvedVideoCost;
|
||||||
return config.creditCostPerVideo ?? config.creditCostPerImage ?? 0;
|
return config.creditCostPerVideo ?? config.creditCostPerImage ?? 0;
|
||||||
})()
|
})()
|
||||||
@ -4122,6 +4126,7 @@ async function consumeGeneratedVideoPoints(params: {
|
|||||||
messageId: string;
|
messageId: string;
|
||||||
modelRefs: Array<string | null | undefined>;
|
modelRefs: Array<string | null | undefined>;
|
||||||
duration?: number | null;
|
duration?: number | null;
|
||||||
|
resolution?: string | null;
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const pointsConfig = await getPointsSystemConfig();
|
const pointsConfig = await getPointsSystemConfig();
|
||||||
@ -4139,13 +4144,21 @@ async function consumeGeneratedVideoPoints(params: {
|
|||||||
const resolved = await resolveBillingModelByReference(ref);
|
const resolved = await resolveBillingModelByReference(ref);
|
||||||
if (!resolved) continue;
|
if (!resolved) continue;
|
||||||
billingModel = resolved;
|
billingModel = resolved;
|
||||||
if (getModelCreditCost(resolved, "video", { duration: params.duration }) > 0) break;
|
if (
|
||||||
|
getModelCreditCost(resolved, "video", {
|
||||||
|
duration: params.duration,
|
||||||
|
resolution: params.resolution,
|
||||||
|
}) > 0
|
||||||
|
) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!billingModel) return;
|
if (!billingModel) return;
|
||||||
|
|
||||||
const unitCost = getModelCreditCost(billingModel, "video", {
|
const unitCost = getModelCreditCost(billingModel, "video", {
|
||||||
duration: params.duration,
|
duration: params.duration,
|
||||||
|
resolution: params.resolution,
|
||||||
});
|
});
|
||||||
if (unitCost <= 0) return;
|
if (unitCost <= 0) return;
|
||||||
|
|
||||||
@ -4156,7 +4169,7 @@ async function consumeGeneratedVideoPoints(params: {
|
|||||||
modelId: billingModel.id,
|
modelId: billingModel.id,
|
||||||
chatId: params.chatId,
|
chatId: params.chatId,
|
||||||
messageId: params.messageId,
|
messageId: params.messageId,
|
||||||
note: `模型(${billingModel.displayName || billingModel.modelId || billingModel.id}) 生视频 1 条${params.duration ? `(${params.duration} 秒)` : ""}`,
|
note: `模型(${billingModel.displayName || billingModel.modelId || billingModel.id}) 生视频 1 条${params.duration ? `(${params.duration} 秒)` : ""}${params.resolution ? `(${params.resolution})` : ""}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!consumeResult.ok) {
|
if (!consumeResult.ok) {
|
||||||
@ -6795,6 +6808,9 @@ export async function POST(request: Request) {
|
|||||||
messageId: finalMessageId,
|
messageId: finalMessageId,
|
||||||
modelRefs: executionResult.modelRefs,
|
modelRefs: executionResult.modelRefs,
|
||||||
duration: Number((finalVideoMetadata as any)?.duration) || undefined,
|
duration: Number((finalVideoMetadata as any)?.duration) || undefined,
|
||||||
|
resolution:
|
||||||
|
String((finalVideoMetadata as any)?.resolution || "").trim() ||
|
||||||
|
undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
scheduleVideoArchive({
|
scheduleVideoArchive({
|
||||||
@ -8099,6 +8115,9 @@ export async function POST(request: Request) {
|
|||||||
messageId: assistantMessageId,
|
messageId: assistantMessageId,
|
||||||
modelRefs: [result.modelRef || presetModelId],
|
modelRefs: [result.modelRef || presetModelId],
|
||||||
duration: Number((result.metadata as any)?.duration) || undefined,
|
duration: Number((result.metadata as any)?.duration) || undefined,
|
||||||
|
resolution:
|
||||||
|
String((result.metadata as any)?.resolution || "").trim() ||
|
||||||
|
undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
scheduleVideoArchive({
|
scheduleVideoArchive({
|
||||||
@ -8609,6 +8628,12 @@ export async function POST(request: Request) {
|
|||||||
Number((result.metadata as any)?.duration) ||
|
Number((result.metadata as any)?.duration) ||
|
||||||
Number(effectiveVideoParams?.duration) ||
|
Number(effectiveVideoParams?.duration) ||
|
||||||
undefined,
|
undefined,
|
||||||
|
resolution:
|
||||||
|
String(
|
||||||
|
(result.metadata as any)?.resolution ||
|
||||||
|
effectiveVideoParams?.resolution ||
|
||||||
|
""
|
||||||
|
).trim() || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (videoUrl) {
|
if (videoUrl) {
|
||||||
@ -10705,6 +10730,9 @@ export async function POST(request: Request) {
|
|||||||
messageId,
|
messageId,
|
||||||
modelRefs: billedModelRefs,
|
modelRefs: billedModelRefs,
|
||||||
duration: Number(effectiveVideoParams?.duration) || undefined,
|
duration: Number(effectiveVideoParams?.duration) || undefined,
|
||||||
|
resolution:
|
||||||
|
String(effectiveVideoParams?.resolution || "").trim() ||
|
||||||
|
undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -11698,6 +11726,7 @@ export async function POST(request: Request) {
|
|||||||
messageId: videoMessageId,
|
messageId: videoMessageId,
|
||||||
modelRefs: [finalVideoModelData.id, providerModelData.id, finalModelId],
|
modelRefs: [finalVideoModelData.id, providerModelData.id, finalModelId],
|
||||||
duration: Number(videoOptions.duration) || undefined,
|
duration: Number(videoOptions.duration) || undefined,
|
||||||
|
resolution: String(videoOptions.resolution || "").trim() || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
dataStream.write({
|
dataStream.write({
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
buildVideoDurationCostMap,
|
buildVideoDurationCostMap,
|
||||||
getConfiguredVideoDurationOptions,
|
getConfiguredVideoDurationOptions,
|
||||||
|
normalizeVideoResolutionCostMap,
|
||||||
} from "@/lib/ai/video-model-config";
|
} from "@/lib/ai/video-model-config";
|
||||||
|
|
||||||
// 🆕 图片生成模型特性配置 Schema
|
// 🆕 图片生成模型特性配置 Schema
|
||||||
@ -35,6 +36,7 @@ const imageGenConfigSchema = z.object({
|
|||||||
creditCostPerVideo: z.number().min(0).max(100000).optional(),
|
creditCostPerVideo: z.number().min(0).max(100000).optional(),
|
||||||
videoDurationOptions: z.array(z.number().min(1).max(600)).max(20).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(),
|
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(),
|
enableLoadingPlaceholder: z.boolean().optional(),
|
||||||
loadingColor: z.string().optional(),
|
loadingColor: z.string().optional(),
|
||||||
}).optional();
|
}).optional();
|
||||||
@ -53,6 +55,9 @@ function normalizeImageGenConfig(config: z.infer<typeof imageGenConfigSchema>) {
|
|||||||
...config,
|
...config,
|
||||||
videoDurationOptions: videoDurationOptions.length ? videoDurationOptions : undefined,
|
videoDurationOptions: videoDurationOptions.length ? videoDurationOptions : undefined,
|
||||||
creditCostPerVideoByDuration,
|
creditCostPerVideoByDuration,
|
||||||
|
creditCostPerVideoByResolution: normalizeVideoResolutionCostMap(
|
||||||
|
config.creditCostPerVideoByResolution
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
} from "@/lib/db/queries";
|
} from "@/lib/db/queries";
|
||||||
import { resolveThreeDModelRealId } from "@/lib/model3d/model-alias-config";
|
import { resolveThreeDModelRealId } from "@/lib/model3d/model-alias-config";
|
||||||
import { getThreeDModelAliasConfigForRuntime } from "@/lib/model3d/model-alias-config-server";
|
import { getThreeDModelAliasConfigForRuntime } from "@/lib/model3d/model-alias-config-server";
|
||||||
|
import { get3DWorldCreditCost } from "@/lib/points/config";
|
||||||
import { WorldLabsApiError, worldLabsRequest } from "@/lib/worldlabs/client";
|
import { WorldLabsApiError, worldLabsRequest } from "@/lib/worldlabs/client";
|
||||||
import {
|
import {
|
||||||
disableWorldLabsProvider,
|
disableWorldLabsProvider,
|
||||||
@ -155,12 +156,17 @@ export async function POST(request: Request) {
|
|||||||
return NextResponse.json({ success: false, error: "缺少有效的 mode" }, { status: 400 });
|
return NextResponse.json({ success: false, error: "缺少有效的 mode" }, { status: 400 });
|
||||||
}
|
}
|
||||||
const modelAliasConfig = await getThreeDModelAliasConfigForRuntime();
|
const modelAliasConfig = await getThreeDModelAliasConfigForRuntime();
|
||||||
|
const resolvedWorldModel = resolveThreeDModelRealId(
|
||||||
|
readOptionalTrimmedText(body.model),
|
||||||
|
modelAliasConfig.worldModels,
|
||||||
|
);
|
||||||
|
|
||||||
const pointsConfig = await getPointsSystemConfig();
|
const pointsConfig = await getPointsSystemConfig();
|
||||||
const shouldChargePoints = shouldCharge3DPoints(mode);
|
const shouldChargePoints = shouldCharge3DPoints(mode);
|
||||||
const unitCost = Number.isFinite(Number(pointsConfig.creditCostPer3DWorld))
|
const unitCost = get3DWorldCreditCost(
|
||||||
? Math.max(0, Math.round(Number(pointsConfig.creditCostPer3DWorld)))
|
pointsConfig,
|
||||||
: 0;
|
resolvedWorldModel || "marble-1.1",
|
||||||
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
shouldChargePoints &&
|
shouldChargePoints &&
|
||||||
@ -193,10 +199,6 @@ export async function POST(request: Request) {
|
|||||||
readOptionalTrimmedText(body.displayName) ||
|
readOptionalTrimmedText(body.displayName) ||
|
||||||
readOptionalTrimmedText(body.prompt) ||
|
readOptionalTrimmedText(body.prompt) ||
|
||||||
`World ${new Date().toLocaleString("zh-CN")}`;
|
`World ${new Date().toLocaleString("zh-CN")}`;
|
||||||
const resolvedWorldModel = resolveThreeDModelRealId(
|
|
||||||
readOptionalTrimmedText(body.model),
|
|
||||||
modelAliasConfig.worldModels,
|
|
||||||
);
|
|
||||||
const worldPrompt = buildWorldPrompt(body, mode);
|
const worldPrompt = buildWorldPrompt(body, mode);
|
||||||
|
|
||||||
const candidates = preferredProviderId
|
const candidates = preferredProviderId
|
||||||
@ -266,8 +268,8 @@ export async function POST(request: Request) {
|
|||||||
userId: session.user.id,
|
userId: session.user.id,
|
||||||
amount: unitCost,
|
amount: unitCost,
|
||||||
reason: "world3d_generation",
|
reason: "world3d_generation",
|
||||||
modelId: "worldlabs-3d",
|
modelId: `worldlabs-3d:${resolvedWorldModel || "marble-1.1"}`,
|
||||||
note: "3D世界生成消耗积分",
|
note: `3D世界生成消耗积分(${resolvedWorldModel || "marble-1.1"})`,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[Points] Failed to consume World Labs 3D points:", error);
|
console.error("[Points] Failed to consume World Labs 3D points:", error);
|
||||||
|
|||||||
@ -313,7 +313,12 @@ async function createEnhanceProJob(params: {
|
|||||||
crop_to_fill: normalizeBoolean(params.requestBody.crop_to_fill, false),
|
crop_to_fill: normalizeBoolean(params.requestBody.crop_to_fill, false),
|
||||||
model: params.requestBody.model || params.config.defaultModel,
|
model: params.requestBody.model || params.config.defaultModel,
|
||||||
upscaling_activated: normalizeBoolean(params.requestBody.upscaling_activated, false),
|
upscaling_activated: normalizeBoolean(params.requestBody.upscaling_activated, false),
|
||||||
image_scaling_factor: clampNumber(params.requestBody.image_scaling_factor, 1, 32, 1),
|
image_scaling_factor: clampNumber(
|
||||||
|
params.requestBody.image_scaling_factor,
|
||||||
|
1,
|
||||||
|
params.config.maxUpscaleFactor,
|
||||||
|
1,
|
||||||
|
),
|
||||||
creativity: clampInteger(params.requestBody.creativity, 1, 6, 3),
|
creativity: clampInteger(params.requestBody.creativity, 1, 6, 3),
|
||||||
texture: clampInteger(params.requestBody.texture, 1, 5, 3),
|
texture: clampInteger(params.requestBody.texture, 1, 5, 3),
|
||||||
sharpen: clampNumber(params.requestBody.sharpen, 0, 1, 0.5),
|
sharpen: clampNumber(params.requestBody.sharpen, 0, 1, 0.5),
|
||||||
@ -647,12 +652,21 @@ export async function POST(request: NextRequest) {
|
|||||||
body.model === "Redefine"
|
body.model === "Redefine"
|
||||||
? body.model
|
? body.model
|
||||||
: config.defaultModel;
|
: config.defaultModel;
|
||||||
|
const upscalingActivated = body.upscaling_activated === true;
|
||||||
|
const imageScalingFactor = clampNumber(
|
||||||
|
body.image_scaling_factor,
|
||||||
|
1,
|
||||||
|
config.maxUpscaleFactor,
|
||||||
|
1,
|
||||||
|
);
|
||||||
const pointsConfig = await getPointsSystemConfig();
|
const pointsConfig = await getPointsSystemConfig();
|
||||||
const pointsEstimate = calculateEnhanceProPointsEstimate(pointsConfig, {
|
const pointsEstimate = calculateEnhanceProPointsEstimate(pointsConfig, {
|
||||||
width: clampedDimensions.width,
|
width: clampedDimensions.width,
|
||||||
height: clampedDimensions.height,
|
height: clampedDimensions.height,
|
||||||
batchSize,
|
batchSize,
|
||||||
model: selectedModel,
|
model: selectedModel,
|
||||||
|
upscalingActivated,
|
||||||
|
scaleFactor: imageScalingFactor,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (pointsEstimate.chargeable && pointsConfig.blockOnInsufficient !== false) {
|
if (pointsEstimate.chargeable && pointsConfig.blockOnInsufficient !== false) {
|
||||||
@ -683,6 +697,8 @@ export async function POST(request: NextRequest) {
|
|||||||
...body,
|
...body,
|
||||||
batchSize,
|
batchSize,
|
||||||
model: selectedModel,
|
model: selectedModel,
|
||||||
|
upscaling_activated: upscalingActivated,
|
||||||
|
image_scaling_factor: imageScalingFactor,
|
||||||
},
|
},
|
||||||
sourceImageUrl: sourceInput.sourceImageUrl,
|
sourceImageUrl: sourceInput.sourceImageUrl,
|
||||||
width: clampedDimensions.width,
|
width: clampedDimensions.width,
|
||||||
|
|||||||
@ -9764,14 +9764,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
? VIDEO_CANVAS_TOOLBAR_BUTTON_CLASS
|
? VIDEO_CANVAS_TOOLBAR_BUTTON_CLASS
|
||||||
: "h-9 w-9 rounded-xl"
|
: "h-9 w-9 rounded-xl"
|
||||||
)}
|
)}
|
||||||
disabled={!selectedImageNode}
|
onClick={() => router.push(`/canvas/${id}/realtime?mode=enhance-pro`)}
|
||||||
onClick={() => {
|
|
||||||
if (!selectedImageNode) {
|
|
||||||
toast.info("请先选中一张图片");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
router.push(`/canvas/${id}/realtime?mode=enhance-pro`);
|
|
||||||
}}
|
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
@ -9781,7 +9774,7 @@ export default function CanvasEditorPage({ params }: PageProps) {
|
|||||||
<TooltipContent side="top">
|
<TooltipContent side="top">
|
||||||
{selectedImageNode
|
{selectedImageNode
|
||||||
? "图像增强 Pro"
|
? "图像增强 Pro"
|
||||||
: "先选中图片后进入图像增强 Pro"}
|
: "进入图像增强 Pro 后上传图片"}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@ -40,6 +40,7 @@ export default function CanvasRealtimePage() {
|
|||||||
const handleApplyResult = useCallback(
|
const handleApplyResult = useCallback(
|
||||||
async ({
|
async ({
|
||||||
imageUrl,
|
imageUrl,
|
||||||
|
sourceImageUrl,
|
||||||
prompt,
|
prompt,
|
||||||
title,
|
title,
|
||||||
model,
|
model,
|
||||||
@ -47,6 +48,7 @@ export default function CanvasRealtimePage() {
|
|||||||
height,
|
height,
|
||||||
}: {
|
}: {
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
|
sourceImageUrl?: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
@ -74,6 +76,7 @@ export default function CanvasRealtimePage() {
|
|||||||
const targetHeight = hasNaturalSize
|
const targetHeight = hasNaturalSize
|
||||||
? Math.max(140, Math.round(safeHeight * scale))
|
? Math.max(140, Math.round(safeHeight * scale))
|
||||||
: fallbackSize;
|
: fallbackSize;
|
||||||
|
const compareSourceUrl = String(sourceImageUrl || "").trim();
|
||||||
|
|
||||||
const x =
|
const x =
|
||||||
(stageWidth * 0.45 - viewportX) / viewportZoom - targetWidth / 2;
|
(stageWidth * 0.45 - viewportX) / viewportZoom - targetWidth / 2;
|
||||||
@ -86,10 +89,16 @@ export default function CanvasRealtimePage() {
|
|||||||
size: { width: targetWidth, height: targetHeight },
|
size: { width: targetWidth, height: targetHeight },
|
||||||
data: {
|
data: {
|
||||||
src: imageUrl,
|
src: imageUrl,
|
||||||
|
originalSrc: compareSourceUrl || undefined,
|
||||||
status: "completed",
|
status: "completed",
|
||||||
title: title || "实时画布结果",
|
title: title || "实时画布结果",
|
||||||
prompt,
|
prompt,
|
||||||
model: model || "bfl/flux-2",
|
model: model || "bfl/flux-2",
|
||||||
|
metadata: compareSourceUrl
|
||||||
|
? {
|
||||||
|
compareSourceUrl,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
},
|
},
|
||||||
connections: [],
|
connections: [],
|
||||||
});
|
});
|
||||||
|
|||||||
@ -68,6 +68,7 @@ type EnhanceProClientConfig = {
|
|||||||
defaultOutputFormat: EnhanceProOutputFormat;
|
defaultOutputFormat: EnhanceProOutputFormat;
|
||||||
allowBatchSize: boolean;
|
allowBatchSize: boolean;
|
||||||
maxBatchSize: number;
|
maxBatchSize: number;
|
||||||
|
maxUpscaleFactor: number;
|
||||||
maxOutputEdge: number;
|
maxOutputEdge: number;
|
||||||
enableAdvancedControls: boolean;
|
enableAdvancedControls: boolean;
|
||||||
presets: EnhanceProPreset[];
|
presets: EnhanceProPreset[];
|
||||||
@ -109,6 +110,7 @@ const FALLBACK_CONFIG: EnhanceProClientConfig = {
|
|||||||
defaultOutputFormat: "jpg",
|
defaultOutputFormat: "jpg",
|
||||||
allowBatchSize: true,
|
allowBatchSize: true,
|
||||||
maxBatchSize: 4,
|
maxBatchSize: 4,
|
||||||
|
maxUpscaleFactor: 4,
|
||||||
maxOutputEdge: 4096,
|
maxOutputEdge: 4096,
|
||||||
enableAdvancedControls: true,
|
enableAdvancedControls: true,
|
||||||
presets: defaultEnhanceProPresets(),
|
presets: defaultEnhanceProPresets(),
|
||||||
@ -244,7 +246,11 @@ function applyPresetToForm(
|
|||||||
crop_to_fill: preset.cropToFill,
|
crop_to_fill: preset.cropToFill,
|
||||||
model: preset.model,
|
model: preset.model,
|
||||||
upscaling_activated: preset.upscalingActivated,
|
upscaling_activated: preset.upscalingActivated,
|
||||||
image_scaling_factor: clampNumber(preset.imageScalingFactor, 1, 32),
|
image_scaling_factor: clampNumber(
|
||||||
|
preset.imageScalingFactor,
|
||||||
|
1,
|
||||||
|
config.maxUpscaleFactor
|
||||||
|
),
|
||||||
creativity: clampNumber(Math.round(preset.creativity), 1, 6),
|
creativity: clampNumber(Math.round(preset.creativity), 1, 6),
|
||||||
texture: clampNumber(Math.round(preset.texture), 1, 5),
|
texture: clampNumber(Math.round(preset.texture), 1, 5),
|
||||||
sharpen: clampNumber(preset.sharpen, 0, 1),
|
sharpen: clampNumber(preset.sharpen, 0, 1),
|
||||||
@ -362,6 +368,7 @@ interface EnhanceProWorkspaceProps {
|
|||||||
onRequestAddImage?: () => void;
|
onRequestAddImage?: () => void;
|
||||||
onApplyResult: (payload: {
|
onApplyResult: (payload: {
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
|
sourceImageUrl?: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
@ -450,9 +457,19 @@ export function EnhanceProWorkspace({
|
|||||||
height: formState.height,
|
height: formState.height,
|
||||||
batchSize: formState.batchSize,
|
batchSize: formState.batchSize,
|
||||||
model: formState.model,
|
model: formState.model,
|
||||||
|
upscalingActivated: formState.upscaling_activated,
|
||||||
|
scaleFactor: formState.image_scaling_factor,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
[formState.batchSize, formState.height, formState.model, formState.width, pointsConfig]
|
[
|
||||||
|
formState.batchSize,
|
||||||
|
formState.height,
|
||||||
|
formState.image_scaling_factor,
|
||||||
|
formState.model,
|
||||||
|
formState.upscaling_activated,
|
||||||
|
formState.width,
|
||||||
|
pointsConfig,
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -578,7 +595,7 @@ export function EnhanceProWorkspace({
|
|||||||
image_scaling_factor: clampNumber(
|
image_scaling_factor: clampNumber(
|
||||||
formState.image_scaling_factor,
|
formState.image_scaling_factor,
|
||||||
1,
|
1,
|
||||||
32
|
config.maxUpscaleFactor
|
||||||
),
|
),
|
||||||
creativity: clampNumber(Math.round(formState.creativity), 1, 6),
|
creativity: clampNumber(Math.round(formState.creativity), 1, 6),
|
||||||
texture: clampNumber(Math.round(formState.texture), 1, 5),
|
texture: clampNumber(Math.round(formState.texture), 1, 5),
|
||||||
@ -613,6 +630,7 @@ export function EnhanceProWorkspace({
|
|||||||
}
|
}
|
||||||
onApplyResult({
|
onApplyResult({
|
||||||
imageUrl: selectedGeneratedImage.originalUrl,
|
imageUrl: selectedGeneratedImage.originalUrl,
|
||||||
|
sourceImageUrl: sourceImageUrl || undefined,
|
||||||
prompt: formState.prompt.trim() || "图像增强 Pro",
|
prompt: formState.prompt.trim() || "图像增强 Pro",
|
||||||
title: "图像增强 Pro 结果",
|
title: "图像增强 Pro 结果",
|
||||||
model: selectedPreset?.model || config.defaultModel,
|
model: selectedPreset?.model || config.defaultModel,
|
||||||
@ -739,14 +757,14 @@ export function EnhanceProWorkspace({
|
|||||||
<div className="relative flex flex-col items-center justify-center gap-3 px-6 text-center">
|
<div className="relative flex flex-col items-center justify-center gap-3 px-6 text-center">
|
||||||
<Button
|
<Button
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-14 rounded-[22px] px-8 text-[15px] font-semibold tracking-[-0.02em]",
|
"h-16 rounded-[22px] px-10 text-[15px] font-semibold tracking-[-0.02em] sm:h-20 sm:px-12 sm:text-lg",
|
||||||
PRIMARY_ACTION_BUTTON_CLASS
|
PRIMARY_ACTION_BUTTON_CLASS
|
||||||
)}
|
)}
|
||||||
disabled={!onRequestAddImage}
|
disabled={!onRequestAddImage}
|
||||||
onClick={onRequestAddImage}
|
onClick={onRequestAddImage}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<ImageIcon className="mr-2 h-5 w-5" />
|
<ImageIcon className="mr-2 h-5 w-5 sm:h-6 sm:w-6" />
|
||||||
添加图片
|
添加图片
|
||||||
</Button>
|
</Button>
|
||||||
<div className="text-[#666666] text-[13px] leading-6">
|
<div className="text-[#666666] text-[13px] leading-6">
|
||||||
@ -804,6 +822,9 @@ export function EnhanceProWorkspace({
|
|||||||
{pointsEstimate.chargeMode === "resolution_bucket" && pointsEstimate.bucketName
|
{pointsEstimate.chargeMode === "resolution_bucket" && pointsEstimate.bucketName
|
||||||
? ` · ${pointsEstimate.bucketName}`
|
? ` · ${pointsEstimate.bucketName}`
|
||||||
: ""}
|
: ""}
|
||||||
|
{pointsEstimate.chargeMode === "scale_factor"
|
||||||
|
? ` · ${pointsEstimate.scaleFactor}x`
|
||||||
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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">
|
<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">
|
||||||
@ -1130,7 +1151,7 @@ export function EnhanceProWorkspace({
|
|||||||
<Input
|
<Input
|
||||||
className={COMPACT_FIELD_CLASS}
|
className={COMPACT_FIELD_CLASS}
|
||||||
id="enhance-workspace-scale"
|
id="enhance-workspace-scale"
|
||||||
max="32"
|
max={config.maxUpscaleFactor}
|
||||||
min="1"
|
min="1"
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
handleNumericField(
|
handleNumericField(
|
||||||
@ -1138,7 +1159,7 @@ export function EnhanceProWorkspace({
|
|||||||
clampNumber(
|
clampNumber(
|
||||||
Number.parseFloat(event.target.value) || 1,
|
Number.parseFloat(event.target.value) || 1,
|
||||||
1,
|
1,
|
||||||
32
|
config.maxUpscaleFactor
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -148,6 +148,7 @@ type RealtimeCanvasDialogProps = {
|
|||||||
initialMode?: RealtimeWorkspaceMode;
|
initialMode?: RealtimeWorkspaceMode;
|
||||||
onApplyResult: (payload: {
|
onApplyResult: (payload: {
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
|
sourceImageUrl?: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
@ -2956,6 +2957,7 @@ export function RealtimeCanvasDialog({
|
|||||||
const handleApplyEnhanceResult = useCallback(
|
const handleApplyEnhanceResult = useCallback(
|
||||||
(payload: {
|
(payload: {
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
|
sourceImageUrl?: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
|||||||
@ -38,6 +38,7 @@ type EnhanceProClientConfig = {
|
|||||||
defaultOutputFormat: EnhanceProOutputFormat;
|
defaultOutputFormat: EnhanceProOutputFormat;
|
||||||
allowBatchSize: boolean;
|
allowBatchSize: boolean;
|
||||||
maxBatchSize: number;
|
maxBatchSize: number;
|
||||||
|
maxUpscaleFactor: number;
|
||||||
maxOutputEdge: number;
|
maxOutputEdge: number;
|
||||||
enableAdvancedControls: boolean;
|
enableAdvancedControls: boolean;
|
||||||
presets: EnhanceProPreset[];
|
presets: EnhanceProPreset[];
|
||||||
@ -73,6 +74,7 @@ const FALLBACK_CONFIG: EnhanceProClientConfig = {
|
|||||||
defaultOutputFormat: "jpg",
|
defaultOutputFormat: "jpg",
|
||||||
allowBatchSize: true,
|
allowBatchSize: true,
|
||||||
maxBatchSize: 4,
|
maxBatchSize: 4,
|
||||||
|
maxUpscaleFactor: 4,
|
||||||
maxOutputEdge: 4096,
|
maxOutputEdge: 4096,
|
||||||
enableAdvancedControls: true,
|
enableAdvancedControls: true,
|
||||||
presets: defaultEnhanceProPresets(),
|
presets: defaultEnhanceProPresets(),
|
||||||
@ -151,7 +153,11 @@ function applyPresetToForm(
|
|||||||
crop_to_fill: preset.cropToFill,
|
crop_to_fill: preset.cropToFill,
|
||||||
model: preset.model,
|
model: preset.model,
|
||||||
upscaling_activated: preset.upscalingActivated,
|
upscaling_activated: preset.upscalingActivated,
|
||||||
image_scaling_factor: clampNumber(preset.imageScalingFactor, 1, 32),
|
image_scaling_factor: clampNumber(
|
||||||
|
preset.imageScalingFactor,
|
||||||
|
1,
|
||||||
|
config.maxUpscaleFactor
|
||||||
|
),
|
||||||
creativity: clampNumber(Math.round(preset.creativity), 1, 6),
|
creativity: clampNumber(Math.round(preset.creativity), 1, 6),
|
||||||
texture: clampNumber(Math.round(preset.texture), 1, 5),
|
texture: clampNumber(Math.round(preset.texture), 1, 5),
|
||||||
sharpen: clampNumber(preset.sharpen, 0, 1),
|
sharpen: clampNumber(preset.sharpen, 0, 1),
|
||||||
@ -274,7 +280,11 @@ export function EnhanceProDialog({
|
|||||||
crop_to_fill: formState.crop_to_fill,
|
crop_to_fill: formState.crop_to_fill,
|
||||||
model: formState.model,
|
model: formState.model,
|
||||||
upscaling_activated: formState.upscaling_activated,
|
upscaling_activated: formState.upscaling_activated,
|
||||||
image_scaling_factor: clampNumber(formState.image_scaling_factor, 1, 32),
|
image_scaling_factor: clampNumber(
|
||||||
|
formState.image_scaling_factor,
|
||||||
|
1,
|
||||||
|
config.maxUpscaleFactor
|
||||||
|
),
|
||||||
creativity: clampNumber(Math.round(formState.creativity), 1, 6),
|
creativity: clampNumber(Math.round(formState.creativity), 1, 6),
|
||||||
texture: clampNumber(Math.round(formState.texture), 1, 5),
|
texture: clampNumber(Math.round(formState.texture), 1, 5),
|
||||||
sharpen: clampNumber(formState.sharpen, 0, 1),
|
sharpen: clampNumber(formState.sharpen, 0, 1),
|
||||||
@ -549,13 +559,17 @@ export function EnhanceProDialog({
|
|||||||
id="enhance-pro-image-scaling"
|
id="enhance-pro-image-scaling"
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="32"
|
max={config.maxUpscaleFactor}
|
||||||
step="0.5"
|
step="0.5"
|
||||||
value={formState.image_scaling_factor}
|
value={formState.image_scaling_factor}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
handleNumericField(
|
handleNumericField(
|
||||||
"image_scaling_factor",
|
"image_scaling_factor",
|
||||||
clampNumber(Number.parseFloat(event.target.value) || 1, 1, 32),
|
clampNumber(
|
||||||
|
Number.parseFloat(event.target.value) || 1,
|
||||||
|
1,
|
||||||
|
config.maxUpscaleFactor
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -207,6 +207,7 @@ function videoPreset(
|
|||||||
creditCostPerVideo: 0,
|
creditCostPerVideo: 0,
|
||||||
videoDurationOptions: durationOptions,
|
videoDurationOptions: durationOptions,
|
||||||
creditCostPerVideoByDuration: {},
|
creditCostPerVideoByDuration: {},
|
||||||
|
creditCostPerVideoByResolution: {},
|
||||||
enableLoadingPlaceholder: true,
|
enableLoadingPlaceholder: true,
|
||||||
loadingColor: "blue",
|
loadingColor: "blue",
|
||||||
},
|
},
|
||||||
|
|||||||
@ -508,6 +508,79 @@ 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) {
|
async function inferReferenceAspectRatioFromImageUrl(url?: string | null) {
|
||||||
const normalized = normalizeText(url);
|
const normalized = normalizeText(url);
|
||||||
if (!normalized || !/^https?:\/\//i.test(normalized)) return undefined;
|
if (!normalized || !/^https?:\/\//i.test(normalized)) return undefined;
|
||||||
@ -637,7 +710,7 @@ async function requestKreaJob(
|
|||||||
endpoint,
|
endpoint,
|
||||||
status: response.status,
|
status: response.status,
|
||||||
requestFields: Object.keys(body),
|
requestFields: Object.keys(body),
|
||||||
response: payload,
|
response: stringifyForLog(payload || rawText),
|
||||||
});
|
});
|
||||||
const message =
|
const message =
|
||||||
typeof payload?.error === "string"
|
typeof payload?.error === "string"
|
||||||
@ -915,6 +988,11 @@ export function createKrea({ apiKey, baseURL }: KreaConfig) {
|
|||||||
const normalizedVideoUrls = (
|
const normalizedVideoUrls = (
|
||||||
await Promise.all(videoUrls.map((url) => normalizeAssetUrl(url)))
|
await Promise.all(videoUrls.map((url) => normalizeAssetUrl(url)))
|
||||||
).filter(Boolean);
|
).filter(Boolean);
|
||||||
|
const kreaImageUrls = (
|
||||||
|
await Promise.all(
|
||||||
|
normalizedImageUrls.map((url) => uploadKreaReferenceAsset(baseURL, apiKey, url))
|
||||||
|
)
|
||||||
|
).filter(Boolean);
|
||||||
const requestFields =
|
const requestFields =
|
||||||
preset?.requestFields ||
|
preset?.requestFields ||
|
||||||
(kind === "video"
|
(kind === "video"
|
||||||
@ -933,7 +1011,7 @@ export function createKrea({ apiKey, baseURL }: KreaConfig) {
|
|||||||
endpoint,
|
endpoint,
|
||||||
kind,
|
kind,
|
||||||
prompt,
|
prompt,
|
||||||
imageUrls: normalizedImageUrls,
|
imageUrls: kreaImageUrls,
|
||||||
requestFields,
|
requestFields,
|
||||||
});
|
});
|
||||||
const { body, imageSize } = buildKreaBody({
|
const { body, imageSize } = buildKreaBody({
|
||||||
@ -942,7 +1020,7 @@ export function createKrea({ apiKey, baseURL }: KreaConfig) {
|
|||||||
requestFields,
|
requestFields,
|
||||||
requiredFields,
|
requiredFields,
|
||||||
prompt: promptForKrea,
|
prompt: promptForKrea,
|
||||||
imageUrls: normalizedImageUrls,
|
imageUrls: kreaImageUrls,
|
||||||
videoUrls: normalizedVideoUrls,
|
videoUrls: normalizedVideoUrls,
|
||||||
referenceAspectRatio,
|
referenceAspectRatio,
|
||||||
imageParams: options.imageParams,
|
imageParams: options.imageParams,
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
export type VideoDurationCostMap = Record<string, number>;
|
export type VideoDurationCostMap = Record<string, number>;
|
||||||
|
export type VideoResolutionCostMap = Record<string, number>;
|
||||||
|
|
||||||
export type VideoImageGenConfigLike = {
|
export type VideoImageGenConfigLike = {
|
||||||
creditCostPerVideo?: number | null;
|
creditCostPerVideo?: number | null;
|
||||||
videoDurationOptions?: number[] | null;
|
videoDurationOptions?: number[] | null;
|
||||||
creditCostPerVideoByDuration?: Record<string, number> | null;
|
creditCostPerVideoByDuration?: Record<string, number> | null;
|
||||||
|
creditCostPerVideoByResolution?: Record<string, number> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizePositiveInteger(value: unknown) {
|
function normalizePositiveInteger(value: unknown) {
|
||||||
@ -54,6 +56,42 @@ export function normalizeVideoDurationCostMap(
|
|||||||
return Object.fromEntries(entries);
|
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(
|
export function getConfiguredVideoDurationOptions(
|
||||||
config: VideoImageGenConfigLike | null | undefined
|
config: VideoImageGenConfigLike | null | undefined
|
||||||
): number[] {
|
): number[] {
|
||||||
@ -85,9 +123,20 @@ export function buildVideoDurationCostMap(params: {
|
|||||||
|
|
||||||
export function getVideoCreditCostFromConfig(
|
export function getVideoCreditCostFromConfig(
|
||||||
config: VideoImageGenConfigLike | null | undefined,
|
config: VideoImageGenConfigLike | null | undefined,
|
||||||
duration?: number | null
|
duration?: number | null,
|
||||||
|
resolution?: string | null
|
||||||
): number {
|
): number {
|
||||||
const normalizedDuration = normalizePositiveInteger(duration);
|
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 durationOptions = getConfiguredVideoDurationOptions(config);
|
||||||
const durationCostMap = normalizeVideoDurationCostMap(
|
const durationCostMap = normalizeVideoDurationCostMap(
|
||||||
config?.creditCostPerVideoByDuration,
|
config?.creditCostPerVideoByDuration,
|
||||||
|
|||||||
@ -3925,6 +3925,7 @@ interface ImageGenConfig {
|
|||||||
creditCostPerVideo?: number;
|
creditCostPerVideo?: number;
|
||||||
videoDurationOptions?: number[];
|
videoDurationOptions?: number[];
|
||||||
creditCostPerVideoByDuration?: Record<string, number>;
|
creditCostPerVideoByDuration?: Record<string, number>;
|
||||||
|
creditCostPerVideoByResolution?: Record<string, number>;
|
||||||
enableLoadingPlaceholder?: boolean;
|
enableLoadingPlaceholder?: boolean;
|
||||||
loadingColor?: string;
|
loadingColor?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,6 +49,7 @@ export type EnhanceProKreaConfig = {
|
|||||||
defaultOutputFormat: EnhanceProOutputFormat;
|
defaultOutputFormat: EnhanceProOutputFormat;
|
||||||
allowBatchSize: boolean;
|
allowBatchSize: boolean;
|
||||||
maxBatchSize: number;
|
maxBatchSize: number;
|
||||||
|
maxUpscaleFactor: number;
|
||||||
maxOutputEdge: number;
|
maxOutputEdge: number;
|
||||||
enableAdvancedControls: boolean;
|
enableAdvancedControls: boolean;
|
||||||
presets: EnhanceProPreset[];
|
presets: EnhanceProPreset[];
|
||||||
@ -195,7 +196,10 @@ function normalizeEnhanceProPreset(
|
|||||||
index: number,
|
index: number,
|
||||||
configDefaults: Pick<
|
configDefaults: Pick<
|
||||||
EnhanceProKreaConfig,
|
EnhanceProKreaConfig,
|
||||||
"defaultModel" | "defaultOutputFormat" | "maxBatchSize"
|
| "defaultModel"
|
||||||
|
| "defaultOutputFormat"
|
||||||
|
| "maxBatchSize"
|
||||||
|
| "maxUpscaleFactor"
|
||||||
>,
|
>,
|
||||||
): EnhanceProPreset {
|
): EnhanceProPreset {
|
||||||
const defaults = defaultEnhanceProPresets()[index] || defaultEnhanceProPresets()[0];
|
const defaults = defaultEnhanceProPresets()[index] || defaultEnhanceProPresets()[0];
|
||||||
@ -271,7 +275,7 @@ function normalizeEnhanceProPreset(
|
|||||||
imageScalingFactor: clampFloat(
|
imageScalingFactor: clampFloat(
|
||||||
raw.imageScalingFactor,
|
raw.imageScalingFactor,
|
||||||
1,
|
1,
|
||||||
32,
|
configDefaults.maxUpscaleFactor,
|
||||||
defaults.imageScalingFactor,
|
defaults.imageScalingFactor,
|
||||||
),
|
),
|
||||||
creativity: clampInteger(raw.creativity, 1, 6, defaults.creativity),
|
creativity: clampInteger(raw.creativity, 1, 6, defaults.creativity),
|
||||||
@ -293,6 +297,7 @@ export function defaultEnhanceProKreaConfig(): EnhanceProKreaConfig {
|
|||||||
defaultOutputFormat: ENHANCE_PRO_KREA_DEFAULT_OUTPUT_FORMAT,
|
defaultOutputFormat: ENHANCE_PRO_KREA_DEFAULT_OUTPUT_FORMAT,
|
||||||
allowBatchSize: true,
|
allowBatchSize: true,
|
||||||
maxBatchSize: 4,
|
maxBatchSize: 4,
|
||||||
|
maxUpscaleFactor: 4,
|
||||||
maxOutputEdge: 4096,
|
maxOutputEdge: 4096,
|
||||||
enableAdvancedControls: true,
|
enableAdvancedControls: true,
|
||||||
presets: defaultEnhanceProPresets(),
|
presets: defaultEnhanceProPresets(),
|
||||||
@ -311,15 +316,22 @@ export function normalizeEnhanceProKreaConfig(
|
|||||||
|
|
||||||
const defaultModel = String(raw.defaultModel || "").trim();
|
const defaultModel = String(raw.defaultModel || "").trim();
|
||||||
const defaultOutputFormat = String(raw.defaultOutputFormat || "").trim();
|
const defaultOutputFormat = String(raw.defaultOutputFormat || "").trim();
|
||||||
const normalizedPresets = Array.isArray(raw.presets)
|
const maxUpscaleFactor = clampFloat(
|
||||||
? raw.presets.map((item, index) =>
|
raw.maxUpscaleFactor,
|
||||||
normalizeEnhanceProPreset(item, index, {
|
1,
|
||||||
defaultModel: defaults.defaultModel,
|
32,
|
||||||
defaultOutputFormat: defaults.defaultOutputFormat,
|
defaults.maxUpscaleFactor,
|
||||||
maxBatchSize: clampInteger(raw.maxBatchSize, 1, 4, defaults.maxBatchSize),
|
);
|
||||||
}),
|
const normalizedPresets = (
|
||||||
)
|
Array.isArray(raw.presets) ? raw.presets : defaultEnhanceProPresets()
|
||||||
: defaultEnhanceProPresets();
|
).map((item, index) =>
|
||||||
|
normalizeEnhanceProPreset(item, index, {
|
||||||
|
defaultModel: defaults.defaultModel,
|
||||||
|
defaultOutputFormat: defaults.defaultOutputFormat,
|
||||||
|
maxBatchSize: clampInteger(raw.maxBatchSize, 1, 4, defaults.maxBatchSize),
|
||||||
|
maxUpscaleFactor,
|
||||||
|
}),
|
||||||
|
);
|
||||||
const presetWithDefault = normalizedPresets.some((preset) => preset.isDefault)
|
const presetWithDefault = normalizedPresets.some((preset) => preset.isDefault)
|
||||||
? normalizedPresets
|
? normalizedPresets
|
||||||
: normalizedPresets.map((preset, index) => ({
|
: normalizedPresets.map((preset, index) => ({
|
||||||
@ -342,7 +354,8 @@ export function normalizeEnhanceProKreaConfig(
|
|||||||
defaultModel:
|
defaultModel:
|
||||||
defaultModel === "Recovery" ||
|
defaultModel === "Recovery" ||
|
||||||
defaultModel === "Recovery V2" ||
|
defaultModel === "Recovery V2" ||
|
||||||
defaultModel === "Reimagine"
|
defaultModel === "Reimagine" ||
|
||||||
|
defaultModel === "Redefine"
|
||||||
? defaultModel
|
? defaultModel
|
||||||
: defaults.defaultModel,
|
: defaults.defaultModel,
|
||||||
defaultOutputFormat:
|
defaultOutputFormat:
|
||||||
@ -353,6 +366,7 @@ export function normalizeEnhanceProKreaConfig(
|
|||||||
: defaults.defaultOutputFormat,
|
: defaults.defaultOutputFormat,
|
||||||
allowBatchSize: raw.allowBatchSize !== false,
|
allowBatchSize: raw.allowBatchSize !== false,
|
||||||
maxBatchSize: clampInteger(raw.maxBatchSize, 1, 4, defaults.maxBatchSize),
|
maxBatchSize: clampInteger(raw.maxBatchSize, 1, 4, defaults.maxBatchSize),
|
||||||
|
maxUpscaleFactor,
|
||||||
maxOutputEdge: clampInteger(
|
maxOutputEdge: clampInteger(
|
||||||
raw.maxOutputEdge,
|
raw.maxOutputEdge,
|
||||||
512,
|
512,
|
||||||
@ -376,6 +390,7 @@ export function sanitizeEnhanceProKreaConfigForClient(
|
|||||||
defaultOutputFormat: config.defaultOutputFormat,
|
defaultOutputFormat: config.defaultOutputFormat,
|
||||||
allowBatchSize: config.allowBatchSize,
|
allowBatchSize: config.allowBatchSize,
|
||||||
maxBatchSize: config.maxBatchSize,
|
maxBatchSize: config.maxBatchSize,
|
||||||
|
maxUpscaleFactor: config.maxUpscaleFactor,
|
||||||
maxOutputEdge: config.maxOutputEdge,
|
maxOutputEdge: config.maxOutputEdge,
|
||||||
enableAdvancedControls: config.enableAdvancedControls,
|
enableAdvancedControls: config.enableAdvancedControls,
|
||||||
presets: config.presets,
|
presets: config.presets,
|
||||||
|
|||||||
@ -3,7 +3,10 @@ import type { EnhanceProKreaModel } from "@/lib/enhance-pro/config";
|
|||||||
export const POINTS_SYSTEM_CONFIG_KEY = "points_system_config";
|
export const POINTS_SYSTEM_CONFIG_KEY = "points_system_config";
|
||||||
export const POINTS_PACKAGES_KEY = "points_packages";
|
export const POINTS_PACKAGES_KEY = "points_packages";
|
||||||
|
|
||||||
export type EnhanceProPointsChargeMode = "fixed" | "resolution_bucket";
|
export type EnhanceProPointsChargeMode =
|
||||||
|
| "fixed"
|
||||||
|
| "resolution_bucket"
|
||||||
|
| "scale_factor";
|
||||||
|
|
||||||
export type EnhanceProPointsResolutionBucket = {
|
export type EnhanceProPointsResolutionBucket = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -16,7 +19,9 @@ export type EnhanceProPointsPricingConfig = {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
chargeMode: EnhanceProPointsChargeMode;
|
chargeMode: EnhanceProPointsChargeMode;
|
||||||
fixedCost: number;
|
fixedCost: number;
|
||||||
|
chargeOnlyWhenUpscaling: boolean;
|
||||||
batchSizeMultiplierEnabled: boolean;
|
batchSizeMultiplierEnabled: boolean;
|
||||||
|
scaleFactorCosts: Record<string, number>;
|
||||||
resolutionBuckets: EnhanceProPointsResolutionBucket[];
|
resolutionBuckets: EnhanceProPointsResolutionBucket[];
|
||||||
modelMultipliers: Record<EnhanceProKreaModel, number>;
|
modelMultipliers: Record<EnhanceProKreaModel, number>;
|
||||||
};
|
};
|
||||||
@ -26,6 +31,8 @@ export type PointsSystemConfig = {
|
|||||||
blockOnInsufficient: boolean;
|
blockOnInsufficient: boolean;
|
||||||
creditCostPer3DModel: number;
|
creditCostPer3DModel: number;
|
||||||
creditCostPer3DWorld: number;
|
creditCostPer3DWorld: number;
|
||||||
|
creditCostPer3DWorldMarble11: number;
|
||||||
|
creditCostPer3DWorldMarble11Plus: number;
|
||||||
enhanceProPricing: EnhanceProPointsPricingConfig;
|
enhanceProPricing: EnhanceProPointsPricingConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -43,11 +50,21 @@ export const defaultPointsSystemConfig: PointsSystemConfig = {
|
|||||||
blockOnInsufficient: true,
|
blockOnInsufficient: true,
|
||||||
creditCostPer3DModel: 0,
|
creditCostPer3DModel: 0,
|
||||||
creditCostPer3DWorld: 0,
|
creditCostPer3DWorld: 0,
|
||||||
|
creditCostPer3DWorldMarble11: 0,
|
||||||
|
creditCostPer3DWorldMarble11Plus: 0,
|
||||||
enhanceProPricing: {
|
enhanceProPricing: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
chargeMode: "resolution_bucket",
|
chargeMode: "scale_factor",
|
||||||
fixedCost: 45,
|
fixedCost: 45,
|
||||||
|
chargeOnlyWhenUpscaling: true,
|
||||||
batchSizeMultiplierEnabled: true,
|
batchSizeMultiplierEnabled: true,
|
||||||
|
scaleFactorCosts: {
|
||||||
|
"1": 0,
|
||||||
|
"1.5": 15,
|
||||||
|
"2": 20,
|
||||||
|
"3": 35,
|
||||||
|
"4": 45,
|
||||||
|
},
|
||||||
resolutionBuckets: [
|
resolutionBuckets: [
|
||||||
{
|
{
|
||||||
id: "enhance-pro-1mp",
|
id: "enhance-pro-1mp",
|
||||||
@ -129,6 +146,46 @@ function normalizeMegapixels(input: unknown, fallback: number) {
|
|||||||
: fallback;
|
: 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(
|
function normalizeEnhanceProResolutionBucket(
|
||||||
value: unknown,
|
value: unknown,
|
||||||
index: number,
|
index: number,
|
||||||
@ -186,12 +243,22 @@ export function normalizeEnhanceProPointsPricingConfig(
|
|||||||
raw.modelMultipliers && typeof raw.modelMultipliers === "object"
|
raw.modelMultipliers && typeof raw.modelMultipliers === "object"
|
||||||
? raw.modelMultipliers
|
? raw.modelMultipliers
|
||||||
: defaults.modelMultipliers;
|
: defaults.modelMultipliers;
|
||||||
|
const chargeMode: EnhanceProPointsChargeMode =
|
||||||
|
raw.chargeMode === "fixed" ||
|
||||||
|
raw.chargeMode === "resolution_bucket" ||
|
||||||
|
raw.chargeMode === "scale_factor"
|
||||||
|
? raw.chargeMode
|
||||||
|
: defaults.chargeMode;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled: raw.enabled === true,
|
enabled: raw.enabled === true,
|
||||||
chargeMode: raw.chargeMode === "fixed" ? "fixed" : "resolution_bucket",
|
chargeMode,
|
||||||
fixedCost: normalizeCost(raw.fixedCost ?? legacyFixedCost ?? defaults.fixedCost),
|
fixedCost: normalizeCost(raw.fixedCost ?? legacyFixedCost ?? defaults.fixedCost),
|
||||||
|
chargeOnlyWhenUpscaling: raw.chargeOnlyWhenUpscaling !== false,
|
||||||
batchSizeMultiplierEnabled: raw.batchSizeMultiplierEnabled !== false,
|
batchSizeMultiplierEnabled: raw.batchSizeMultiplierEnabled !== false,
|
||||||
|
scaleFactorCosts: normalizeEnhanceProScaleFactorCosts(
|
||||||
|
raw.scaleFactorCosts
|
||||||
|
),
|
||||||
resolutionBuckets:
|
resolutionBuckets:
|
||||||
resolutionBuckets.length > 0 ? resolutionBuckets : defaults.resolutionBuckets,
|
resolutionBuckets.length > 0 ? resolutionBuckets : defaults.resolutionBuckets,
|
||||||
modelMultipliers: {
|
modelMultipliers: {
|
||||||
@ -231,6 +298,8 @@ export function calculateEnhanceProPointsEstimate(
|
|||||||
height: number;
|
height: number;
|
||||||
batchSize?: number;
|
batchSize?: number;
|
||||||
model?: EnhanceProKreaModel;
|
model?: EnhanceProKreaModel;
|
||||||
|
upscalingActivated?: boolean;
|
||||||
|
scaleFactor?: number;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const pricing = normalizeEnhanceProPointsPricingConfig(
|
const pricing = normalizeEnhanceProPointsPricingConfig(
|
||||||
@ -245,6 +314,8 @@ export function calculateEnhanceProPointsEstimate(
|
|||||||
: "Redefine";
|
: "Redefine";
|
||||||
const megapixels = getEnhanceProResolutionMegapixels(params.width, params.height);
|
const megapixels = getEnhanceProResolutionMegapixels(params.width, params.height);
|
||||||
const batchSize = Math.max(1, Math.round(Number(params.batchSize) || 1));
|
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 batchMultiplier = pricing.batchSizeMultiplierEnabled ? batchSize : 1;
|
||||||
const modelMultiplier =
|
const modelMultiplier =
|
||||||
pricing.modelMultipliers[model] || defaultPointsSystemConfig.enhanceProPricing.modelMultipliers[model];
|
pricing.modelMultipliers[model] || defaultPointsSystemConfig.enhanceProPricing.modelMultipliers[model];
|
||||||
@ -254,10 +325,32 @@ export function calculateEnhanceProPointsEstimate(
|
|||||||
pricing.resolutionBuckets[pricing.resolutionBuckets.length - 1] ||
|
pricing.resolutionBuckets[pricing.resolutionBuckets.length - 1] ||
|
||||||
null
|
null
|
||||||
: 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 =
|
const baseCost =
|
||||||
pricing.chargeMode === "fixed"
|
pricing.chargeOnlyWhenUpscaling && !upscalingActivated
|
||||||
? pricing.fixedCost
|
? 0
|
||||||
: Math.max(0, Number(bucket?.cost || 0));
|
: pricing.chargeMode === "fixed"
|
||||||
|
? pricing.fixedCost
|
||||||
|
: pricing.chargeMode === "scale_factor"
|
||||||
|
? Math.max(0, Number(scaleFactorCost?.cost || 0))
|
||||||
|
: Math.max(0, Number(bucket?.cost || 0));
|
||||||
const rawCost = baseCost * batchMultiplier * modelMultiplier;
|
const rawCost = baseCost * batchMultiplier * modelMultiplier;
|
||||||
const cost = baseCost > 0 ? Math.max(1, Math.ceil(rawCost)) : 0;
|
const cost = baseCost > 0 ? Math.max(1, Math.ceil(rawCost)) : 0;
|
||||||
const enabled = pointsConfig.enabled === true && pricing.enabled === true;
|
const enabled = pointsConfig.enabled === true && pricing.enabled === true;
|
||||||
@ -266,6 +359,9 @@ export function calculateEnhanceProPointsEstimate(
|
|||||||
enabled,
|
enabled,
|
||||||
chargeable: enabled && cost > 0,
|
chargeable: enabled && cost > 0,
|
||||||
chargeMode: pricing.chargeMode,
|
chargeMode: pricing.chargeMode,
|
||||||
|
chargeOnlyWhenUpscaling: pricing.chargeOnlyWhenUpscaling,
|
||||||
|
upscalingActivated,
|
||||||
|
scaleFactor,
|
||||||
megapixels,
|
megapixels,
|
||||||
batchSize,
|
batchSize,
|
||||||
batchMultiplier,
|
batchMultiplier,
|
||||||
@ -273,11 +369,26 @@ export function calculateEnhanceProPointsEstimate(
|
|||||||
modelMultiplier,
|
modelMultiplier,
|
||||||
baseCost,
|
baseCost,
|
||||||
cost,
|
cost,
|
||||||
|
scaleFactorCostKey: scaleFactorCost?.factorKey || null,
|
||||||
bucketId: bucket?.id || null,
|
bucketId: bucket?.id || null,
|
||||||
bucketName: bucket?.name || 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 {
|
export function normalizePointsSystemConfig(value: unknown): PointsSystemConfig {
|
||||||
if (!value || typeof value !== "object") {
|
if (!value || typeof value !== "object") {
|
||||||
return defaultPointsSystemConfig;
|
return defaultPointsSystemConfig;
|
||||||
@ -288,6 +399,9 @@ export function normalizePointsSystemConfig(value: unknown): PointsSystemConfig
|
|||||||
creditCostPerEnhancePro?: number;
|
creditCostPerEnhancePro?: number;
|
||||||
};
|
};
|
||||||
const legacy3DCost = normalizeCost(raw.creditCostPer3D);
|
const legacy3DCost = normalizeCost(raw.creditCostPer3D);
|
||||||
|
const generic3DWorldCost = normalizeCost(
|
||||||
|
raw.creditCostPer3DWorld ?? legacy3DCost
|
||||||
|
);
|
||||||
const legacyEnhanceProCost = normalizeCost(raw.creditCostPerEnhancePro);
|
const legacyEnhanceProCost = normalizeCost(raw.creditCostPerEnhancePro);
|
||||||
return {
|
return {
|
||||||
enabled: raw.enabled === true,
|
enabled: raw.enabled === true,
|
||||||
@ -298,6 +412,12 @@ export function normalizePointsSystemConfig(value: unknown): PointsSystemConfig
|
|||||||
creditCostPer3DWorld: normalizeCost(
|
creditCostPer3DWorld: normalizeCost(
|
||||||
raw.creditCostPer3DWorld ?? legacy3DCost
|
raw.creditCostPer3DWorld ?? legacy3DCost
|
||||||
),
|
),
|
||||||
|
creditCostPer3DWorldMarble11: normalizeCost(
|
||||||
|
raw.creditCostPer3DWorldMarble11 ?? generic3DWorldCost
|
||||||
|
),
|
||||||
|
creditCostPer3DWorldMarble11Plus: normalizeCost(
|
||||||
|
raw.creditCostPer3DWorldMarble11Plus ?? generic3DWorldCost
|
||||||
|
),
|
||||||
enhanceProPricing: normalizeEnhanceProPointsPricingConfig(
|
enhanceProPricing: normalizeEnhanceProPointsPricingConfig(
|
||||||
raw.enhanceProPricing,
|
raw.enhanceProPricing,
|
||||||
legacyEnhanceProCost
|
legacyEnhanceProCost
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user