diff --git a/app/(admin)/settings/admin-settings-client.tsx b/app/(admin)/settings/admin-settings-client.tsx
index 5bf6881dd..39b0deca8 100644
--- a/app/(admin)/settings/admin-settings-client.tsx
+++ b/app/(admin)/settings/admin-settings-client.tsx
@@ -192,6 +192,33 @@ const DEFAULT_BFL_BASE_URL = "https://api.bfl.ai";
const HOME_CHAT_ANIMATION_MAX_BYTES = 1024 * 1024;
const HOME_CHAT_ANIMATION_CONFIG_KEY = "home_chat_animation_config";
const VIDEO_PRICING_PREVIEW_RESOLUTIONS = ["480p", "720p", "1080p"] as const;
+const DEFAULT_VIDEO_RESOLUTION_MULTIPLIERS: Record<
+ (typeof VIDEO_PRICING_PREVIEW_RESOLUTIONS)[number],
+ number
+> = {
+ "480p": 1,
+ "720p": 1.5,
+ "1080p": 2,
+};
+
+function formatCompactNumber(value: number, fractionDigits = 2) {
+ if (!Number.isFinite(value)) return "0";
+ const fixed = value.toFixed(fractionDigits);
+ return fixed.replace(/\.?0+$/, "");
+}
+
+function normalizeAdminMultiplier(value: unknown, fallback = 1) {
+ const parsedValue = Number(value);
+ if (!Number.isFinite(parsedValue) || parsedValue <= 0) return fallback;
+ return Math.min(20, Math.max(0.01, Number(parsedValue.toFixed(2))));
+}
+
+function roundCreditCost(value: unknown) {
+ const parsedValue = Number(value);
+ return Number.isFinite(parsedValue) && parsedValue > 0
+ ? Math.max(1, Math.round(parsedValue))
+ : 0;
+}
function createEmptyCanvasSkillTag(): CanvasSkillTag {
return {
@@ -448,17 +475,72 @@ function applyRecommendedVideoPricing(config: ImageGenConfig | undefined) {
};
}
+function getVideoReferenceDurationCost(config: ImageGenConfig | undefined) {
+ const durationCostEntries = Object.entries(
+ config?.creditCostPerVideoByDuration || {}
+ )
+ .map(([durationKey, cost]) => ({
+ duration: Number(durationKey),
+ cost: roundCreditCost(cost),
+ }))
+ .filter((item) => Number.isFinite(item.duration) && item.cost > 0)
+ .sort((left, right) => left.duration - right.duration);
+ return (
+ durationCostEntries[durationCostEntries.length - 1]?.cost ||
+ roundCreditCost(config?.creditCostPerVideo) ||
+ recommendedVideoDurationReferenceCosts[
+ String(
+ recommendedVideoDurationOptions[recommendedVideoDurationOptions.length - 1]
+ )
+ ] ||
+ 1
+ );
+}
+
+function getVideoResolutionMultiplier(
+ config: ImageGenConfig | undefined,
+ resolution: (typeof VIDEO_PRICING_PREVIEW_RESOLUTIONS)[number]
+) {
+ const referenceCost = getVideoReferenceDurationCost(config);
+ const rawCost =
+ config?.creditCostPerVideoByResolution?.[resolution] ??
+ referenceCost * DEFAULT_VIDEO_RESOLUTION_MULTIPLIERS[resolution];
+ if (referenceCost <= 0) {
+ return DEFAULT_VIDEO_RESOLUTION_MULTIPLIERS[resolution];
+ }
+ return normalizeAdminMultiplier(
+ Number(rawCost) / referenceCost,
+ DEFAULT_VIDEO_RESOLUTION_MULTIPLIERS[resolution]
+ );
+}
+
+function getEnhanceProScaleMultiplier(
+ pricing: PointsSystemConfig["enhanceProPricing"],
+ factorKey: string,
+ cost: number
+) {
+ const baseCost = roundCreditCost(pricing.fixedCost);
+ if (baseCost <= 0) return normalizeAdminMultiplier(factorKey);
+ return normalizeAdminMultiplier(Number(cost) / baseCost, normalizeAdminMultiplier(factorKey));
+}
+
function applyRecommendedEnhanceProPricing(
pricing: PointsSystemConfig["enhanceProPricing"]
): PointsSystemConfig["enhanceProPricing"] {
const recommended = defaultPointsSystemConfig.enhanceProPricing;
+ const baseCost = roundCreditCost(pricing.fixedCost || recommended.fixedCost);
return {
...pricing,
chargeMode: recommended.chargeMode,
- fixedCost: recommended.fixedCost,
+ fixedCost: baseCost,
chargeOnlyWhenUpscaling: recommended.chargeOnlyWhenUpscaling,
batchSizeMultiplierEnabled: recommended.batchSizeMultiplierEnabled,
- scaleFactorCosts: { ...recommended.scaleFactorCosts },
+ scaleFactorCosts: Object.fromEntries(
+ Object.keys(recommended.scaleFactorCosts).map((factorKey) => [
+ factorKey,
+ roundCreditCost(baseCost * normalizeAdminMultiplier(factorKey)),
+ ])
+ ),
modelMultipliers: { ...recommended.modelMultipliers },
};
}
@@ -2563,6 +2645,9 @@ export function AdminSettingsClient() {
const configuredVideoDurationOptions = getConfiguredVideoDurationOptions(
modelForm.imageGenConfig
);
+ const videoReferenceDurationCost = getVideoReferenceDurationCost(
+ modelForm.imageGenConfig
+ );
const modelKindHint =
(selectedModelProviderType === "runway" || selectedModelProviderType === "krea") &&
!modelForm.modelId.trim()
@@ -3002,21 +3087,53 @@ export function AdminSettingsClient() {
});
};
- const updateEnhanceProScaleFactorCost = (
- factorKey: string,
- nextCost: number
- ) => {
+ const updateEnhanceProFixedCost = (nextFixedCost: number) => {
setSiteSettings((prev) => {
const config = normalizePointsSystemConfig(prev.pointsSystemConfig);
+ const pricing = config.enhanceProPricing;
+ const fixedCost = roundCreditCost(nextFixedCost);
+ const scaleFactorCosts = Object.fromEntries(
+ Object.entries(pricing.scaleFactorCosts).map(([factorKey, cost]) => {
+ const multiplier = getEnhanceProScaleMultiplier(
+ pricing,
+ factorKey,
+ Number(cost)
+ );
+ return [factorKey, roundCreditCost(fixedCost * multiplier)];
+ })
+ );
return {
...prev,
pointsSystemConfig: {
...config,
enhanceProPricing: {
- ...config.enhanceProPricing,
+ ...pricing,
+ fixedCost,
+ scaleFactorCosts,
+ },
+ },
+ };
+ });
+ };
+
+ const updateEnhanceProScaleFactorMultiplier = (
+ factorKey: string,
+ nextMultiplier: number
+ ) => {
+ setSiteSettings((prev) => {
+ const config = normalizePointsSystemConfig(prev.pointsSystemConfig);
+ const pricing = config.enhanceProPricing;
+ const fixedCost = roundCreditCost(pricing.fixedCost);
+ const multiplier = normalizeAdminMultiplier(nextMultiplier, Number(factorKey) || 1);
+ return {
+ ...prev,
+ pointsSystemConfig: {
+ ...config,
+ enhanceProPricing: {
+ ...pricing,
scaleFactorCosts: {
- ...config.enhanceProPricing.scaleFactorCosts,
- [factorKey]: Math.max(0, Math.round(nextCost) || 0),
+ ...pricing.scaleFactorCosts,
+ [factorKey]: roundCreditCost(fixedCost * multiplier),
},
},
},
@@ -8531,7 +8648,7 @@ export function AdminSettingsClient() {
图像增强 Pro 积分规则
- 默认按基础 20 积分计算;启用放大后按放大倍数折算,例如 1.25x 为 25 积分、2x 为 40 积分。
+ 按基础积分 × 档位倍率计算;启用放大后可为 1x、1.25x、1.5x、2x、3x、4x 分别设置倍率。
@@ -8633,19 +8750,9 @@ export function AdminSettingsClient() {
type="number"
value={pointsSystemConfig.enhanceProPricing.fixedCost}
onChange={(e) =>
- setSiteSettings({
- ...siteSettings,
- pointsSystemConfig: {
- ...pointsSystemConfig,
- enhanceProPricing: {
- ...pointsSystemConfig.enhanceProPricing,
- fixedCost: Math.max(
- 0,
- Number.parseInt(e.target.value || "0", 10) || 0
- ),
- },
- },
- })
+ updateEnhanceProFixedCost(
+ Number.parseInt(e.target.value || "0", 10) || 0
+ )
}
/>
@@ -8681,34 +8788,52 @@ export function AdminSettingsClient() {
-
+
- “放大倍率”计费模式下优先使用精确档位;未配置的小数倍率按基础价 × 倍数自动计算。
+ “放大倍率”计费模式下按基础积分 × 档位倍率计算;未配置的小数倍率也会按基础积分 × 倍率自动估算。
-
+
{Object.entries(
pointsSystemConfig.enhanceProPricing.scaleFactorCosts
)
.sort(([left], [right]) => Number(left) - Number(right))
- .map(([factorKey, cost]) => (
-
-
-
- updateEnhanceProScaleFactorCost(
- factorKey,
- Number.parseInt(e.target.value || "0", 10) || 0
- )
- }
- />
-
- ))}
+ .map(([factorKey, cost]) => {
+ const multiplier = getEnhanceProScaleMultiplier(
+ pointsSystemConfig.enhanceProPricing,
+ factorKey,
+ cost
+ );
+ const exactCost =
+ roundCreditCost(
+ pointsSystemConfig.enhanceProPricing.fixedCost
+ ) * multiplier;
+ return (
+
+
+
+ updateEnhanceProScaleFactorMultiplier(
+ factorKey,
+ Number(e.target.value || factorKey)
+ )
+ }
+ />
+
+ 该档积分:{formatCompactNumber(exactCost)}
+ {roundCreditCost(exactCost) !== exactCost
+ ? `,扣 ${roundCreditCost(exactCost)}`
+ : ""}
+
+
+ );
+ })}
@@ -12319,8 +12444,8 @@ export function AdminSettingsClient() {
推荐视频积分规则
- 按 5/10/15 秒 = 1/1.5/2 倍,480p/720p/1080p 基础价 =
- 100/200/250 积分计算。
+ 按 5/10/15 秒 = 165/350/600 积分,480p/720p/1080p =
+ 600 × 1/1.5/2 倍计算。