import type { EnhanceProKreaModel } from "@/lib/enhance-pro/config"; export const POINTS_SYSTEM_CONFIG_KEY = "points_system_config"; export const POINTS_PACKAGES_KEY = "points_packages"; export type EnhanceProPointsChargeMode = | "fixed" | "resolution_bucket" | "scale_factor"; export type EnhanceProPointsResolutionBucket = { id: string; name: string; maxMegapixels: number; cost: number; }; export type EnhanceProPointsPricingConfig = { enabled: boolean; chargeMode: EnhanceProPointsChargeMode; fixedCost: number; chargeOnlyWhenUpscaling: boolean; batchSizeMultiplierEnabled: boolean; scaleFactorCosts: Record; resolutionBuckets: EnhanceProPointsResolutionBucket[]; modelMultipliers: Record; }; export type PointsSystemConfig = { enabled: boolean; blockOnInsufficient: boolean; creditCostPer3DModel: number; creditCostPer3DModelPbr: number; creditCostPer3DModelSmartLowPoly: number; creditCostPer3DModelQuad: number; creditCostPer3DModelRefine: number; creditCostPer3DModelTexture: number; creditCostPer3DModelAutoRig: number; creditCostPer3DModelHighpolyToLowpoly: number; creditCostPer3DWorld: number; creditCostPer3DWorldMarble11: number; creditCostPer3DWorldMarble11Plus: number; creditCostPerImageMultiView: number; creditCostPerImageRemoveBackground: number; creditCostPerImageBoxCutout: number; creditCostPerImageWatermarkRemove: number; creditCostPerImageExtend: number; creditCostPerImageRepaint: number; creditCostPerImageAngleEdit: number; enhanceProPricing: EnhanceProPointsPricingConfig; }; export type ThreeDTaskCreditParams = { taskType?: string | null; pbr?: boolean | null; quad?: boolean | null; smartLowPoly?: boolean | null; }; export type ImageToolCreditFeature = | "multi-view" | "remove-background" | "box-cutout" | "watermark-remove" | "extend" | "repaint" | "angle-edit"; export type PointsPackage = { id: string; name: string; points: number; price: number; currency: string; isEnabled: boolean; }; export const defaultPointsSystemConfig: PointsSystemConfig = { enabled: false, blockOnInsufficient: true, creditCostPer3DModel: 0, creditCostPer3DModelPbr: 0, creditCostPer3DModelSmartLowPoly: 0, creditCostPer3DModelQuad: 0, creditCostPer3DModelRefine: 0, creditCostPer3DModelTexture: 0, creditCostPer3DModelAutoRig: 0, creditCostPer3DModelHighpolyToLowpoly: 0, creditCostPer3DWorld: 0, creditCostPer3DWorldMarble11: 0, creditCostPer3DWorldMarble11Plus: 0, creditCostPerImageMultiView: 0, creditCostPerImageRemoveBackground: 0, creditCostPerImageBoxCutout: 0, creditCostPerImageWatermarkRemove: 0, creditCostPerImageExtend: 0, creditCostPerImageRepaint: 0, creditCostPerImageAngleEdit: 0, enhanceProPricing: { enabled: false, chargeMode: "scale_factor", fixedCost: 20, chargeOnlyWhenUpscaling: false, batchSizeMultiplierEnabled: true, scaleFactorCosts: { "1": 20, "1.25": 25, "1.5": 30, "2": 40, "3": 60, "4": 80, }, resolutionBuckets: [ { id: "enhance-pro-1mp", name: "1MP 以内", maxMegapixels: 1, cost: 20, }, { id: "enhance-pro-2mp", name: "2MP 以内", maxMegapixels: 2, cost: 30, }, { id: "enhance-pro-4mp", name: "4MP 以内", maxMegapixels: 4, cost: 45, }, { id: "enhance-pro-8mp", name: "8MP 以内", maxMegapixels: 8, cost: 65, }, { id: "enhance-pro-16mp", name: "16MP 以内", maxMegapixels: 16, cost: 90, }, ], modelMultipliers: { Redefine: 1, Recovery: 1, "Recovery V2": 1, Reimagine: 1, }, }, }; export const defaultPointsPackages: PointsPackage[] = [ { id: "starter-1000", name: "入门包", points: 1000, price: 9.9, currency: "CNY", isEnabled: true, }, { id: "pro-5000", name: "专业包", points: 5000, price: 39.9, currency: "CNY", isEnabled: true, }, ]; function normalizeCost(input: unknown) { const parsedValue = Number(input); return Number.isFinite(parsedValue) && parsedValue > 0 ? Math.min(100000, Math.round(parsedValue)) : 0; } function normalizeMultiplier(input: unknown, fallback: number) { const parsedValue = Number(input); return Number.isFinite(parsedValue) && parsedValue > 0 ? Math.min(10, Math.max(0.1, Number(parsedValue.toFixed(2)))) : fallback; } function normalizeMegapixels(input: unknown, fallback: number) { const parsedValue = Number(input); return Number.isFinite(parsedValue) && parsedValue > 0 ? Math.min(100, Math.max(0.1, Number(parsedValue.toFixed(2)))) : 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) .map(([factorKey, costValue]) => { const factor = tryParseScaleFactor(factorKey); if (factor === null) return null; return [formatEnhanceProScaleFactorKey(factor), normalizeCost(costValue)] as const; }) .filter((entry): entry is readonly [string, number] => Boolean(entry)) .sort((left, right) => Number(left[0]) - Number(right[0])); if (normalizedEntries.length === 0) { return defaults; } return Object.fromEntries(normalizedEntries); } function normalizeEnhanceProResolutionBucket( value: unknown, index: number, fallback: EnhanceProPointsResolutionBucket ): EnhanceProPointsResolutionBucket | null { if (!value || typeof value !== "object" || Array.isArray(value)) { return fallback; } const raw = value as Partial; const id = String(raw.id || fallback.id || `enhance-pro-bucket-${index + 1}`).trim(); const name = String(raw.name || fallback.name || `档位 ${index + 1}`).trim(); const maxMegapixels = normalizeMegapixels( raw.maxMegapixels, fallback.maxMegapixels ); const cost = normalizeCost(raw.cost ?? fallback.cost); if (!id || !name || cost <= 0) { return null; } return { id: id.slice(0, 64), name: name.slice(0, 64), maxMegapixels, cost, }; } export function normalizeEnhanceProPointsPricingConfig( value: unknown, legacyFixedCost?: number ): EnhanceProPointsPricingConfig { const defaults = defaultPointsSystemConfig.enhanceProPricing; const raw = value && typeof value === "object" && !Array.isArray(value) ? (value as Partial) : {}; const resolutionBuckets = Array.isArray(raw.resolutionBuckets) ? raw.resolutionBuckets .map((item, index) => normalizeEnhanceProResolutionBucket( item, index, defaults.resolutionBuckets[index] || defaults.resolutionBuckets[defaults.resolutionBuckets.length - 1] ) ) .filter((item): item is EnhanceProPointsResolutionBucket => Boolean(item)) .sort((left, right) => left.maxMegapixels - right.maxMegapixels) : defaults.resolutionBuckets; const modelMultipliersRaw = raw.modelMultipliers && typeof raw.modelMultipliers === "object" ? raw.modelMultipliers : defaults.modelMultipliers; const chargeMode: EnhanceProPointsChargeMode = raw.chargeMode === "fixed" || raw.chargeMode === "resolution_bucket" || raw.chargeMode === "scale_factor" ? raw.chargeMode : defaults.chargeMode; return { enabled: raw.enabled === true, chargeMode, fixedCost: normalizeCost(raw.fixedCost ?? legacyFixedCost ?? defaults.fixedCost), chargeOnlyWhenUpscaling: raw.chargeOnlyWhenUpscaling !== false, batchSizeMultiplierEnabled: raw.batchSizeMultiplierEnabled !== false, scaleFactorCosts: normalizeEnhanceProScaleFactorCosts( raw.scaleFactorCosts ), resolutionBuckets: resolutionBuckets.length > 0 ? resolutionBuckets : defaults.resolutionBuckets, modelMultipliers: { Redefine: normalizeMultiplier( modelMultipliersRaw?.Redefine, defaults.modelMultipliers.Redefine ), Recovery: normalizeMultiplier( modelMultipliersRaw?.Recovery, defaults.modelMultipliers.Recovery ), "Recovery V2": normalizeMultiplier( modelMultipliersRaw?.["Recovery V2"], defaults.modelMultipliers["Recovery V2"] ), Reimagine: normalizeMultiplier( modelMultipliersRaw?.Reimagine, defaults.modelMultipliers.Reimagine ), }, }; } export function getEnhanceProResolutionMegapixels( width: number, height: number ) { const safeWidth = Math.max(1, Math.round(Number(width) || 1)); const safeHeight = Math.max(1, Math.round(Number(height) || 1)); return Number(((safeWidth * safeHeight) / 1_048_576).toFixed(2)); } export function calculateEnhanceProPointsEstimate( pointsConfig: PointsSystemConfig, params: { width: number; height: number; batchSize?: number; model?: EnhanceProKreaModel; upscalingActivated?: boolean; scaleFactor?: number; } ) { const pricing = normalizeEnhanceProPointsPricingConfig( pointsConfig.enhanceProPricing ); const model: EnhanceProKreaModel = params.model === "Recovery" || params.model === "Recovery V2" || params.model === "Reimagine" || params.model === "Redefine" ? params.model : "Redefine"; const megapixels = getEnhanceProResolutionMegapixels(params.width, params.height); const batchSize = Math.max(1, Math.round(Number(params.batchSize) || 1)); const upscalingActivated = params.upscalingActivated === true; const scaleFactor = normalizeScaleFactor(params.scaleFactor ?? 1, 1); const billingScaleFactor = upscalingActivated ? scaleFactor : 1; const batchMultiplier = pricing.batchSizeMultiplierEnabled ? batchSize : 1; const modelMultiplier = pricing.modelMultipliers[model] || defaultPointsSystemConfig.enhanceProPricing.modelMultipliers[model]; const bucket = pricing.chargeMode === "resolution_bucket" ? pricing.resolutionBuckets.find((item) => megapixels <= item.maxMegapixels) || pricing.resolutionBuckets[pricing.resolutionBuckets.length - 1] || null : null; const scaleFactorCosts = normalizeEnhanceProScaleFactorCosts( pricing.scaleFactorCosts ); const scaleFactorCostEntries = Object.entries(scaleFactorCosts) .map(([factorKey, cost]) => ({ factorKey, factor: Number(factorKey), cost, })) .filter((item) => Number.isFinite(item.factor)) .sort((left, right) => left.factor - right.factor); const scaleFactorCost = scaleFactorCostEntries.find( (item) => Math.abs(item.factor - 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 ?? linearScaleFactorCost)) : Math.max(0, Number(bucket?.cost || 0)); const rawCost = baseCost * batchMultiplier * modelMultiplier; const cost = baseCost > 0 ? Math.max(1, Math.ceil(rawCost)) : 0; const enabled = pointsConfig.enabled === true && pricing.enabled === true; return { enabled, chargeable: enabled && cost > 0, chargeMode: pricing.chargeMode, chargeOnlyWhenUpscaling: pricing.chargeOnlyWhenUpscaling, upscalingActivated, scaleFactor, billingScaleFactor, megapixels, batchSize, batchMultiplier, model, modelMultiplier, baseCost, cost, scaleFactorCostKey: scaleFactorCost?.factorKey || null, bucketId: bucket?.id || null, bucketName: bucket?.name || null, }; } export function get3DWorldCreditCost( pointsConfig: PointsSystemConfig, modelId?: string | null ) { const normalizedModelId = String(modelId || "").trim().toLowerCase(); if (normalizedModelId === "marble-1.1-plus") { return normalizeCost(pointsConfig.creditCostPer3DWorldMarble11Plus); } if (normalizedModelId === "marble-1.1") { return normalizeCost(pointsConfig.creditCostPer3DWorldMarble11); } return normalizeCost(pointsConfig.creditCostPer3DWorld); } export function get3DTaskCreditCost( pointsConfig: PointsSystemConfig, params: ThreeDTaskCreditParams ) { const taskType = String(params.taskType || "").trim(); if ( taskType === "text_to_model" || taskType === "image_to_model" || taskType === "multiview_to_model" ) { let cost = normalizeCost(pointsConfig.creditCostPer3DModel); if (params.pbr === true) { cost += normalizeCost(pointsConfig.creditCostPer3DModelPbr); } if (params.smartLowPoly === true) { cost += normalizeCost(pointsConfig.creditCostPer3DModelSmartLowPoly); } if (params.quad === true) { cost += normalizeCost(pointsConfig.creditCostPer3DModelQuad); } return cost; } if (taskType === "refine_model") { return normalizeCost(pointsConfig.creditCostPer3DModelRefine); } if (taskType === "texture_model") { return normalizeCost(pointsConfig.creditCostPer3DModelTexture); } if (taskType === "animate_rig") { return normalizeCost(pointsConfig.creditCostPer3DModelAutoRig); } if (taskType === "highpoly_to_lowpoly") { return normalizeCost(pointsConfig.creditCostPer3DModelHighpolyToLowpoly); } return 0; } export function getImageToolCreditCost( pointsConfig: PointsSystemConfig, feature: ImageToolCreditFeature | null | undefined ) { switch (feature) { case "multi-view": return normalizeCost(pointsConfig.creditCostPerImageMultiView); case "remove-background": return normalizeCost(pointsConfig.creditCostPerImageRemoveBackground); case "box-cutout": return normalizeCost(pointsConfig.creditCostPerImageBoxCutout); case "watermark-remove": return normalizeCost(pointsConfig.creditCostPerImageWatermarkRemove); case "extend": return normalizeCost(pointsConfig.creditCostPerImageExtend); case "repaint": return normalizeCost(pointsConfig.creditCostPerImageRepaint); case "angle-edit": return normalizeCost(pointsConfig.creditCostPerImageAngleEdit); default: return 0; } } export function normalizePointsSystemConfig(value: unknown): PointsSystemConfig { if (!value || typeof value !== "object") { return defaultPointsSystemConfig; } const raw = value as Partial & { creditCostPer3D?: number; creditCostPerEnhancePro?: number; }; const legacy3DCost = normalizeCost(raw.creditCostPer3D); const generic3DWorldCost = normalizeCost( raw.creditCostPer3DWorld ?? legacy3DCost ); const legacyEnhanceProCost = normalizeCost(raw.creditCostPerEnhancePro); return { enabled: raw.enabled === true, blockOnInsufficient: raw.blockOnInsufficient !== false, creditCostPer3DModel: normalizeCost( raw.creditCostPer3DModel ?? legacy3DCost ), creditCostPer3DModelPbr: normalizeCost(raw.creditCostPer3DModelPbr), creditCostPer3DModelSmartLowPoly: normalizeCost( raw.creditCostPer3DModelSmartLowPoly ), creditCostPer3DModelQuad: normalizeCost(raw.creditCostPer3DModelQuad), creditCostPer3DModelRefine: normalizeCost(raw.creditCostPer3DModelRefine), creditCostPer3DModelTexture: normalizeCost(raw.creditCostPer3DModelTexture), creditCostPer3DModelAutoRig: normalizeCost(raw.creditCostPer3DModelAutoRig), creditCostPer3DModelHighpolyToLowpoly: normalizeCost( raw.creditCostPer3DModelHighpolyToLowpoly ), creditCostPer3DWorld: normalizeCost( raw.creditCostPer3DWorld ?? legacy3DCost ), creditCostPer3DWorldMarble11: normalizeCost( raw.creditCostPer3DWorldMarble11 ?? generic3DWorldCost ), creditCostPer3DWorldMarble11Plus: normalizeCost( raw.creditCostPer3DWorldMarble11Plus ?? generic3DWorldCost ), creditCostPerImageMultiView: normalizeCost(raw.creditCostPerImageMultiView), creditCostPerImageRemoveBackground: normalizeCost( raw.creditCostPerImageRemoveBackground ), creditCostPerImageBoxCutout: normalizeCost(raw.creditCostPerImageBoxCutout), creditCostPerImageWatermarkRemove: normalizeCost( raw.creditCostPerImageWatermarkRemove ), creditCostPerImageExtend: normalizeCost(raw.creditCostPerImageExtend), creditCostPerImageRepaint: normalizeCost(raw.creditCostPerImageRepaint), creditCostPerImageAngleEdit: normalizeCost( raw.creditCostPerImageAngleEdit ), enhanceProPricing: normalizeEnhanceProPointsPricingConfig( raw.enhanceProPricing, legacyEnhanceProCost ), }; } export function normalizePointsPackages(value: unknown): PointsPackage[] { if (!Array.isArray(value)) { return defaultPointsPackages; } const normalized = value .map((item, index) => { if (!item || typeof item !== "object") return null; const raw = item as Partial; const id = String(raw.id || `package-${index + 1}`).trim(); const name = String(raw.name || "").trim(); const points = Number(raw.points); const price = Number(raw.price); const currency = String(raw.currency || "CNY").trim().toUpperCase(); if (!id || !name || !Number.isFinite(points) || points <= 0) return null; if (!Number.isFinite(price) || price < 0) return null; return { id: id.slice(0, 64), name: name.slice(0, 64), points: Math.round(points), price: Number(price.toFixed(2)), currency: currency.slice(0, 8) || "CNY", isEnabled: raw.isEnabled !== false, } satisfies PointsPackage; }) .filter((item): item is PointsPackage => Boolean(item)) .slice(0, 30); return normalized.length > 0 ? normalized : defaultPointsPackages; }