mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7756-add-prices-for-Kling-Motion-Control-node-2d36d73d3650814193f3c84f25624518) by [Unito](https://www.unito.io)
2269 lines
75 KiB
TypeScript
2269 lines
75 KiB
TypeScript
import { formatCreditsFromUsd } from '@/base/credits/comfyCredits'
|
|
import type { INodeInputSlot, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
|
import type { IComboWidget } from '@/lib/litegraph/src/types/widgets'
|
|
|
|
const DEFAULT_NUMBER_OPTIONS: Intl.NumberFormatOptions = {
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0
|
|
}
|
|
|
|
type CreditFormatOptions = {
|
|
suffix?: string
|
|
note?: string
|
|
approximate?: boolean
|
|
separator?: string
|
|
}
|
|
|
|
const formatCreditsValue = (usd: number): string =>
|
|
formatCreditsFromUsd({
|
|
usd,
|
|
numberOptions: DEFAULT_NUMBER_OPTIONS
|
|
})
|
|
|
|
const makePrefix = (approximate?: boolean) => (approximate ? '~' : '')
|
|
|
|
const makeSuffix = (suffix?: string) => suffix ?? '/Run'
|
|
|
|
const appendNote = (note?: string) => (note ? ` ${note}` : '')
|
|
|
|
const formatCreditsLabel = (
|
|
usd: number,
|
|
{ suffix, note, approximate }: CreditFormatOptions = {}
|
|
): string =>
|
|
`${makePrefix(approximate)}${formatCreditsValue(usd)} credits${makeSuffix(suffix)}${appendNote(note)}`
|
|
|
|
const formatCreditsRangeLabel = (
|
|
minUsd: number,
|
|
maxUsd: number,
|
|
{ suffix, note, approximate }: CreditFormatOptions = {}
|
|
): string => {
|
|
const min = formatCreditsValue(minUsd)
|
|
const max = formatCreditsValue(maxUsd)
|
|
const rangeValue = min === max ? min : `${min}-${max}`
|
|
return `${makePrefix(approximate)}${rangeValue} credits${makeSuffix(suffix)}${appendNote(note)}`
|
|
}
|
|
|
|
const formatCreditsListLabel = (
|
|
usdValues: number[],
|
|
{ suffix, note, approximate, separator }: CreditFormatOptions = {}
|
|
): string => {
|
|
const parts = usdValues.map((value) => formatCreditsValue(value))
|
|
const value = parts.join(separator ?? '/')
|
|
return `${makePrefix(approximate)}${value} credits${makeSuffix(suffix)}${appendNote(note)}`
|
|
}
|
|
|
|
/**
|
|
* Function that calculates dynamic pricing based on node widget values
|
|
*/
|
|
type PricingFunction = (node: LGraphNode) => string
|
|
|
|
/**
|
|
* Safely executes a pricing function with error handling
|
|
* Returns null if the function throws an error, allowing the node to still render
|
|
*/
|
|
function safePricingExecution(
|
|
fn: PricingFunction,
|
|
node: LGraphNode,
|
|
fallback: string = ''
|
|
): string {
|
|
try {
|
|
return fn(node)
|
|
} catch (error) {
|
|
// Log error in development but don't throw to avoid breaking node rendering
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.warn(
|
|
'Pricing calculation failed for node:',
|
|
node.constructor?.nodeData?.name,
|
|
error
|
|
)
|
|
}
|
|
return fallback
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function to calculate Runway duration-based pricing
|
|
* @param node - The LiteGraph node
|
|
* @returns Formatted price string
|
|
*/
|
|
const calculateRunwayDurationPrice = (node: LGraphNode): string => {
|
|
const durationWidget = node.widgets?.find(
|
|
(w) => w.name === 'duration'
|
|
) as IComboWidget
|
|
|
|
if (!durationWidget) return formatCreditsLabel(0.0715, { suffix: '/second' })
|
|
|
|
const duration = Number(durationWidget.value)
|
|
const validDuration = isNaN(duration) ? 5 : duration
|
|
const cost = 0.0715 * validDuration
|
|
return formatCreditsLabel(cost)
|
|
}
|
|
|
|
const makeOmniProDurationCalculator =
|
|
(pricePerSecond: number): PricingFunction =>
|
|
(node: LGraphNode): string => {
|
|
const durationWidget = node.widgets?.find(
|
|
(w) => w.name === 'duration'
|
|
) as IComboWidget
|
|
if (!durationWidget)
|
|
return formatCreditsLabel(pricePerSecond, { suffix: '/second' })
|
|
|
|
const seconds = parseFloat(String(durationWidget.value))
|
|
if (!Number.isFinite(seconds))
|
|
return formatCreditsLabel(pricePerSecond, { suffix: '/second' })
|
|
|
|
const cost = pricePerSecond * seconds
|
|
return formatCreditsLabel(cost)
|
|
}
|
|
|
|
const klingMotionControlPricingCalculator: PricingFunction = (
|
|
node: LGraphNode
|
|
): string => {
|
|
const modeWidget = node.widgets?.find(
|
|
(w) => w.name === 'mode'
|
|
) as IComboWidget
|
|
|
|
if (!modeWidget) {
|
|
return formatCreditsListLabel([0.07, 0.112], {
|
|
suffix: '/second',
|
|
note: '(std/pro)'
|
|
})
|
|
}
|
|
|
|
const mode = String(modeWidget.value).toLowerCase()
|
|
|
|
if (mode === 'pro') return formatCreditsLabel(0.112, { suffix: '/second' })
|
|
if (mode === 'std') return formatCreditsLabel(0.07, { suffix: '/second' })
|
|
|
|
return formatCreditsListLabel([0.07, 0.112], {
|
|
suffix: '/second',
|
|
note: '(std/pro)'
|
|
})
|
|
}
|
|
|
|
const pixversePricingCalculator = (node: LGraphNode): string => {
|
|
const durationWidget = node.widgets?.find(
|
|
(w) => w.name === 'duration_seconds'
|
|
) as IComboWidget
|
|
const qualityWidget = node.widgets?.find(
|
|
(w) => w.name === 'quality'
|
|
) as IComboWidget
|
|
const motionModeWidget = node.widgets?.find(
|
|
(w) => w.name === 'motion_mode'
|
|
) as IComboWidget
|
|
|
|
if (!durationWidget || !qualityWidget) {
|
|
return formatCreditsRangeLabel(0.45, 1.2, {
|
|
note: '(varies with duration, quality & motion mode)'
|
|
})
|
|
}
|
|
|
|
const duration = String(durationWidget.value)
|
|
const quality = String(qualityWidget.value)
|
|
const motionMode = String(motionModeWidget?.value)
|
|
|
|
// Basic pricing based on duration and quality
|
|
if (duration.includes('5')) {
|
|
if (quality.includes('1080p')) return formatCreditsLabel(1.2)
|
|
if (quality.includes('720p') && motionMode?.includes('fast'))
|
|
return formatCreditsLabel(1.2)
|
|
if (quality.includes('720p') && motionMode?.includes('normal'))
|
|
return formatCreditsLabel(0.6)
|
|
if (quality.includes('540p') && motionMode?.includes('fast'))
|
|
return formatCreditsLabel(0.9)
|
|
if (quality.includes('540p') && motionMode?.includes('normal'))
|
|
return formatCreditsLabel(0.45)
|
|
if (quality.includes('360p') && motionMode?.includes('fast'))
|
|
return formatCreditsLabel(0.9)
|
|
if (quality.includes('360p') && motionMode?.includes('normal'))
|
|
return formatCreditsLabel(0.45)
|
|
} else if (duration.includes('8')) {
|
|
if (quality.includes('540p') && motionMode?.includes('normal'))
|
|
return formatCreditsLabel(0.9)
|
|
if (quality.includes('540p') && motionMode?.includes('fast'))
|
|
return formatCreditsLabel(1.2)
|
|
if (quality.includes('360p') && motionMode?.includes('normal'))
|
|
return formatCreditsLabel(0.9)
|
|
if (quality.includes('360p') && motionMode?.includes('fast'))
|
|
return formatCreditsLabel(1.2)
|
|
if (quality.includes('1080p') && motionMode?.includes('normal'))
|
|
return formatCreditsLabel(1.2)
|
|
if (quality.includes('1080p') && motionMode?.includes('fast'))
|
|
return formatCreditsLabel(1.2)
|
|
if (quality.includes('720p') && motionMode?.includes('normal'))
|
|
return formatCreditsLabel(1.2)
|
|
if (quality.includes('720p') && motionMode?.includes('fast'))
|
|
return formatCreditsLabel(1.2)
|
|
}
|
|
|
|
return formatCreditsLabel(0.9)
|
|
}
|
|
|
|
const byteDanceVideoPricingCalculator = (node: LGraphNode): string => {
|
|
const modelWidget = node.widgets?.find(
|
|
(w) => w.name === 'model'
|
|
) as IComboWidget
|
|
const durationWidget = node.widgets?.find(
|
|
(w) => w.name === 'duration'
|
|
) as IComboWidget
|
|
const resolutionWidget = node.widgets?.find(
|
|
(w) => w.name === 'resolution'
|
|
) as IComboWidget
|
|
|
|
if (!modelWidget || !durationWidget || !resolutionWidget) return 'Token-based'
|
|
|
|
const model = String(modelWidget.value).toLowerCase()
|
|
const resolution = String(resolutionWidget.value).toLowerCase()
|
|
const seconds = parseFloat(String(durationWidget.value))
|
|
const priceByModel: Record<string, Record<string, [number, number]>> = {
|
|
'seedance-1-0-pro': {
|
|
'480p': [0.23, 0.24],
|
|
'720p': [0.51, 0.56],
|
|
'1080p': [1.18, 1.22]
|
|
},
|
|
'seedance-1-0-pro-fast': {
|
|
'480p': [0.09, 0.1],
|
|
'720p': [0.21, 0.23],
|
|
'1080p': [0.47, 0.49]
|
|
},
|
|
'seedance-1-0-lite': {
|
|
'480p': [0.17, 0.18],
|
|
'720p': [0.37, 0.41],
|
|
'1080p': [0.85, 0.88]
|
|
}
|
|
}
|
|
|
|
const modelKey = model.includes('seedance-1-0-pro-fast')
|
|
? 'seedance-1-0-pro-fast'
|
|
: model.includes('seedance-1-0-pro')
|
|
? 'seedance-1-0-pro'
|
|
: model.includes('seedance-1-0-lite')
|
|
? 'seedance-1-0-lite'
|
|
: ''
|
|
|
|
const resKey = resolution.includes('1080')
|
|
? '1080p'
|
|
: resolution.includes('720')
|
|
? '720p'
|
|
: resolution.includes('480')
|
|
? '480p'
|
|
: ''
|
|
|
|
const baseRange =
|
|
modelKey && resKey ? priceByModel[modelKey]?.[resKey] : undefined
|
|
if (!baseRange) return 'Token-based'
|
|
|
|
const [min10s, max10s] = baseRange
|
|
const scale = seconds / 10
|
|
const minCost = min10s * scale
|
|
const maxCost = max10s * scale
|
|
|
|
if (minCost === maxCost) return formatCreditsLabel(minCost)
|
|
return formatCreditsRangeLabel(minCost, maxCost)
|
|
}
|
|
|
|
const ltxvPricingCalculator = (node: LGraphNode): string => {
|
|
const modelWidget = node.widgets?.find(
|
|
(w) => w.name === 'model'
|
|
) as IComboWidget
|
|
const durationWidget = node.widgets?.find(
|
|
(w) => w.name === 'duration'
|
|
) as IComboWidget
|
|
const resolutionWidget = node.widgets?.find(
|
|
(w) => w.name === 'resolution'
|
|
) as IComboWidget
|
|
|
|
const fallback = formatCreditsRangeLabel(0.04, 0.24, {
|
|
suffix: '/second'
|
|
})
|
|
if (!modelWidget || !durationWidget || !resolutionWidget) return fallback
|
|
|
|
const model = String(modelWidget.value).toLowerCase()
|
|
const resolution = String(resolutionWidget.value).toLowerCase()
|
|
const seconds = parseFloat(String(durationWidget.value))
|
|
const priceByModel: Record<string, Record<string, number>> = {
|
|
'ltx-2 (pro)': {
|
|
'1920x1080': 0.06,
|
|
'2560x1440': 0.12,
|
|
'3840x2160': 0.24
|
|
},
|
|
'ltx-2 (fast)': {
|
|
'1920x1080': 0.04,
|
|
'2560x1440': 0.08,
|
|
'3840x2160': 0.16
|
|
}
|
|
}
|
|
|
|
const modelTable = priceByModel[model]
|
|
if (!modelTable) return fallback
|
|
|
|
const pps = modelTable[resolution]
|
|
if (!pps) return fallback
|
|
|
|
const cost = pps * seconds
|
|
return formatCreditsLabel(cost)
|
|
}
|
|
|
|
const klingVideoWithAudioPricingCalculator: PricingFunction = (
|
|
node: LGraphNode
|
|
): string => {
|
|
const durationWidget = node.widgets?.find(
|
|
(w) => w.name === 'duration'
|
|
) as IComboWidget
|
|
const generateAudioWidget = node.widgets?.find(
|
|
(w) => w.name === 'generate_audio'
|
|
) as IComboWidget
|
|
|
|
if (!durationWidget || !generateAudioWidget) {
|
|
return formatCreditsRangeLabel(0.35, 1.4, {
|
|
note: '(varies with duration & audio)'
|
|
})
|
|
}
|
|
|
|
const duration = String(durationWidget.value)
|
|
const generateAudio =
|
|
String(generateAudioWidget.value).toLowerCase() === 'true'
|
|
|
|
if (duration === '5') {
|
|
return generateAudio ? formatCreditsLabel(0.7) : formatCreditsLabel(0.35)
|
|
}
|
|
|
|
if (duration === '10') {
|
|
return generateAudio ? formatCreditsLabel(1.4) : formatCreditsLabel(0.7)
|
|
}
|
|
|
|
// Fallback for unexpected duration values
|
|
return formatCreditsRangeLabel(0.35, 1.4, {
|
|
note: '(varies with duration & audio)'
|
|
})
|
|
}
|
|
|
|
// ---- constants ----
|
|
const SORA_SIZES = {
|
|
BASIC: new Set(['720x1280', '1280x720']),
|
|
PRO: new Set(['1024x1792', '1792x1024'])
|
|
}
|
|
const ALL_SIZES = new Set([...SORA_SIZES.BASIC, ...SORA_SIZES.PRO])
|
|
|
|
// ---- sora-2 pricing helpers ----
|
|
function validateSora2Selection(
|
|
modelRaw: string,
|
|
duration: number,
|
|
sizeRaw: string
|
|
): string | undefined {
|
|
const model = modelRaw?.toLowerCase() ?? ''
|
|
const size = sizeRaw?.toLowerCase() ?? ''
|
|
|
|
if (!duration || Number.isNaN(duration)) return 'Set duration (4s / 8s / 12s)'
|
|
if (!size) return 'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)'
|
|
if (!ALL_SIZES.has(size))
|
|
return 'Invalid size. Must be 720x1280, 1280x720, 1024x1792, or 1792x1024.'
|
|
|
|
if (model.includes('sora-2-pro')) return undefined
|
|
|
|
if (model.includes('sora-2') && !SORA_SIZES.BASIC.has(size))
|
|
return 'sora-2 supports only 720x1280 or 1280x720'
|
|
|
|
if (!model.includes('sora-2')) return 'Unsupported model'
|
|
|
|
return undefined
|
|
}
|
|
|
|
function perSecForSora2(modelRaw: string, sizeRaw: string): number {
|
|
const model = modelRaw?.toLowerCase() ?? ''
|
|
const size = sizeRaw?.toLowerCase() ?? ''
|
|
|
|
if (model.includes('sora-2-pro')) {
|
|
return SORA_SIZES.PRO.has(size) ? 0.5 : 0.3
|
|
}
|
|
if (model.includes('sora-2')) return 0.1
|
|
|
|
return SORA_SIZES.PRO.has(size) ? 0.5 : 0.1
|
|
}
|
|
|
|
function formatRunPrice(perSec: number, duration: number) {
|
|
return formatCreditsLabel(Number((perSec * duration).toFixed(2)))
|
|
}
|
|
|
|
// ---- pricing calculator ----
|
|
const sora2PricingCalculator: PricingFunction = (node: LGraphNode): string => {
|
|
const getWidgetValue = (name: string) =>
|
|
String(node.widgets?.find((w) => w.name === name)?.value ?? '')
|
|
|
|
const model = getWidgetValue('model')
|
|
const size = getWidgetValue('size')
|
|
const duration = Number(
|
|
node.widgets?.find((w) => ['duration', 'duration_s'].includes(w.name))
|
|
?.value
|
|
)
|
|
|
|
if (!model || !size || !duration) return 'Set model, duration & size'
|
|
|
|
const validationError = validateSora2Selection(model, duration, size)
|
|
if (validationError) return validationError
|
|
|
|
const perSec = perSecForSora2(model, size)
|
|
return formatRunPrice(perSec, duration)
|
|
}
|
|
|
|
/**
|
|
* Pricing for Tripo 3D generation nodes (Text / Image / Multiview)
|
|
* based on Tripo credits:
|
|
*
|
|
* Turbo / V3 / V2.5 / V2.0:
|
|
* Text -> 10 (no texture) / 20 (standard texture)
|
|
* Image -> 20 (no texture) / 30 (standard texture)
|
|
* Multiview -> 20 (no texture) / 30 (standard texture)
|
|
*
|
|
* V1.4:
|
|
* Text -> 20
|
|
* Image -> 30
|
|
* (Multiview treated same as Image if used)
|
|
*
|
|
* Advanced extras (added on top of generation credits):
|
|
* quad -> +5 credits
|
|
* style -> +5 credits (if style != "None")
|
|
* HD texture -> +10 credits (texture_quality = "detailed")
|
|
* detailed geometry -> +20 credits (geometry_quality = "detailed")
|
|
*
|
|
* 1 credit = $0.01
|
|
*/
|
|
const calculateTripo3DGenerationPrice = (
|
|
node: LGraphNode,
|
|
task: 'text' | 'image' | 'multiview'
|
|
): string => {
|
|
const getWidget = (name: string): IComboWidget | undefined =>
|
|
node.widgets?.find((w) => w.name === name) as IComboWidget | undefined
|
|
|
|
const getString = (name: string, defaultValue: string): string => {
|
|
const widget = getWidget(name)
|
|
if (!widget || widget.value === undefined || widget.value === null) {
|
|
return defaultValue
|
|
}
|
|
return String(widget.value)
|
|
}
|
|
|
|
const getBool = (name: string, defaultValue: boolean): boolean => {
|
|
const widget = getWidget(name)
|
|
if (!widget || widget.value === undefined || widget.value === null) {
|
|
return defaultValue
|
|
}
|
|
|
|
const v = widget.value
|
|
if (typeof v === 'number') return v !== 0
|
|
const lower = String(v).toLowerCase()
|
|
if (lower === 'true') return true
|
|
if (lower === 'false') return false
|
|
|
|
return defaultValue
|
|
}
|
|
|
|
// ---- read widget values with sensible defaults (mirroring backend) ----
|
|
const modelVersionRaw = getString('model_version', '').toLowerCase()
|
|
if (modelVersionRaw === '')
|
|
return formatCreditsRangeLabel(0.1, 0.65, {
|
|
note: '(varies with quad, style, texture & quality)'
|
|
})
|
|
const styleRaw = getString('style', 'None')
|
|
const hasStyle = styleRaw.toLowerCase() !== 'none'
|
|
|
|
// Backend defaults: texture=true, pbr=true, quad=false, qualities="standard"
|
|
const hasTexture = getBool('texture', false)
|
|
const hasPbr = getBool('pbr', false)
|
|
const quad = getBool('quad', false)
|
|
|
|
const textureQualityRaw = getString(
|
|
'texture_quality',
|
|
'standard'
|
|
).toLowerCase()
|
|
const geometryQualityRaw = getString(
|
|
'geometry_quality',
|
|
'standard'
|
|
).toLowerCase()
|
|
|
|
const isHdTexture = textureQualityRaw === 'detailed'
|
|
const isDetailedGeometry = geometryQualityRaw === 'detailed'
|
|
|
|
const withTexture = hasTexture || hasPbr
|
|
|
|
let baseCredits: number
|
|
|
|
if (modelVersionRaw.includes('v1.4')) {
|
|
// V1.4 model: Text=20, Image=30, Refine=30
|
|
if (task === 'text') {
|
|
baseCredits = 20
|
|
} else {
|
|
// treat Multiview same as Image if V1.4 is ever used there
|
|
baseCredits = 30
|
|
}
|
|
} else {
|
|
// V3.0, V2.5, V2.0 models
|
|
if (!withTexture) {
|
|
if (task === 'text') {
|
|
baseCredits = 10 // Text to 3D without texture
|
|
} else {
|
|
baseCredits = 20 // Image/Multiview to 3D without texture
|
|
}
|
|
} else {
|
|
if (task === 'text') {
|
|
baseCredits = 20 // Text to 3D with standard texture
|
|
} else {
|
|
baseCredits = 30 // Image/Multiview to 3D with standard texture
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- advanced extras on top of base generation ----
|
|
let credits = baseCredits
|
|
|
|
if (hasStyle) credits += 5 // Style
|
|
if (quad) credits += 5 // Quad Topology
|
|
if (isHdTexture) credits += 10 // HD Texture
|
|
if (isDetailedGeometry) credits += 20 // Detailed Geometry Quality
|
|
|
|
const dollars = credits * 0.01
|
|
return formatCreditsLabel(dollars)
|
|
}
|
|
|
|
/**
|
|
* Static pricing data for API nodes, now supporting both strings and functions
|
|
*/
|
|
const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
|
{
|
|
FluxProCannyNode: {
|
|
displayPrice: formatCreditsLabel(0.05)
|
|
},
|
|
FluxProDepthNode: {
|
|
displayPrice: formatCreditsLabel(0.05)
|
|
},
|
|
FluxProExpandNode: {
|
|
displayPrice: formatCreditsLabel(0.05)
|
|
},
|
|
FluxProFillNode: {
|
|
displayPrice: formatCreditsLabel(0.05)
|
|
},
|
|
FluxProUltraImageNode: {
|
|
displayPrice: formatCreditsLabel(0.06)
|
|
},
|
|
FluxProKontextProNode: {
|
|
displayPrice: formatCreditsLabel(0.04)
|
|
},
|
|
FluxProKontextMaxNode: {
|
|
displayPrice: formatCreditsLabel(0.08)
|
|
},
|
|
Flux2ProImageNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const widthW = node.widgets?.find(
|
|
(w) => w.name === 'width'
|
|
) as IComboWidget
|
|
const heightW = node.widgets?.find(
|
|
(w) => w.name === 'height'
|
|
) as IComboWidget
|
|
|
|
const w = Number(widthW?.value)
|
|
const h = Number(heightW?.value)
|
|
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {
|
|
// global min/max for this node given schema bounds (1MP..4MP output)
|
|
return formatCreditsRangeLabel(0.03, 0.15)
|
|
}
|
|
|
|
// Is the 'images' input connected?
|
|
const imagesInput = node.inputs?.find(
|
|
(i) => i.name === 'images'
|
|
) as INodeInputSlot
|
|
const hasRefs =
|
|
typeof imagesInput?.link !== 'undefined' && imagesInput.link != null
|
|
|
|
// Output cost: ceil((w*h)/MP); first MP $0.03, each additional $0.015
|
|
const MP = 1024 * 1024
|
|
const outMP = Math.max(1, Math.floor((w * h + MP - 1) / MP))
|
|
const outputCost = 0.03 + 0.015 * Math.max(outMP - 1, 0)
|
|
|
|
if (hasRefs) {
|
|
// Unknown ref count/size on the frontend:
|
|
// min extra is $0.015, max extra is $0.120 (8 MP cap / 8 refs)
|
|
const minTotal = outputCost + 0.015
|
|
const maxTotal = outputCost + 0.12
|
|
return formatCreditsRangeLabel(minTotal, maxTotal, {
|
|
approximate: true
|
|
})
|
|
}
|
|
|
|
// Precise text-to-image price
|
|
return formatCreditsLabel(outputCost)
|
|
}
|
|
},
|
|
Flux2MaxImageNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const widthW = node.widgets?.find(
|
|
(w) => w.name === 'width'
|
|
) as IComboWidget
|
|
const heightW = node.widgets?.find(
|
|
(w) => w.name === 'height'
|
|
) as IComboWidget
|
|
|
|
const w = Number(widthW?.value)
|
|
const h = Number(heightW?.value)
|
|
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {
|
|
// global min/max for this node given schema bounds (1MP..4MP output)
|
|
return formatCreditsRangeLabel(0.07, 0.35)
|
|
}
|
|
|
|
// Is the 'images' input connected?
|
|
const imagesInput = node.inputs?.find(
|
|
(i) => i.name === 'images'
|
|
) as INodeInputSlot
|
|
const hasRefs =
|
|
typeof imagesInput?.link !== 'undefined' && imagesInput.link != null
|
|
|
|
// Output cost: ceil((w*h)/MP); first MP $0.07, each additional $0.03
|
|
const MP = 1024 * 1024
|
|
const outMP = Math.max(1, Math.floor((w * h + MP - 1) / MP))
|
|
const outputCost = 0.07 + 0.03 * Math.max(outMP - 1, 0)
|
|
|
|
if (hasRefs) {
|
|
// Unknown ref count/size on the frontend:
|
|
// min extra is $0.03, max extra is $0.24 (8 MP cap / 8 refs)
|
|
const minTotal = outputCost + 0.03
|
|
const maxTotal = outputCost + 0.24
|
|
return formatCreditsRangeLabel(minTotal, maxTotal)
|
|
}
|
|
|
|
return formatCreditsLabel(outputCost)
|
|
}
|
|
},
|
|
OpenAIVideoSora2: {
|
|
displayPrice: sora2PricingCalculator
|
|
},
|
|
IdeogramV1: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const numImagesWidget = node.widgets?.find(
|
|
(w) => w.name === 'num_images'
|
|
) as IComboWidget
|
|
const turboWidget = node.widgets?.find(
|
|
(w) => w.name === 'turbo'
|
|
) as IComboWidget
|
|
|
|
if (!numImagesWidget)
|
|
return formatCreditsRangeLabel(0.03, 0.09, {
|
|
suffix: ' x num_images/Run'
|
|
})
|
|
|
|
const numImages = Number(numImagesWidget.value) || 1
|
|
const turbo = String(turboWidget?.value).toLowerCase() === 'true'
|
|
const basePrice = turbo ? 0.0286 : 0.0858
|
|
const cost = Number((basePrice * numImages).toFixed(2))
|
|
return formatCreditsLabel(cost)
|
|
}
|
|
},
|
|
IdeogramV2: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const numImagesWidget = node.widgets?.find(
|
|
(w) => w.name === 'num_images'
|
|
) as IComboWidget
|
|
const turboWidget = node.widgets?.find(
|
|
(w) => w.name === 'turbo'
|
|
) as IComboWidget
|
|
|
|
if (!numImagesWidget)
|
|
return formatCreditsRangeLabel(0.07, 0.11, {
|
|
suffix: ' x num_images/Run'
|
|
})
|
|
|
|
const numImages = Number(numImagesWidget.value) || 1
|
|
const turbo = String(turboWidget?.value).toLowerCase() === 'true'
|
|
const basePrice = turbo ? 0.0715 : 0.1144
|
|
const cost = Number((basePrice * numImages).toFixed(2))
|
|
return formatCreditsLabel(cost)
|
|
}
|
|
},
|
|
IdeogramV3: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const renderingSpeedWidget = node.widgets?.find(
|
|
(w) => w.name === 'rendering_speed'
|
|
) as IComboWidget
|
|
const numImagesWidget = node.widgets?.find(
|
|
(w) => w.name === 'num_images'
|
|
) as IComboWidget
|
|
const characterInput = node.inputs?.find(
|
|
(i) => i.name === 'character_image'
|
|
) as INodeInputSlot
|
|
const hasCharacter =
|
|
typeof characterInput?.link !== 'undefined' &&
|
|
characterInput.link != null
|
|
|
|
if (!renderingSpeedWidget)
|
|
return formatCreditsRangeLabel(0.04, 0.11, {
|
|
suffix: ' x num_images/Run',
|
|
note: '(varies with rendering speed & num_images)'
|
|
})
|
|
|
|
const numImages = Number(numImagesWidget?.value) || 1
|
|
let basePrice = 0.0858 // default balanced price
|
|
|
|
const renderingSpeed = String(renderingSpeedWidget.value)
|
|
if (renderingSpeed.toLowerCase().includes('quality')) {
|
|
if (hasCharacter) {
|
|
basePrice = 0.286
|
|
} else {
|
|
basePrice = 0.1287
|
|
}
|
|
} else if (renderingSpeed.toLowerCase().includes('default')) {
|
|
if (hasCharacter) {
|
|
basePrice = 0.2145
|
|
} else {
|
|
basePrice = 0.0858
|
|
}
|
|
} else if (renderingSpeed.toLowerCase().includes('turbo')) {
|
|
if (hasCharacter) {
|
|
basePrice = 0.143
|
|
} else {
|
|
basePrice = 0.0429
|
|
}
|
|
}
|
|
|
|
const totalCost = Number((basePrice * numImages).toFixed(2))
|
|
return formatCreditsLabel(totalCost)
|
|
}
|
|
},
|
|
KlingCameraControlI2VNode: {
|
|
displayPrice: formatCreditsLabel(0.49)
|
|
},
|
|
KlingCameraControlT2VNode: {
|
|
displayPrice: formatCreditsLabel(0.14)
|
|
},
|
|
KlingDualCharacterVideoEffectNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const modeWidget = node.widgets?.find(
|
|
(w) => w.name === 'mode'
|
|
) as IComboWidget
|
|
const modelWidget = node.widgets?.find(
|
|
(w) => w.name === 'model_name'
|
|
) as IComboWidget
|
|
const durationWidget = node.widgets?.find(
|
|
(w) => w.name === 'duration'
|
|
) as IComboWidget
|
|
if (!modeWidget || !modelWidget || !durationWidget)
|
|
return formatCreditsRangeLabel(0.14, 2.8, {
|
|
note: '(varies with model, mode & duration)'
|
|
})
|
|
|
|
const modeValue = String(modeWidget.value)
|
|
const durationValue = String(durationWidget.value)
|
|
const modelValue = String(modelWidget.value)
|
|
|
|
// Same pricing matrix as KlingTextToVideoNode
|
|
if (modelValue.includes('v1-6') || modelValue.includes('v1-5')) {
|
|
if (modeValue.includes('pro')) {
|
|
return durationValue.includes('10')
|
|
? formatCreditsLabel(0.98)
|
|
: formatCreditsLabel(0.49)
|
|
} else {
|
|
return durationValue.includes('10')
|
|
? formatCreditsLabel(0.56)
|
|
: formatCreditsLabel(0.28)
|
|
}
|
|
} else if (modelValue.includes('v1')) {
|
|
if (modeValue.includes('pro')) {
|
|
return durationValue.includes('10')
|
|
? formatCreditsLabel(0.98)
|
|
: formatCreditsLabel(0.49)
|
|
} else {
|
|
return durationValue.includes('10')
|
|
? formatCreditsLabel(0.28)
|
|
: formatCreditsLabel(0.14)
|
|
}
|
|
}
|
|
|
|
return formatCreditsLabel(0.14)
|
|
}
|
|
},
|
|
KlingImage2VideoNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const modeWidget = node.widgets?.find(
|
|
(w) => w.name === 'mode'
|
|
) as IComboWidget
|
|
const modelWidget = node.widgets?.find(
|
|
(w) => w.name === 'model_name'
|
|
) as IComboWidget
|
|
const durationWidget = node.widgets?.find(
|
|
(w) => w.name === 'duration'
|
|
) as IComboWidget
|
|
|
|
if (!modeWidget) {
|
|
if (!modelWidget)
|
|
return formatCreditsRangeLabel(0.14, 2.8, {
|
|
note: '(varies with model, mode & duration)'
|
|
})
|
|
|
|
const modelValue = String(modelWidget.value)
|
|
if (
|
|
modelValue.includes('v2-1-master') ||
|
|
modelValue.includes('v2-master')
|
|
) {
|
|
return formatCreditsLabel(1.4)
|
|
} else if (
|
|
modelValue.includes('v1-6') ||
|
|
modelValue.includes('v1-5')
|
|
) {
|
|
return formatCreditsLabel(0.28)
|
|
}
|
|
return formatCreditsLabel(0.14)
|
|
}
|
|
|
|
const modeValue = String(modeWidget.value)
|
|
const durationValue = String(durationWidget.value)
|
|
const modelValue = String(modelWidget.value)
|
|
|
|
// Same pricing matrix as KlingTextToVideoNode
|
|
if (modelValue.includes('v2-5-turbo')) {
|
|
if (durationValue.includes('10')) {
|
|
return formatCreditsLabel(0.7)
|
|
}
|
|
return formatCreditsLabel(0.35) // 5s default
|
|
} else if (
|
|
modelValue.includes('v2-1-master') ||
|
|
modelValue.includes('v2-master')
|
|
) {
|
|
if (durationValue.includes('10')) {
|
|
return formatCreditsLabel(2.8)
|
|
}
|
|
return formatCreditsLabel(1.4) // 5s default
|
|
} else if (
|
|
modelValue.includes('v2-1') ||
|
|
modelValue.includes('v1-6') ||
|
|
modelValue.includes('v1-5')
|
|
) {
|
|
if (modeValue.includes('pro')) {
|
|
return durationValue.includes('10')
|
|
? formatCreditsLabel(0.98)
|
|
: formatCreditsLabel(0.49)
|
|
} else {
|
|
return durationValue.includes('10')
|
|
? formatCreditsLabel(0.56)
|
|
: formatCreditsLabel(0.28)
|
|
}
|
|
} else if (modelValue.includes('v1')) {
|
|
if (modeValue.includes('pro')) {
|
|
return durationValue.includes('10')
|
|
? formatCreditsLabel(0.98)
|
|
: formatCreditsLabel(0.49)
|
|
} else {
|
|
return durationValue.includes('10')
|
|
? formatCreditsLabel(0.28)
|
|
: formatCreditsLabel(0.14)
|
|
}
|
|
}
|
|
|
|
return formatCreditsLabel(0.14)
|
|
}
|
|
},
|
|
KlingImageGenerationNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const imageInputWidget = node.inputs?.find((i) => i.name === 'image')
|
|
// If link is not null => image is connected => modality is image to image
|
|
const modality = imageInputWidget?.link
|
|
? 'image to image'
|
|
: 'text to image'
|
|
const modelWidget = node.widgets?.find(
|
|
(w) => w.name === 'model_name'
|
|
) as IComboWidget
|
|
const nWidget = node.widgets?.find(
|
|
(w) => w.name === 'n'
|
|
) as IComboWidget
|
|
|
|
if (!modelWidget)
|
|
return formatCreditsRangeLabel(0.0035, 0.028, {
|
|
suffix: ' x n/Run',
|
|
note: '(varies with modality & model)'
|
|
})
|
|
|
|
const model = String(modelWidget.value)
|
|
const n = Number(nWidget?.value) || 1
|
|
let basePrice = 0.014 // default
|
|
|
|
if (modality.includes('text to image')) {
|
|
if (model.includes('kling-v1-5') || model.includes('kling-v2')) {
|
|
basePrice = 0.014
|
|
} else if (model.includes('kling-v1')) {
|
|
basePrice = 0.0035
|
|
}
|
|
} else if (modality.includes('image to image')) {
|
|
if (model.includes('kling-v1-5')) {
|
|
basePrice = 0.028
|
|
} else if (model.includes('kling-v1')) {
|
|
basePrice = 0.0035
|
|
}
|
|
}
|
|
|
|
const totalCost = basePrice * n
|
|
return formatCreditsLabel(totalCost)
|
|
}
|
|
},
|
|
KlingLipSyncAudioToVideoNode: {
|
|
displayPrice: formatCreditsLabel(0.1, { approximate: true })
|
|
},
|
|
KlingLipSyncTextToVideoNode: {
|
|
displayPrice: formatCreditsLabel(0.1, { approximate: true })
|
|
},
|
|
KlingSingleImageVideoEffectNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const effectSceneWidget = node.widgets?.find(
|
|
(w) => w.name === 'effect_scene'
|
|
) as IComboWidget
|
|
|
|
if (!effectSceneWidget)
|
|
return formatCreditsRangeLabel(0.28, 0.49, {
|
|
note: '(varies with effect scene)'
|
|
})
|
|
|
|
const effectScene = String(effectSceneWidget.value)
|
|
if (
|
|
effectScene.includes('fuzzyfuzzy') ||
|
|
effectScene.includes('squish')
|
|
) {
|
|
return formatCreditsLabel(0.28)
|
|
} else if (effectScene.includes('dizzydizzy')) {
|
|
return formatCreditsLabel(0.49)
|
|
} else if (effectScene.includes('bloombloom')) {
|
|
return formatCreditsLabel(0.49)
|
|
} else if (effectScene.includes('expansion')) {
|
|
return formatCreditsLabel(0.28)
|
|
}
|
|
|
|
return formatCreditsLabel(0.28)
|
|
}
|
|
},
|
|
KlingStartEndFrameNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
// Same pricing as KlingTextToVideoNode per CSV ("Same as text to video")
|
|
const modeWidget = node.widgets?.find(
|
|
(w) => w.name === 'mode'
|
|
) as IComboWidget
|
|
if (!modeWidget)
|
|
return formatCreditsRangeLabel(0.14, 2.8, {
|
|
note: '(varies with model, mode & duration)'
|
|
})
|
|
|
|
const modeValue = String(modeWidget.value)
|
|
|
|
// Same pricing matrix as KlingTextToVideoNode
|
|
if (modeValue.includes('v2-5-turbo')) {
|
|
if (modeValue.includes('10')) {
|
|
return formatCreditsLabel(0.7)
|
|
}
|
|
return formatCreditsLabel(0.35) // 5s default
|
|
} else if (modeValue.includes('v2-1')) {
|
|
if (modeValue.includes('10s')) {
|
|
return formatCreditsLabel(0.98) // pro, 10s
|
|
}
|
|
return formatCreditsLabel(0.49) // pro, 5s default
|
|
} else if (modeValue.includes('v2-master')) {
|
|
if (modeValue.includes('10s')) {
|
|
return formatCreditsLabel(2.8)
|
|
}
|
|
return formatCreditsLabel(1.4) // 5s default
|
|
} else if (modeValue.includes('v1-6')) {
|
|
if (modeValue.includes('pro')) {
|
|
return modeValue.includes('10s')
|
|
? formatCreditsLabel(0.98)
|
|
: formatCreditsLabel(0.49)
|
|
} else {
|
|
return modeValue.includes('10s')
|
|
? formatCreditsLabel(0.56)
|
|
: formatCreditsLabel(0.28)
|
|
}
|
|
} else if (modeValue.includes('v1')) {
|
|
if (modeValue.includes('pro')) {
|
|
return modeValue.includes('10s')
|
|
? formatCreditsLabel(0.98)
|
|
: formatCreditsLabel(0.49)
|
|
} else {
|
|
return modeValue.includes('10s')
|
|
? formatCreditsLabel(0.28)
|
|
: formatCreditsLabel(0.14)
|
|
}
|
|
}
|
|
|
|
return formatCreditsLabel(0.14)
|
|
}
|
|
},
|
|
KlingTextToVideoNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const modeWidget = node.widgets?.find(
|
|
(w) => w.name === 'mode'
|
|
) as IComboWidget
|
|
if (!modeWidget)
|
|
return formatCreditsRangeLabel(0.14, 2.8, {
|
|
note: '(varies with model, mode & duration)'
|
|
})
|
|
|
|
const modeValue = String(modeWidget.value)
|
|
|
|
// Pricing matrix from CSV data based on mode string content
|
|
if (modeValue.includes('v2-5-turbo')) {
|
|
if (modeValue.includes('10')) {
|
|
return formatCreditsLabel(0.7)
|
|
}
|
|
return formatCreditsLabel(0.35) // 5s default
|
|
} else if (modeValue.includes('v2-1-master')) {
|
|
if (modeValue.includes('10s')) {
|
|
return formatCreditsLabel(2.8) // price is the same as for v2-master model
|
|
}
|
|
return formatCreditsLabel(1.4) // price is the same as for v2-master model
|
|
} else if (modeValue.includes('v2-master')) {
|
|
if (modeValue.includes('10s')) {
|
|
return formatCreditsLabel(2.8)
|
|
}
|
|
return formatCreditsLabel(1.4) // 5s default
|
|
} else if (modeValue.includes('v1-6')) {
|
|
if (modeValue.includes('pro')) {
|
|
return modeValue.includes('10s')
|
|
? formatCreditsLabel(0.98)
|
|
: formatCreditsLabel(0.49)
|
|
} else {
|
|
return modeValue.includes('10s')
|
|
? formatCreditsLabel(0.56)
|
|
: formatCreditsLabel(0.28)
|
|
}
|
|
} else if (modeValue.includes('v1')) {
|
|
if (modeValue.includes('pro')) {
|
|
return modeValue.includes('10s')
|
|
? formatCreditsLabel(0.98)
|
|
: formatCreditsLabel(0.49)
|
|
} else {
|
|
return modeValue.includes('10s')
|
|
? formatCreditsLabel(0.28)
|
|
: formatCreditsLabel(0.14)
|
|
}
|
|
}
|
|
|
|
return formatCreditsLabel(0.14)
|
|
}
|
|
},
|
|
KlingVideoExtendNode: {
|
|
displayPrice: formatCreditsLabel(0.28)
|
|
},
|
|
KlingVirtualTryOnNode: {
|
|
displayPrice: formatCreditsLabel(0.07)
|
|
},
|
|
KlingOmniProTextToVideoNode: {
|
|
displayPrice: makeOmniProDurationCalculator(0.112)
|
|
},
|
|
KlingOmniProFirstLastFrameNode: {
|
|
displayPrice: makeOmniProDurationCalculator(0.112)
|
|
},
|
|
KlingOmniProImageToVideoNode: {
|
|
displayPrice: makeOmniProDurationCalculator(0.112)
|
|
},
|
|
KlingOmniProVideoToVideoNode: {
|
|
displayPrice: makeOmniProDurationCalculator(0.168)
|
|
},
|
|
KlingMotionControl: {
|
|
displayPrice: klingMotionControlPricingCalculator
|
|
},
|
|
KlingOmniProEditVideoNode: {
|
|
displayPrice: formatCreditsLabel(0.168, { suffix: '/second' })
|
|
},
|
|
KlingOmniProImageNode: {
|
|
displayPrice: formatCreditsLabel(0.028)
|
|
},
|
|
KlingTextToVideoWithAudio: {
|
|
displayPrice: klingVideoWithAudioPricingCalculator
|
|
},
|
|
KlingImageToVideoWithAudio: {
|
|
displayPrice: klingVideoWithAudioPricingCalculator
|
|
},
|
|
LumaImageToVideoNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
// Same pricing as LumaVideoNode per CSV
|
|
const modelWidget = node.widgets?.find(
|
|
(w) => w.name === 'model'
|
|
) as IComboWidget
|
|
const resolutionWidget = node.widgets?.find(
|
|
(w) => w.name === 'resolution'
|
|
) as IComboWidget
|
|
const durationWidget = node.widgets?.find(
|
|
(w) => w.name === 'duration'
|
|
) as IComboWidget
|
|
|
|
if (!modelWidget || !resolutionWidget || !durationWidget) {
|
|
return formatCreditsRangeLabel(0.2, 16.4, {
|
|
note: '(varies with model, resolution & duration)'
|
|
})
|
|
}
|
|
|
|
const model = String(modelWidget.value)
|
|
const resolution = String(resolutionWidget.value).toLowerCase()
|
|
const duration = String(durationWidget.value)
|
|
|
|
if (model.includes('ray-flash-2')) {
|
|
if (duration.includes('5s')) {
|
|
if (resolution.includes('4k')) return formatCreditsLabel(3.13)
|
|
if (resolution.includes('1080p')) return formatCreditsLabel(0.79)
|
|
if (resolution.includes('720p')) return formatCreditsLabel(0.34)
|
|
if (resolution.includes('540p')) return formatCreditsLabel(0.2)
|
|
} else if (duration.includes('9s')) {
|
|
if (resolution.includes('4k')) return formatCreditsLabel(5.65)
|
|
if (resolution.includes('1080p')) return formatCreditsLabel(1.42)
|
|
if (resolution.includes('720p')) return formatCreditsLabel(0.61)
|
|
if (resolution.includes('540p')) return formatCreditsLabel(0.36)
|
|
}
|
|
} else if (model.includes('ray-2')) {
|
|
if (duration.includes('5s')) {
|
|
if (resolution.includes('4k')) return formatCreditsLabel(9.11)
|
|
if (resolution.includes('1080p')) return formatCreditsLabel(2.27)
|
|
if (resolution.includes('720p')) return formatCreditsLabel(1.02)
|
|
if (resolution.includes('540p')) return formatCreditsLabel(0.57)
|
|
} else if (duration.includes('9s')) {
|
|
if (resolution.includes('4k')) return formatCreditsLabel(16.4)
|
|
if (resolution.includes('1080p')) return formatCreditsLabel(4.1)
|
|
if (resolution.includes('720p')) return formatCreditsLabel(1.83)
|
|
if (resolution.includes('540p')) return formatCreditsLabel(1.03)
|
|
}
|
|
} else if (model.includes('ray-1-6')) {
|
|
return formatCreditsLabel(0.5)
|
|
}
|
|
|
|
return formatCreditsLabel(0.79)
|
|
}
|
|
},
|
|
LumaVideoNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const modelWidget = node.widgets?.find(
|
|
(w) => w.name === 'model'
|
|
) as IComboWidget
|
|
const resolutionWidget = node.widgets?.find(
|
|
(w) => w.name === 'resolution'
|
|
) as IComboWidget
|
|
const durationWidget = node.widgets?.find(
|
|
(w) => w.name === 'duration'
|
|
) as IComboWidget
|
|
|
|
if (!modelWidget || !resolutionWidget || !durationWidget) {
|
|
return formatCreditsRangeLabel(0.2, 16.4, {
|
|
note: '(varies with model, resolution & duration)'
|
|
})
|
|
}
|
|
|
|
const model = String(modelWidget.value)
|
|
const resolution = String(resolutionWidget.value).toLowerCase()
|
|
const duration = String(durationWidget.value)
|
|
|
|
if (model.includes('ray-flash-2')) {
|
|
if (duration.includes('5s')) {
|
|
if (resolution.includes('4k')) return formatCreditsLabel(3.13)
|
|
if (resolution.includes('1080p')) return formatCreditsLabel(0.79)
|
|
if (resolution.includes('720p')) return formatCreditsLabel(0.34)
|
|
if (resolution.includes('540p')) return formatCreditsLabel(0.2)
|
|
} else if (duration.includes('9s')) {
|
|
if (resolution.includes('4k')) return formatCreditsLabel(5.65)
|
|
if (resolution.includes('1080p')) return formatCreditsLabel(1.42)
|
|
if (resolution.includes('720p')) return formatCreditsLabel(0.61)
|
|
if (resolution.includes('540p')) return formatCreditsLabel(0.36)
|
|
}
|
|
} else if (model.includes('ray-2')) {
|
|
if (duration.includes('5s')) {
|
|
if (resolution.includes('4k')) return formatCreditsLabel(9.11)
|
|
if (resolution.includes('1080p')) return formatCreditsLabel(2.27)
|
|
if (resolution.includes('720p')) return formatCreditsLabel(1.02)
|
|
if (resolution.includes('540p')) return formatCreditsLabel(0.57)
|
|
} else if (duration.includes('9s')) {
|
|
if (resolution.includes('4k')) return formatCreditsLabel(16.4)
|
|
if (resolution.includes('1080p')) return formatCreditsLabel(4.1)
|
|
if (resolution.includes('720p')) return formatCreditsLabel(1.83)
|
|
if (resolution.includes('540p')) return formatCreditsLabel(1.03)
|
|
}
|
|
} else if (model.includes('ray-1-6')) {
|
|
return formatCreditsLabel(0.5)
|
|
}
|
|
|
|
return formatCreditsLabel(0.79)
|
|
}
|
|
},
|
|
MinimaxImageToVideoNode: {
|
|
displayPrice: formatCreditsLabel(0.43)
|
|
},
|
|
MinimaxTextToVideoNode: {
|
|
displayPrice: formatCreditsLabel(0.43)
|
|
},
|
|
MinimaxHailuoVideoNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const resolutionWidget = node.widgets?.find(
|
|
(w) => w.name === 'resolution'
|
|
) as IComboWidget
|
|
const durationWidget = node.widgets?.find(
|
|
(w) => w.name === 'duration'
|
|
) as IComboWidget
|
|
|
|
if (!resolutionWidget || !durationWidget) {
|
|
return formatCreditsRangeLabel(0.28, 0.56, {
|
|
note: '(varies with resolution & duration)'
|
|
})
|
|
}
|
|
|
|
const resolution = String(resolutionWidget.value)
|
|
const duration = String(durationWidget.value)
|
|
|
|
if (resolution.includes('768P')) {
|
|
if (duration.includes('6')) return formatCreditsLabel(0.28)
|
|
if (duration.includes('10')) return formatCreditsLabel(0.56)
|
|
} else if (resolution.includes('1080P')) {
|
|
if (duration.includes('6')) return formatCreditsLabel(0.49)
|
|
}
|
|
|
|
return formatCreditsLabel(0.43) // default median
|
|
}
|
|
},
|
|
OpenAIDalle2: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const sizeWidget = node.widgets?.find(
|
|
(w) => w.name === 'size'
|
|
) as IComboWidget
|
|
const nWidget = node.widgets?.find(
|
|
(w) => w.name === 'n'
|
|
) as IComboWidget
|
|
|
|
if (!sizeWidget)
|
|
return formatCreditsRangeLabel(0.016, 0.02, {
|
|
suffix: ' x n/Run',
|
|
note: '(varies with size & n)'
|
|
})
|
|
|
|
const size = String(sizeWidget.value)
|
|
const n = Number(nWidget?.value) || 1
|
|
let basePrice = 0.02 // default
|
|
|
|
if (size.includes('1024x1024')) {
|
|
basePrice = 0.02
|
|
} else if (size.includes('512x512')) {
|
|
basePrice = 0.018
|
|
} else if (size.includes('256x256')) {
|
|
basePrice = 0.016
|
|
}
|
|
|
|
const totalCost = Number((basePrice * n).toFixed(3))
|
|
return formatCreditsLabel(totalCost)
|
|
}
|
|
},
|
|
OpenAIDalle3: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
// Get size and quality widgets
|
|
const sizeWidget = node.widgets?.find(
|
|
(w) => w.name === 'size'
|
|
) as IComboWidget
|
|
const qualityWidget = node.widgets?.find(
|
|
(w) => w.name === 'quality'
|
|
) as IComboWidget
|
|
|
|
if (!sizeWidget || !qualityWidget)
|
|
return formatCreditsRangeLabel(0.04, 0.12, {
|
|
note: '(varies with size & quality)'
|
|
})
|
|
|
|
const size = String(sizeWidget.value)
|
|
const quality = String(qualityWidget.value)
|
|
|
|
// Pricing matrix based on CSV data
|
|
if (size.includes('1024x1024')) {
|
|
return quality.includes('hd')
|
|
? formatCreditsLabel(0.08)
|
|
: formatCreditsLabel(0.04)
|
|
} else if (size.includes('1792x1024') || size.includes('1024x1792')) {
|
|
return quality.includes('hd')
|
|
? formatCreditsLabel(0.12)
|
|
: formatCreditsLabel(0.08)
|
|
}
|
|
|
|
// Default value
|
|
return formatCreditsLabel(0.04)
|
|
}
|
|
},
|
|
OpenAIGPTImage1: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const qualityWidget = node.widgets?.find(
|
|
(w) => w.name === 'quality'
|
|
) as IComboWidget
|
|
const nWidget = node.widgets?.find(
|
|
(w) => w.name === 'n'
|
|
) as IComboWidget
|
|
|
|
if (!qualityWidget)
|
|
return formatCreditsRangeLabel(0.011, 0.3, {
|
|
suffix: ' x n/Run',
|
|
note: '(varies with quality & n)'
|
|
})
|
|
|
|
const quality = String(qualityWidget.value)
|
|
const n = Number(nWidget?.value) || 1
|
|
let range: [number, number] = [0.046, 0.07] // default medium
|
|
|
|
if (quality.includes('high')) {
|
|
range = [0.167, 0.3]
|
|
} else if (quality.includes('medium')) {
|
|
range = [0.046, 0.07]
|
|
} else if (quality.includes('low')) {
|
|
range = [0.011, 0.02]
|
|
}
|
|
|
|
if (n === 1) {
|
|
return formatCreditsRangeLabel(range[0], range[1])
|
|
}
|
|
return formatCreditsRangeLabel(range[0], range[1], {
|
|
suffix: ` x ${n}/Run`
|
|
})
|
|
}
|
|
},
|
|
PixverseImageToVideoNode: {
|
|
displayPrice: pixversePricingCalculator
|
|
},
|
|
PixverseTextToVideoNode: {
|
|
displayPrice: pixversePricingCalculator
|
|
},
|
|
PixverseTransitionVideoNode: {
|
|
displayPrice: pixversePricingCalculator
|
|
},
|
|
RecraftCreativeUpscaleNode: {
|
|
displayPrice: formatCreditsLabel(0.25)
|
|
},
|
|
RecraftCrispUpscaleNode: {
|
|
displayPrice: formatCreditsLabel(0.004)
|
|
},
|
|
RecraftGenerateColorFromImageNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const nWidget = node.widgets?.find(
|
|
(w) => w.name === 'n'
|
|
) as IComboWidget
|
|
if (!nWidget) return formatCreditsLabel(0.04, { suffix: ' x n/Run' })
|
|
|
|
const n = Number(nWidget.value) || 1
|
|
const cost = Number((0.04 * n).toFixed(2))
|
|
return formatCreditsLabel(cost)
|
|
}
|
|
},
|
|
RecraftGenerateImageNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const nWidget = node.widgets?.find(
|
|
(w) => w.name === 'n'
|
|
) as IComboWidget
|
|
if (!nWidget) return formatCreditsLabel(0.04, { suffix: ' x n/Run' })
|
|
|
|
const n = Number(nWidget.value) || 1
|
|
const cost = Number((0.04 * n).toFixed(2))
|
|
return formatCreditsLabel(cost)
|
|
}
|
|
},
|
|
RecraftGenerateVectorImageNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const nWidget = node.widgets?.find(
|
|
(w) => w.name === 'n'
|
|
) as IComboWidget
|
|
if (!nWidget) return formatCreditsLabel(0.08, { suffix: ' x n/Run' })
|
|
|
|
const n = Number(nWidget.value) || 1
|
|
const cost = Number((0.08 * n).toFixed(2))
|
|
return formatCreditsLabel(cost)
|
|
}
|
|
},
|
|
RecraftImageInpaintingNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const nWidget = node.widgets?.find(
|
|
(w) => w.name === 'n'
|
|
) as IComboWidget
|
|
if (!nWidget) return formatCreditsLabel(0.04, { suffix: ' x n/Run' })
|
|
|
|
const n = Number(nWidget.value) || 1
|
|
const cost = Number((0.04 * n).toFixed(2))
|
|
return formatCreditsLabel(cost)
|
|
}
|
|
},
|
|
RecraftImageToImageNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const nWidget = node.widgets?.find(
|
|
(w) => w.name === 'n'
|
|
) as IComboWidget
|
|
if (!nWidget) return formatCreditsLabel(0.04, { suffix: ' x n/Run' })
|
|
|
|
const n = Number(nWidget.value) || 1
|
|
const cost = Number((0.04 * n).toFixed(2))
|
|
return formatCreditsLabel(cost)
|
|
}
|
|
},
|
|
RecraftRemoveBackgroundNode: {
|
|
displayPrice: formatCreditsLabel(0.01)
|
|
},
|
|
RecraftReplaceBackgroundNode: {
|
|
displayPrice: formatCreditsLabel(0.04)
|
|
},
|
|
RecraftTextToImageNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const nWidget = node.widgets?.find(
|
|
(w) => w.name === 'n'
|
|
) as IComboWidget
|
|
if (!nWidget) return formatCreditsLabel(0.04, { suffix: ' x n/Run' })
|
|
|
|
const n = Number(nWidget.value) || 1
|
|
const cost = Number((0.04 * n).toFixed(2))
|
|
return formatCreditsLabel(cost)
|
|
}
|
|
},
|
|
RecraftTextToVectorNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const nWidget = node.widgets?.find(
|
|
(w) => w.name === 'n'
|
|
) as IComboWidget
|
|
if (!nWidget) return formatCreditsLabel(0.08, { suffix: ' x n/Run' })
|
|
|
|
const n = Number(nWidget.value) || 1
|
|
const cost = Number((0.08 * n).toFixed(2))
|
|
return formatCreditsLabel(cost)
|
|
}
|
|
},
|
|
RecraftVectorizeImageNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const nWidget = node.widgets?.find(
|
|
(w) => w.name === 'n'
|
|
) as IComboWidget
|
|
if (!nWidget) return formatCreditsLabel(0.01, { suffix: ' x n/Run' })
|
|
|
|
const n = Number(nWidget.value) || 1
|
|
const cost = Number((0.01 * n).toFixed(2))
|
|
return formatCreditsLabel(cost)
|
|
}
|
|
},
|
|
StabilityStableImageSD_3_5Node: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const modelWidget = node.widgets?.find(
|
|
(w) => w.name === 'model'
|
|
) as IComboWidget
|
|
|
|
if (!modelWidget)
|
|
return formatCreditsRangeLabel(0.035, 0.065, {
|
|
note: '(varies with model)'
|
|
})
|
|
|
|
const model = String(modelWidget.value).toLowerCase()
|
|
if (model.includes('large')) {
|
|
return formatCreditsLabel(0.065)
|
|
} else if (model.includes('medium')) {
|
|
return formatCreditsLabel(0.035)
|
|
}
|
|
|
|
return formatCreditsLabel(0.035)
|
|
}
|
|
},
|
|
StabilityStableImageUltraNode: {
|
|
displayPrice: formatCreditsLabel(0.08)
|
|
},
|
|
StabilityUpscaleConservativeNode: {
|
|
displayPrice: formatCreditsLabel(0.25)
|
|
},
|
|
StabilityUpscaleCreativeNode: {
|
|
displayPrice: formatCreditsLabel(0.25)
|
|
},
|
|
StabilityUpscaleFastNode: {
|
|
displayPrice: formatCreditsLabel(0.01)
|
|
},
|
|
StabilityTextToAudio: {
|
|
displayPrice: formatCreditsLabel(0.2)
|
|
},
|
|
StabilityAudioToAudio: {
|
|
displayPrice: formatCreditsLabel(0.2)
|
|
},
|
|
StabilityAudioInpaint: {
|
|
displayPrice: formatCreditsLabel(0.2)
|
|
},
|
|
VeoVideoGenerationNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const durationWidget = node.widgets?.find(
|
|
(w) => w.name === 'duration_seconds'
|
|
) as IComboWidget
|
|
|
|
if (!durationWidget)
|
|
return formatCreditsRangeLabel(2.5, 5.0, {
|
|
note: '(varies with duration)'
|
|
})
|
|
|
|
const price = 0.5 * Number(durationWidget.value)
|
|
return formatCreditsLabel(price)
|
|
}
|
|
},
|
|
Veo3VideoGenerationNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const modelWidget = node.widgets?.find(
|
|
(w) => w.name === 'model'
|
|
) as IComboWidget
|
|
const generateAudioWidget = node.widgets?.find(
|
|
(w) => w.name === 'generate_audio'
|
|
) as IComboWidget
|
|
|
|
if (!modelWidget || !generateAudioWidget) {
|
|
return formatCreditsRangeLabel(0.8, 3.2, {
|
|
note: '(varies with model & audio generation)'
|
|
})
|
|
}
|
|
|
|
const model = String(modelWidget.value)
|
|
const generateAudio =
|
|
String(generateAudioWidget.value).toLowerCase() === 'true'
|
|
|
|
if (
|
|
model.includes('veo-3.0-fast-generate-001') ||
|
|
model.includes('veo-3.1-fast-generate')
|
|
) {
|
|
return generateAudio
|
|
? formatCreditsLabel(1.2)
|
|
: formatCreditsLabel(0.8)
|
|
} else if (
|
|
model.includes('veo-3.0-generate-001') ||
|
|
model.includes('veo-3.1-generate')
|
|
) {
|
|
return generateAudio
|
|
? formatCreditsLabel(3.2)
|
|
: formatCreditsLabel(1.6)
|
|
}
|
|
|
|
// Default fallback
|
|
return formatCreditsRangeLabel(0.8, 3.2)
|
|
}
|
|
},
|
|
Veo3FirstLastFrameNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const modelWidget = node.widgets?.find(
|
|
(w) => w.name === 'model'
|
|
) as IComboWidget
|
|
const generateAudioWidget = node.widgets?.find(
|
|
(w) => w.name === 'generate_audio'
|
|
) as IComboWidget
|
|
const durationWidget = node.widgets?.find(
|
|
(w) => w.name === 'duration'
|
|
) as IComboWidget
|
|
|
|
if (!modelWidget || !generateAudioWidget || !durationWidget) {
|
|
return formatCreditsRangeLabel(0.4, 3.2, {
|
|
note: '(varies with model & audio generation)'
|
|
})
|
|
}
|
|
|
|
const model = String(modelWidget.value)
|
|
const generateAudio =
|
|
String(generateAudioWidget.value).toLowerCase() === 'true'
|
|
const seconds = parseFloat(String(durationWidget.value))
|
|
|
|
let pricePerSecond: number | null = null
|
|
if (model.includes('veo-3.1-fast-generate')) {
|
|
pricePerSecond = generateAudio ? 0.15 : 0.1
|
|
} else if (model.includes('veo-3.1-generate')) {
|
|
pricePerSecond = generateAudio ? 0.4 : 0.2
|
|
}
|
|
if (pricePerSecond === null) {
|
|
return formatCreditsRangeLabel(0.4, 3.2)
|
|
}
|
|
const cost = pricePerSecond * seconds
|
|
return formatCreditsLabel(cost)
|
|
}
|
|
},
|
|
LumaImageNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const modelWidget = node.widgets?.find(
|
|
(w) => w.name === 'model'
|
|
) as IComboWidget
|
|
const aspectRatioWidget = node.widgets?.find(
|
|
(w) => w.name === 'aspect_ratio'
|
|
) as IComboWidget
|
|
|
|
if (!modelWidget || !aspectRatioWidget) {
|
|
return formatCreditsRangeLabel(0.0064, 0.026, {
|
|
note: '(varies with model & aspect ratio)'
|
|
})
|
|
}
|
|
|
|
const model = String(modelWidget.value)
|
|
|
|
if (model.includes('photon-flash-1')) {
|
|
return formatCreditsLabel(0.0027)
|
|
} else if (model.includes('photon-1')) {
|
|
return formatCreditsLabel(0.0104)
|
|
}
|
|
|
|
return formatCreditsLabel(0.0246)
|
|
}
|
|
},
|
|
LumaImageModifyNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const modelWidget = node.widgets?.find(
|
|
(w) => w.name === 'model'
|
|
) as IComboWidget
|
|
|
|
if (!modelWidget) {
|
|
return formatCreditsRangeLabel(0.0027, 0.0104, {
|
|
note: '(varies with model)'
|
|
})
|
|
}
|
|
|
|
const model = String(modelWidget.value)
|
|
|
|
if (model.includes('photon-flash-1')) {
|
|
return formatCreditsLabel(0.0027)
|
|
} else if (model.includes('photon-1')) {
|
|
return formatCreditsLabel(0.0104)
|
|
}
|
|
|
|
return formatCreditsLabel(0.0246)
|
|
}
|
|
},
|
|
MoonvalleyTxt2VideoNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const lengthWidget = node.widgets?.find(
|
|
(w) => w.name === 'length'
|
|
) as IComboWidget
|
|
|
|
// If no length widget exists, default to 5s pricing
|
|
if (!lengthWidget) return formatCreditsLabel(1.5)
|
|
|
|
const length = String(lengthWidget.value)
|
|
if (length === '5s') {
|
|
return formatCreditsLabel(1.5)
|
|
} else if (length === '10s') {
|
|
return formatCreditsLabel(3.0)
|
|
}
|
|
|
|
return formatCreditsLabel(1.5)
|
|
}
|
|
},
|
|
MoonvalleyImg2VideoNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const lengthWidget = node.widgets?.find(
|
|
(w) => w.name === 'length'
|
|
) as IComboWidget
|
|
|
|
// If no length widget exists, default to 5s pricing
|
|
if (!lengthWidget) return formatCreditsLabel(1.5)
|
|
|
|
const length = String(lengthWidget.value)
|
|
if (length === '5s') {
|
|
return formatCreditsLabel(1.5)
|
|
} else if (length === '10s') {
|
|
return formatCreditsLabel(3.0)
|
|
}
|
|
|
|
return formatCreditsLabel(1.5)
|
|
}
|
|
},
|
|
MoonvalleyVideo2VideoNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const lengthWidget = node.widgets?.find(
|
|
(w) => w.name === 'length'
|
|
) as IComboWidget
|
|
|
|
// If no length widget exists, default to 5s pricing
|
|
if (!lengthWidget) return formatCreditsLabel(2.25)
|
|
|
|
const length = String(lengthWidget.value)
|
|
if (length === '5s') {
|
|
return formatCreditsLabel(2.25)
|
|
} else if (length === '10s') {
|
|
return formatCreditsLabel(4.0)
|
|
}
|
|
|
|
return formatCreditsLabel(2.25)
|
|
}
|
|
},
|
|
// Runway nodes - using actual node names from ComfyUI
|
|
RunwayTextToImageNode: {
|
|
displayPrice: formatCreditsLabel(0.11)
|
|
},
|
|
RunwayImageToVideoNodeGen3a: {
|
|
displayPrice: calculateRunwayDurationPrice
|
|
},
|
|
RunwayImageToVideoNodeGen4: {
|
|
displayPrice: calculateRunwayDurationPrice
|
|
},
|
|
RunwayFirstLastFrameNode: {
|
|
displayPrice: calculateRunwayDurationPrice
|
|
},
|
|
// Rodin nodes - all have the same pricing structure
|
|
Rodin3D_Regular: {
|
|
displayPrice: formatCreditsLabel(0.4)
|
|
},
|
|
Rodin3D_Detail: {
|
|
displayPrice: formatCreditsLabel(0.4)
|
|
},
|
|
Rodin3D_Smooth: {
|
|
displayPrice: formatCreditsLabel(0.4)
|
|
},
|
|
Rodin3D_Sketch: {
|
|
displayPrice: formatCreditsLabel(0.4)
|
|
},
|
|
// Tripo nodes - using actual node names from ComfyUI
|
|
TripoTextToModelNode: {
|
|
displayPrice: (node: LGraphNode): string =>
|
|
calculateTripo3DGenerationPrice(node, 'text')
|
|
},
|
|
TripoImageToModelNode: {
|
|
displayPrice: (node: LGraphNode): string =>
|
|
calculateTripo3DGenerationPrice(node, 'image')
|
|
},
|
|
TripoMultiviewToModelNode: {
|
|
displayPrice: (node: LGraphNode): string =>
|
|
calculateTripo3DGenerationPrice(node, 'multiview')
|
|
},
|
|
TripoTextureNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const textureQualityWidget = node.widgets?.find(
|
|
(w) => w.name === 'texture_quality'
|
|
) as IComboWidget
|
|
|
|
if (!textureQualityWidget)
|
|
return formatCreditsRangeLabel(0.1, 0.2, {
|
|
note: '(varies with quality)'
|
|
})
|
|
|
|
const textureQuality = String(textureQualityWidget.value)
|
|
return textureQuality.includes('detailed')
|
|
? formatCreditsLabel(0.2)
|
|
: formatCreditsLabel(0.1)
|
|
}
|
|
},
|
|
TripoRigNode: {
|
|
displayPrice: '$0.25/Run'
|
|
},
|
|
TripoConversionNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const getWidgetValue = (name: string) =>
|
|
node.widgets?.find((w) => w.name === name)?.value
|
|
|
|
const getNumber = (name: string, defaultValue: number): number => {
|
|
const raw = getWidgetValue(name)
|
|
if (raw === undefined || raw === null || raw === '')
|
|
return defaultValue
|
|
if (typeof raw === 'number')
|
|
return Number.isFinite(raw) ? raw : defaultValue
|
|
const n = Number(raw)
|
|
return Number.isFinite(n) ? n : defaultValue
|
|
}
|
|
|
|
const getBool = (name: string, defaultValue: boolean): boolean => {
|
|
const v = getWidgetValue(name)
|
|
if (v === undefined || v === null) return defaultValue
|
|
|
|
if (typeof v === 'number') return v !== 0
|
|
const lower = String(v).toLowerCase()
|
|
if (lower === 'true') return true
|
|
if (lower === 'false') return false
|
|
return defaultValue
|
|
}
|
|
|
|
let hasAdvancedParam = false
|
|
|
|
// ---- booleans that trigger advanced when true ----
|
|
if (getBool('quad', false)) hasAdvancedParam = true
|
|
if (getBool('force_symmetry', false)) hasAdvancedParam = true
|
|
if (getBool('flatten_bottom', false)) hasAdvancedParam = true
|
|
if (getBool('pivot_to_center_bottom', false)) hasAdvancedParam = true
|
|
if (getBool('with_animation', false)) hasAdvancedParam = true
|
|
if (getBool('pack_uv', false)) hasAdvancedParam = true
|
|
if (getBool('bake', false)) hasAdvancedParam = true
|
|
if (getBool('export_vertex_colors', false)) hasAdvancedParam = true
|
|
if (getBool('animate_in_place', false)) hasAdvancedParam = true
|
|
|
|
// ---- numeric params with special default sentinels ----
|
|
const faceLimit = getNumber('face_limit', -1)
|
|
if (faceLimit !== -1) hasAdvancedParam = true
|
|
|
|
const textureSize = getNumber('texture_size', 4096)
|
|
if (textureSize !== 4096) hasAdvancedParam = true
|
|
|
|
const flattenBottomThreshold = getNumber(
|
|
'flatten_bottom_threshold',
|
|
0.0
|
|
)
|
|
if (flattenBottomThreshold !== 0.0) hasAdvancedParam = true
|
|
|
|
const scaleFactor = getNumber('scale_factor', 1.0)
|
|
if (scaleFactor !== 1.0) hasAdvancedParam = true
|
|
|
|
// ---- string / combo params with non-default values ----
|
|
const textureFormatRaw = String(
|
|
getWidgetValue('texture_format') ?? 'JPEG'
|
|
).toUpperCase()
|
|
if (textureFormatRaw !== 'JPEG') hasAdvancedParam = true
|
|
|
|
const partNamesRaw = String(getWidgetValue('part_names') ?? '')
|
|
if (partNamesRaw.trim().length > 0) hasAdvancedParam = true
|
|
|
|
const fbxPresetRaw = String(
|
|
getWidgetValue('fbx_preset') ?? 'blender'
|
|
).toLowerCase()
|
|
if (fbxPresetRaw !== 'blender') hasAdvancedParam = true
|
|
|
|
const exportOrientationRaw = String(
|
|
getWidgetValue('export_orientation') ?? 'default'
|
|
).toLowerCase()
|
|
if (exportOrientationRaw !== 'default') hasAdvancedParam = true
|
|
|
|
const credits = hasAdvancedParam ? 10 : 5
|
|
return formatCreditsLabel(credits * 0.01)
|
|
}
|
|
},
|
|
TripoRetargetNode: {
|
|
displayPrice: formatCreditsLabel(0.1)
|
|
},
|
|
TripoRefineNode: {
|
|
displayPrice: formatCreditsLabel(0.3)
|
|
},
|
|
// Google/Gemini nodes
|
|
GeminiNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const modelWidget = node.widgets?.find(
|
|
(w) => w.name === 'model'
|
|
) as IComboWidget
|
|
|
|
if (!modelWidget) return 'Token-based'
|
|
|
|
const model = String(modelWidget.value)
|
|
|
|
// Google Veo video generation
|
|
if (model.includes('veo-2.0')) {
|
|
return formatCreditsLabel(0.5, { suffix: '/second' })
|
|
} else if (model.includes('gemini-2.5-flash-preview-04-17')) {
|
|
return formatCreditsListLabel([0.0003, 0.0025], {
|
|
suffix: ' per 1K tokens'
|
|
})
|
|
} else if (model.includes('gemini-2.5-flash')) {
|
|
return formatCreditsListLabel([0.0003, 0.0025], {
|
|
suffix: ' per 1K tokens'
|
|
})
|
|
} else if (model.includes('gemini-2.5-pro-preview-05-06')) {
|
|
return formatCreditsListLabel([0.00125, 0.01], {
|
|
suffix: ' per 1K tokens'
|
|
})
|
|
} else if (model.includes('gemini-2.5-pro')) {
|
|
return formatCreditsListLabel([0.00125, 0.01], {
|
|
suffix: ' per 1K tokens'
|
|
})
|
|
} else if (model.includes('gemini-3-pro-preview')) {
|
|
return formatCreditsListLabel([0.002, 0.012], {
|
|
suffix: ' per 1K tokens'
|
|
})
|
|
}
|
|
// For other Gemini models, show token-based pricing info
|
|
return 'Token-based'
|
|
}
|
|
},
|
|
GeminiImageNode: {
|
|
displayPrice: formatCreditsLabel(0.039, {
|
|
suffix: '/Image (1K)',
|
|
approximate: true
|
|
})
|
|
},
|
|
GeminiImage2Node: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const resolutionWidget = node.widgets?.find(
|
|
(w) => w.name === 'resolution'
|
|
) as IComboWidget
|
|
|
|
if (!resolutionWidget) return 'Token-based'
|
|
|
|
const resolution = String(resolutionWidget.value)
|
|
if (resolution.includes('1K')) {
|
|
return formatCreditsLabel(0.134, {
|
|
suffix: '/Image',
|
|
approximate: true
|
|
})
|
|
} else if (resolution.includes('2K')) {
|
|
return formatCreditsLabel(0.134, {
|
|
suffix: '/Image',
|
|
approximate: true
|
|
})
|
|
} else if (resolution.includes('4K')) {
|
|
return formatCreditsLabel(0.24, {
|
|
suffix: '/Image',
|
|
approximate: true
|
|
})
|
|
}
|
|
return 'Token-based'
|
|
}
|
|
},
|
|
// OpenAI nodes
|
|
OpenAIChatNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const modelWidget = node.widgets?.find(
|
|
(w) => w.name === 'model'
|
|
) as IComboWidget
|
|
|
|
if (!modelWidget) return 'Token-based'
|
|
|
|
const model = String(modelWidget.value)
|
|
|
|
// Specific pricing for exposed models based on official pricing data (converted to per 1K tokens)
|
|
if (model.includes('o4-mini')) {
|
|
return formatCreditsListLabel([0.0011, 0.0044], {
|
|
suffix: ' per 1K tokens'
|
|
})
|
|
} else if (model.includes('o1-pro')) {
|
|
return formatCreditsListLabel([0.15, 0.6], {
|
|
suffix: ' per 1K tokens'
|
|
})
|
|
} else if (model.includes('o1')) {
|
|
return formatCreditsListLabel([0.015, 0.06], {
|
|
suffix: ' per 1K tokens'
|
|
})
|
|
} else if (model.includes('o3-mini')) {
|
|
return formatCreditsListLabel([0.0011, 0.0044], {
|
|
suffix: ' per 1K tokens'
|
|
})
|
|
} else if (model.includes('o3')) {
|
|
return formatCreditsListLabel([0.01, 0.04], {
|
|
suffix: ' per 1K tokens'
|
|
})
|
|
} else if (model.includes('gpt-4o')) {
|
|
return formatCreditsListLabel([0.0025, 0.01], {
|
|
suffix: ' per 1K tokens'
|
|
})
|
|
} else if (model.includes('gpt-4.1-nano')) {
|
|
return formatCreditsListLabel([0.0001, 0.0004], {
|
|
suffix: ' per 1K tokens'
|
|
})
|
|
} else if (model.includes('gpt-4.1-mini')) {
|
|
return formatCreditsListLabel([0.0004, 0.0016], {
|
|
suffix: ' per 1K tokens'
|
|
})
|
|
} else if (model.includes('gpt-4.1')) {
|
|
return formatCreditsListLabel([0.002, 0.008], {
|
|
suffix: ' per 1K tokens'
|
|
})
|
|
} else if (model.includes('gpt-5-nano')) {
|
|
return formatCreditsListLabel([0.00005, 0.0004], {
|
|
suffix: ' per 1K tokens'
|
|
})
|
|
} else if (model.includes('gpt-5-mini')) {
|
|
return formatCreditsListLabel([0.00025, 0.002], {
|
|
suffix: ' per 1K tokens'
|
|
})
|
|
} else if (model.includes('gpt-5')) {
|
|
return formatCreditsListLabel([0.00125, 0.01], {
|
|
suffix: ' per 1K tokens'
|
|
})
|
|
}
|
|
return 'Token-based'
|
|
}
|
|
},
|
|
ViduTextToVideoNode: {
|
|
displayPrice: formatCreditsLabel(0.4)
|
|
},
|
|
ViduImageToVideoNode: {
|
|
displayPrice: formatCreditsLabel(0.4)
|
|
},
|
|
ViduReferenceVideoNode: {
|
|
displayPrice: formatCreditsLabel(0.4)
|
|
},
|
|
ViduStartEndToVideoNode: {
|
|
displayPrice: formatCreditsLabel(0.4)
|
|
},
|
|
ByteDanceImageNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const modelWidget = node.widgets?.find(
|
|
(w) => w.name === 'model'
|
|
) as IComboWidget
|
|
|
|
if (!modelWidget) return 'Token-based'
|
|
|
|
const model = String(modelWidget.value)
|
|
|
|
if (model.includes('seedream-3-0-t2i')) {
|
|
return formatCreditsLabel(0.03)
|
|
}
|
|
return 'Token-based'
|
|
}
|
|
},
|
|
ByteDanceImageEditNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const modelWidget = node.widgets?.find(
|
|
(w) => w.name === 'model'
|
|
) as IComboWidget
|
|
|
|
if (!modelWidget) return 'Token-based'
|
|
|
|
const model = String(modelWidget.value)
|
|
|
|
if (model.includes('seededit-3-0-i2i')) {
|
|
return formatCreditsLabel(0.03)
|
|
}
|
|
return 'Token-based'
|
|
}
|
|
},
|
|
ByteDanceSeedreamNode: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const modelWidget = node.widgets?.find(
|
|
(w) => w.name === 'model'
|
|
) as IComboWidget
|
|
|
|
const model = String(modelWidget?.value ?? '').toLowerCase()
|
|
let pricePerImage = 0.03 // default for seedream-4-0-250828 and fallback
|
|
if (model.includes('seedream-4-5-251128')) {
|
|
pricePerImage = 0.04
|
|
} else if (model.includes('seedream-4-0-250828')) {
|
|
pricePerImage = 0.03
|
|
}
|
|
return formatCreditsLabel(pricePerImage, {
|
|
suffix: ' x images/Run',
|
|
approximate: true
|
|
})
|
|
}
|
|
},
|
|
ByteDanceTextToVideoNode: {
|
|
displayPrice: byteDanceVideoPricingCalculator
|
|
},
|
|
ByteDanceImageToVideoNode: {
|
|
displayPrice: byteDanceVideoPricingCalculator
|
|
},
|
|
ByteDanceFirstLastFrameNode: {
|
|
displayPrice: byteDanceVideoPricingCalculator
|
|
},
|
|
ByteDanceImageReferenceNode: {
|
|
displayPrice: byteDanceVideoPricingCalculator
|
|
},
|
|
WanTextToVideoApi: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const durationWidget = node.widgets?.find(
|
|
(w) => w.name === 'duration'
|
|
) as IComboWidget
|
|
const resolutionWidget = node.widgets?.find(
|
|
(w) => w.name === 'size'
|
|
) as IComboWidget
|
|
|
|
if (!durationWidget || !resolutionWidget)
|
|
return formatCreditsRangeLabel(0.05, 0.15, { suffix: '/second' })
|
|
|
|
const seconds = parseFloat(String(durationWidget.value))
|
|
const resolutionStr = String(resolutionWidget.value).toLowerCase()
|
|
|
|
const resKey = resolutionStr.includes('1080')
|
|
? '1080p'
|
|
: resolutionStr.includes('720')
|
|
? '720p'
|
|
: resolutionStr.includes('480')
|
|
? '480p'
|
|
: (resolutionStr.match(/^\s*(\d{3,4}p)/)?.[1] ?? '')
|
|
|
|
const pricePerSecond: Record<string, number> = {
|
|
'480p': 0.05,
|
|
'720p': 0.1,
|
|
'1080p': 0.15
|
|
}
|
|
|
|
const pps = pricePerSecond[resKey]
|
|
if (isNaN(seconds) || !pps)
|
|
return formatCreditsRangeLabel(0.05, 0.15, { suffix: '/second' })
|
|
|
|
const cost = Number((pps * seconds).toFixed(2))
|
|
return formatCreditsLabel(cost)
|
|
}
|
|
},
|
|
WanImageToVideoApi: {
|
|
displayPrice: (node: LGraphNode): string => {
|
|
const durationWidget = node.widgets?.find(
|
|
(w) => w.name === 'duration'
|
|
) as IComboWidget
|
|
const resolutionWidget = node.widgets?.find(
|
|
(w) => w.name === 'resolution'
|
|
) as IComboWidget
|
|
|
|
if (!durationWidget || !resolutionWidget)
|
|
return formatCreditsRangeLabel(0.05, 0.15, { suffix: '/second' })
|
|
|
|
const seconds = parseFloat(String(durationWidget.value))
|
|
const resolution = String(resolutionWidget.value).trim().toLowerCase()
|
|
|
|
const pricePerSecond: Record<string, number> = {
|
|
'480p': 0.05,
|
|
'720p': 0.1,
|
|
'1080p': 0.15
|
|
}
|
|
|
|
const pps = pricePerSecond[resolution]
|
|
if (isNaN(seconds) || !pps)
|
|
return formatCreditsRangeLabel(0.05, 0.15, { suffix: '/second' })
|
|
|
|
const cost = Number((pps * seconds).toFixed(2))
|
|
return formatCreditsLabel(cost)
|
|
}
|
|
},
|
|
WanTextToImageApi: {
|
|
displayPrice: formatCreditsLabel(0.03)
|
|
},
|
|
WanImageToImageApi: {
|
|
displayPrice: formatCreditsLabel(0.03)
|
|
},
|
|
LtxvApiTextToVideo: {
|
|
displayPrice: ltxvPricingCalculator
|
|
},
|
|
LtxvApiImageToVideo: {
|
|
displayPrice: ltxvPricingCalculator
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Composable to get node pricing information for API nodes
|
|
*/
|
|
export const useNodePricing = () => {
|
|
/**
|
|
* Get the price display for a node
|
|
*/
|
|
const getNodeDisplayPrice = (node: LGraphNode): string => {
|
|
if (!node.constructor?.nodeData?.api_node) return ''
|
|
|
|
const nodeName = node.constructor.nodeData.name
|
|
const priceConfig = apiNodeCosts[nodeName]
|
|
|
|
if (!priceConfig) return ''
|
|
|
|
// If it's a function, call it with the node to get dynamic pricing
|
|
if (typeof priceConfig.displayPrice === 'function') {
|
|
return safePricingExecution(priceConfig.displayPrice, node, '')
|
|
}
|
|
|
|
// Otherwise return the static price
|
|
return priceConfig.displayPrice
|
|
}
|
|
|
|
const getNodePricingConfig = (node: LGraphNode) =>
|
|
apiNodeCosts[node.constructor.nodeData?.name ?? '']
|
|
|
|
const getRelevantWidgetNames = (nodeType: string): string[] => {
|
|
const widgetMap: Record<string, string[]> = {
|
|
KlingTextToVideoNode: ['mode', 'model_name', 'duration'],
|
|
KlingImage2VideoNode: ['mode', 'model_name', 'duration'],
|
|
KlingImageGenerationNode: ['modality', 'model_name', 'n'],
|
|
KlingDualCharacterVideoEffectNode: ['mode', 'model_name', 'duration'],
|
|
KlingSingleImageVideoEffectNode: ['effect_scene'],
|
|
KlingStartEndFrameNode: ['mode', 'model_name', 'duration'],
|
|
KlingTextToVideoWithAudio: ['duration', 'generate_audio'],
|
|
KlingImageToVideoWithAudio: ['duration', 'generate_audio'],
|
|
KlingOmniProTextToVideoNode: ['duration'],
|
|
KlingOmniProFirstLastFrameNode: ['duration'],
|
|
KlingOmniProImageToVideoNode: ['duration'],
|
|
KlingOmniProVideoToVideoNode: ['duration'],
|
|
KlingMotionControl: ['mode'],
|
|
MinimaxHailuoVideoNode: ['resolution', 'duration'],
|
|
OpenAIDalle3: ['size', 'quality'],
|
|
OpenAIDalle2: ['size', 'n'],
|
|
OpenAIVideoSora2: ['model', 'size', 'duration'],
|
|
OpenAIGPTImage1: ['quality', 'n'],
|
|
IdeogramV1: ['num_images', 'turbo'],
|
|
IdeogramV2: ['num_images', 'turbo'],
|
|
IdeogramV3: ['rendering_speed', 'num_images', 'character_image'],
|
|
FluxProKontextProNode: [],
|
|
FluxProKontextMaxNode: [],
|
|
Flux2ProImageNode: ['width', 'height', 'images'],
|
|
Flux2MaxImageNode: ['width', 'height', 'images'],
|
|
VeoVideoGenerationNode: ['duration_seconds'],
|
|
Veo3VideoGenerationNode: ['model', 'generate_audio'],
|
|
Veo3FirstLastFrameNode: ['model', 'generate_audio', 'duration'],
|
|
LumaVideoNode: ['model', 'resolution', 'duration'],
|
|
LumaImageToVideoNode: ['model', 'resolution', 'duration'],
|
|
LumaImageNode: ['model', 'aspect_ratio'],
|
|
LumaImageModifyNode: ['model', 'aspect_ratio'],
|
|
PixverseTextToVideoNode: ['duration_seconds', 'quality', 'motion_mode'],
|
|
PixverseTransitionVideoNode: [
|
|
'duration_seconds',
|
|
'motion_mode',
|
|
'quality'
|
|
],
|
|
PixverseImageToVideoNode: ['duration_seconds', 'quality', 'motion_mode'],
|
|
StabilityStableImageSD_3_5Node: ['model'],
|
|
RecraftTextToImageNode: ['n'],
|
|
RecraftImageToImageNode: ['n'],
|
|
RecraftImageInpaintingNode: ['n'],
|
|
RecraftTextToVectorNode: ['n'],
|
|
RecraftVectorizeImageNode: ['n'],
|
|
RecraftGenerateColorFromImageNode: ['n'],
|
|
RecraftGenerateImageNode: ['n'],
|
|
RecraftGenerateVectorImageNode: ['n'],
|
|
MoonvalleyTxt2VideoNode: ['length'],
|
|
MoonvalleyImg2VideoNode: ['length'],
|
|
MoonvalleyVideo2VideoNode: ['length'],
|
|
// Runway nodes
|
|
RunwayImageToVideoNodeGen3a: ['duration'],
|
|
RunwayImageToVideoNodeGen4: ['duration'],
|
|
RunwayFirstLastFrameNode: ['duration'],
|
|
// Tripo nodes
|
|
TripoTextToModelNode: [
|
|
'model_version',
|
|
'quad',
|
|
'style',
|
|
'texture',
|
|
'pbr',
|
|
'texture_quality',
|
|
'geometry_quality'
|
|
],
|
|
TripoImageToModelNode: [
|
|
'model_version',
|
|
'quad',
|
|
'style',
|
|
'texture',
|
|
'pbr',
|
|
'texture_quality',
|
|
'geometry_quality'
|
|
],
|
|
TripoMultiviewToModelNode: [
|
|
'model_version',
|
|
'quad',
|
|
'texture',
|
|
'pbr',
|
|
'texture_quality',
|
|
'geometry_quality'
|
|
],
|
|
TripoConversionNode: [
|
|
'quad',
|
|
'face_limit',
|
|
'texture_size',
|
|
'texture_format',
|
|
'force_symmetry',
|
|
'flatten_bottom',
|
|
'flatten_bottom_threshold',
|
|
'pivot_to_center_bottom',
|
|
'scale_factor',
|
|
'with_animation',
|
|
'pack_uv',
|
|
'bake',
|
|
'part_names',
|
|
'fbx_preset',
|
|
'export_vertex_colors',
|
|
'export_orientation',
|
|
'animate_in_place'
|
|
],
|
|
TripoTextureNode: ['texture_quality'],
|
|
// Google/Gemini nodes
|
|
GeminiNode: ['model'],
|
|
GeminiImage2Node: ['resolution'],
|
|
// OpenAI nodes
|
|
OpenAIChatNode: ['model'],
|
|
// ByteDance
|
|
ByteDanceImageNode: ['model'],
|
|
ByteDanceImageEditNode: ['model'],
|
|
ByteDanceSeedreamNode: [
|
|
'model',
|
|
'sequential_image_generation',
|
|
'max_images'
|
|
],
|
|
ByteDanceTextToVideoNode: ['model', 'duration', 'resolution'],
|
|
ByteDanceImageToVideoNode: ['model', 'duration', 'resolution'],
|
|
ByteDanceFirstLastFrameNode: ['model', 'duration', 'resolution'],
|
|
ByteDanceImageReferenceNode: ['model', 'duration', 'resolution'],
|
|
WanTextToVideoApi: ['duration', 'size'],
|
|
WanImageToVideoApi: ['duration', 'resolution'],
|
|
LtxvApiTextToVideo: ['model', 'duration', 'resolution'],
|
|
LtxvApiImageToVideo: ['model', 'duration', 'resolution']
|
|
}
|
|
return widgetMap[nodeType] || []
|
|
}
|
|
|
|
return {
|
|
getNodeDisplayPrice,
|
|
getNodePricingConfig,
|
|
getRelevantWidgetNames
|
|
}
|
|
}
|