fix: align video and enhance pro point pricing
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-04-16 15:09:27 +08:00
parent 169bf2fe4c
commit c63f163b0e
7 changed files with 138 additions and 39 deletions

View File

@ -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>

View File

@ -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) {

View File

@ -1163,7 +1163,7 @@ export function EnhanceProWorkspace({
)
)
}
step="0.5"
step="0.25"
type="number"
value={formState.image_scaling_factor}
/>

View File

@ -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(

View File

@ -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",
},

View File

@ -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;
}

View File

@ -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,