fix: align video and enhance pro point pricing
This commit is contained in:
parent
169bf2fe4c
commit
c63f163b0e
@ -171,6 +171,9 @@ import {
|
||||
buildVideoDurationCostMap,
|
||||
getConfiguredVideoDurationOptions,
|
||||
normalizeVideoDurationOptions,
|
||||
recommendedVideoDurationOptions,
|
||||
recommendedVideoDurationReferenceCosts,
|
||||
recommendedVideoResolutionBaseCosts,
|
||||
} from "@/lib/ai/video-model-config";
|
||||
import type { AgentWorkflowUiConfig } from "@/types/agent";
|
||||
|
||||
@ -421,6 +424,20 @@ function syncVideoDurationPricing(config: ImageGenConfig | undefined, durationOp
|
||||
};
|
||||
}
|
||||
|
||||
function applyRecommendedVideoPricing(config: ImageGenConfig | undefined) {
|
||||
return {
|
||||
...(config || createDefaultImageGenConfig()),
|
||||
creditCostPerVideo: recommendedVideoResolutionBaseCosts["480p"],
|
||||
videoDurationOptions: [...recommendedVideoDurationOptions],
|
||||
creditCostPerVideoByDuration: {
|
||||
...recommendedVideoDurationReferenceCosts,
|
||||
},
|
||||
creditCostPerVideoByResolution: {
|
||||
...recommendedVideoResolutionBaseCosts,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type AiModel = {
|
||||
id: string;
|
||||
providerId: string;
|
||||
@ -5597,7 +5614,7 @@ export function AdminSettingsClient() {
|
||||
type="number"
|
||||
min="1"
|
||||
max={enhanceProConfig.maxUpscaleFactor}
|
||||
step="0.5"
|
||||
step="0.25"
|
||||
value={preset.imageScalingFactor}
|
||||
onChange={(e) =>
|
||||
handleEnhanceProPresetChange(
|
||||
@ -8301,7 +8318,7 @@ export function AdminSettingsClient() {
|
||||
图像增强 Pro 积分规则
|
||||
</Label>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Krea 官方文档没有公开精确单价公式,建议按输出分辨率档位、候选数量和模型倍率计算本地积分。
|
||||
默认按基础 20 积分计算;启用放大后按放大倍数折算,例如 1.25x 为 25 积分、2x 为 40 积分。
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
@ -8326,7 +8343,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">
|
||||
@ -8372,7 +8389,7 @@ export function AdminSettingsClient() {
|
||||
固定模式单次基础积分
|
||||
</Label>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
仅在“固定单价”模式下生效,后续仍会叠加候选数量和模型倍率。
|
||||
固定单价模式直接使用该值;放大倍率模式会用它作为基础价计算未配置的小数倍数。
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 w-full max-w-xs">
|
||||
@ -8405,7 +8422,7 @@ export function AdminSettingsClient() {
|
||||
<div className="space-y-0.5">
|
||||
<Label>只在启用放大时扣积分</Label>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
开启后用户关闭“启用放大”时不扣除图像增强 Pro 积分。
|
||||
关闭时即使用户不启用放大也按 1x 基础价扣积分。
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
@ -8432,7 +8449,7 @@ export function AdminSettingsClient() {
|
||||
<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">
|
||||
@ -12007,6 +12024,34 @@ export function AdminSettingsClient() {
|
||||
|
||||
{isVideoProviderForModelConfig && (
|
||||
<div className="col-span-2 rounded-lg border border-dashed p-4 space-y-4 bg-background/80">
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm font-medium">
|
||||
推荐视频积分规则
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
按 5/10/15 秒 = 1/1.5/2 倍,480p/720p/1080p 基础价 =
|
||||
100/200/250 积分计算。
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setVideoDurationOptionsInput("5, 10, 15");
|
||||
setModelForm({
|
||||
...modelForm,
|
||||
imageGenConfig: applyRecommendedVideoPricing(
|
||||
modelForm.imageGenConfig
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
套用推荐价格
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>支持视频秒数</Label>
|
||||
<Input
|
||||
@ -12094,7 +12139,8 @@ export function AdminSettingsClient() {
|
||||
})}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
配置后会优先按用户选择的秒数扣积分;未命中的秒数才回退到“每条视频积分消耗”。
|
||||
如果同时配置了分辨率积分,系统会把最短秒数的积分作为 1
|
||||
倍参考,其它秒数按比例乘分辨率基础价。
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
@ -12155,7 +12201,7 @@ export function AdminSettingsClient() {
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
配置后会优先按用户选择的视频分辨率扣积分;未命中再回退到秒数或统一价格。
|
||||
配置后按用户选择的视频分辨率取基础价,再叠加上方秒数倍率;未命中再回退到秒数或统一价格。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -9064,6 +9064,7 @@ export async function POST(request: Request) {
|
||||
);
|
||||
const modelCreditCostPerVideo = getModelCreditCost(currentModelConfig, "video", {
|
||||
duration: Number(effectiveVideoParams?.duration) || undefined,
|
||||
resolution: String(effectiveVideoParams?.resolution || "").trim() || undefined,
|
||||
});
|
||||
|
||||
if (isImageGenerationModel) {
|
||||
|
||||
@ -1163,7 +1163,7 @@ export function EnhanceProWorkspace({
|
||||
)
|
||||
)
|
||||
}
|
||||
step="0.5"
|
||||
step="0.25"
|
||||
type="number"
|
||||
value={formState.image_scaling_factor}
|
||||
/>
|
||||
|
||||
@ -560,7 +560,7 @@ export function EnhanceProDialog({
|
||||
type="number"
|
||||
min="1"
|
||||
max={config.maxUpscaleFactor}
|
||||
step="0.5"
|
||||
step="0.25"
|
||||
value={formState.image_scaling_factor}
|
||||
onChange={(event) =>
|
||||
handleNumericField(
|
||||
|
||||
@ -1,3 +1,9 @@
|
||||
import {
|
||||
recommendedVideoDurationOptions,
|
||||
recommendedVideoDurationReferenceCosts,
|
||||
recommendedVideoResolutionBaseCosts,
|
||||
} from "@/lib/ai/video-model-config";
|
||||
|
||||
export type KreaModelKind = "image" | "image-enhance" | "video";
|
||||
|
||||
export interface KreaModelPreset {
|
||||
@ -212,10 +218,13 @@ function videoPreset(
|
||||
capabilities: createVideoCapabilities(modelId, fields),
|
||||
defaultParams: createDefaultParams("video", modelId, fields, requiredFields),
|
||||
imageGenConfig: {
|
||||
creditCostPerVideo: 0,
|
||||
videoDurationOptions: durationOptions,
|
||||
creditCostPerVideoByDuration: {},
|
||||
creditCostPerVideoByResolution: {},
|
||||
creditCostPerVideo: recommendedVideoResolutionBaseCosts["480p"],
|
||||
videoDurationOptions:
|
||||
durationOptions.length > 0
|
||||
? durationOptions
|
||||
: [...recommendedVideoDurationOptions],
|
||||
creditCostPerVideoByDuration: recommendedVideoDurationReferenceCosts,
|
||||
creditCostPerVideoByResolution: recommendedVideoResolutionBaseCosts,
|
||||
enableLoadingPlaceholder: true,
|
||||
loadingColor: "blue",
|
||||
},
|
||||
|
||||
@ -1,6 +1,18 @@
|
||||
export type VideoDurationCostMap = Record<string, number>;
|
||||
export type VideoResolutionCostMap = Record<string, number>;
|
||||
|
||||
export const recommendedVideoDurationOptions = [5, 10, 15] as const;
|
||||
export const recommendedVideoDurationReferenceCosts: VideoDurationCostMap = {
|
||||
"5": 100,
|
||||
"10": 150,
|
||||
"15": 200,
|
||||
};
|
||||
export const recommendedVideoResolutionBaseCosts: VideoResolutionCostMap = {
|
||||
"480p": 100,
|
||||
"720p": 200,
|
||||
"1080p": 250,
|
||||
};
|
||||
|
||||
export type VideoImageGenConfigLike = {
|
||||
creditCostPerVideo?: number | null;
|
||||
videoDurationOptions?: number[] | null;
|
||||
@ -131,25 +143,55 @@ export function getVideoCreditCostFromConfig(
|
||||
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,
|
||||
durationOptions
|
||||
);
|
||||
const durationCostEntries = durationCostMap
|
||||
? Object.entries(durationCostMap)
|
||||
.map(([durationKey, cost]) => ({
|
||||
duration: normalizePositiveInteger(durationKey),
|
||||
cost: Math.max(0, Math.round(Number(cost) || 0)),
|
||||
}))
|
||||
.filter((item): item is { duration: number; cost: number } =>
|
||||
item.duration !== null
|
||||
)
|
||||
.sort((left, right) => left.duration - right.duration)
|
||||
: [];
|
||||
const getDurationCost = () => {
|
||||
if (normalizedDuration === null || durationCostEntries.length === 0) return null;
|
||||
const matched =
|
||||
durationCostEntries.find((item) => item.duration === normalizedDuration) ||
|
||||
durationCostEntries.find((item) => normalizedDuration <= item.duration) ||
|
||||
durationCostEntries[durationCostEntries.length - 1] ||
|
||||
null;
|
||||
return matched?.cost ?? null;
|
||||
};
|
||||
|
||||
if (normalizedDuration !== null && durationCostMap) {
|
||||
const durationKey = String(normalizedDuration);
|
||||
if (Object.prototype.hasOwnProperty.call(durationCostMap, durationKey)) {
|
||||
return durationCostMap[durationKey] || 0;
|
||||
if (normalizedResolution && resolutionCostMap) {
|
||||
if (Object.prototype.hasOwnProperty.call(resolutionCostMap, normalizedResolution)) {
|
||||
const resolutionBaseCost = resolutionCostMap[normalizedResolution] || 0;
|
||||
const durationCost = getDurationCost();
|
||||
const baseDurationCost =
|
||||
durationCostEntries.find((item) => item.cost > 0)?.cost ?? null;
|
||||
|
||||
if (durationCost !== null && baseDurationCost && baseDurationCost > 0) {
|
||||
return Math.max(
|
||||
0,
|
||||
Math.round((resolutionBaseCost * durationCost) / baseDurationCost)
|
||||
);
|
||||
}
|
||||
|
||||
return resolutionBaseCost;
|
||||
}
|
||||
}
|
||||
|
||||
const durationCost = getDurationCost();
|
||||
if (durationCost !== null) {
|
||||
return durationCost;
|
||||
}
|
||||
|
||||
const fallbackCost = Number(config?.creditCostPerVideo ?? 0);
|
||||
return Number.isFinite(fallbackCost) ? Math.max(0, Math.round(fallbackCost)) : 0;
|
||||
}
|
||||
|
||||
@ -55,15 +55,16 @@ export const defaultPointsSystemConfig: PointsSystemConfig = {
|
||||
enhanceProPricing: {
|
||||
enabled: false,
|
||||
chargeMode: "scale_factor",
|
||||
fixedCost: 45,
|
||||
chargeOnlyWhenUpscaling: true,
|
||||
fixedCost: 20,
|
||||
chargeOnlyWhenUpscaling: false,
|
||||
batchSizeMultiplierEnabled: true,
|
||||
scaleFactorCosts: {
|
||||
"1": 0,
|
||||
"1.5": 15,
|
||||
"2": 20,
|
||||
"3": 35,
|
||||
"4": 45,
|
||||
"1": 20,
|
||||
"1.25": 25,
|
||||
"1.5": 30,
|
||||
"2": 40,
|
||||
"3": 60,
|
||||
"4": 80,
|
||||
},
|
||||
resolutionBuckets: [
|
||||
{
|
||||
@ -100,8 +101,8 @@ export const defaultPointsSystemConfig: PointsSystemConfig = {
|
||||
modelMultipliers: {
|
||||
Redefine: 1,
|
||||
Recovery: 1,
|
||||
"Recovery V2": 1.1,
|
||||
Reimagine: 1.25,
|
||||
"Recovery V2": 1,
|
||||
Reimagine: 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -316,6 +317,7 @@ export function calculateEnhanceProPointsEstimate(
|
||||
const batchSize = Math.max(1, Math.round(Number(params.batchSize) || 1));
|
||||
const upscalingActivated = params.upscalingActivated === true;
|
||||
const scaleFactor = normalizeScaleFactor(params.scaleFactor ?? 1, 1);
|
||||
const billingScaleFactor = upscalingActivated ? scaleFactor : 1;
|
||||
const batchMultiplier = pricing.batchSizeMultiplierEnabled ? batchSize : 1;
|
||||
const modelMultiplier =
|
||||
pricing.modelMultipliers[model] || defaultPointsSystemConfig.enhanceProPricing.modelMultipliers[model];
|
||||
@ -338,18 +340,16 @@ export function calculateEnhanceProPointsEstimate(
|
||||
.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;
|
||||
(item) => Math.abs(item.factor - billingScaleFactor) < 0.001
|
||||
) || null;
|
||||
const linearScaleFactorCost = Math.ceil(pricing.fixedCost * billingScaleFactor);
|
||||
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(scaleFactorCost?.cost ?? linearScaleFactorCost))
|
||||
: Math.max(0, Number(bucket?.cost || 0));
|
||||
const rawCost = baseCost * batchMultiplier * modelMultiplier;
|
||||
const cost = baseCost > 0 ? Math.max(1, Math.ceil(rawCost)) : 0;
|
||||
@ -362,6 +362,7 @@ export function calculateEnhanceProPointsEstimate(
|
||||
chargeOnlyWhenUpscaling: pricing.chargeOnlyWhenUpscaling,
|
||||
upscalingActivated,
|
||||
scaleFactor,
|
||||
billingScaleFactor,
|
||||
megapixels,
|
||||
batchSize,
|
||||
batchMultiplier,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user