[backport cloud/1.36] feat(price-badges): add price badges for Vidu2 nodes (#8017)

Backport of #7927 to `cloud/1.36`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8017-backport-cloud-1-36-feat-price-badges-add-price-badges-for-Vidu2-nodes-2e76d73d3650810ea3b1d37744fe0523)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Piskun <13381981+bigcat88@users.noreply.github.com>
This commit is contained in:
Comfy Org PR Bot
2026-01-13 14:29:36 +09:00
committed by GitHub
parent eeb0fd354a
commit fca4931f52

View File

@@ -2161,6 +2161,238 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
return formatCreditsRangeLabel(minTotal, maxTotal)
}
},
Vidu2TextToVideoNode: {
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.075, 0.6, {
note: '(varies with duration & resolution)'
})
}
const duration = parseFloat(String(durationWidget.value))
const resolution = String(resolutionWidget.value).toLowerCase()
// Text-to-Video uses Q2 model only
// 720P: Starts at $0.075, +$0.025/sec
// 1080P: Starts at $0.10, +$0.05/sec
let basePrice: number
let pricePerSecond: number
if (resolution.includes('1080')) {
basePrice = 0.1
pricePerSecond = 0.05
} else {
// 720P default
basePrice = 0.075
pricePerSecond = 0.025
}
if (!Number.isFinite(duration) || duration <= 0) {
return formatCreditsRangeLabel(0.075, 0.6, {
note: '(varies with duration & resolution)'
})
}
const cost = basePrice + pricePerSecond * (duration - 1)
return formatCreditsLabel(cost)
}
},
Vidu2ImageToVideoNode: {
displayPrice: (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 formatCreditsRangeLabel(0.04, 1.0, {
note: '(varies with model, duration & resolution)'
})
}
const model = String(modelWidget.value).toLowerCase()
const duration = parseFloat(String(durationWidget.value))
const resolution = String(resolutionWidget.value).toLowerCase()
const is1080p = resolution.includes('1080')
let basePrice: number
let pricePerSecond: number
if (model.includes('q2-pro-fast')) {
// Q2-pro-fast: 720P $0.04+$0.01/sec, 1080P $0.08+$0.02/sec
basePrice = is1080p ? 0.08 : 0.04
pricePerSecond = is1080p ? 0.02 : 0.01
} else if (model.includes('q2-pro')) {
// Q2-pro: 720P $0.075+$0.05/sec, 1080P $0.275+$0.075/sec
basePrice = is1080p ? 0.275 : 0.075
pricePerSecond = is1080p ? 0.075 : 0.05
} else if (model.includes('q2-turbo')) {
// Q2-turbo: 720P special pricing, 1080P $0.175+$0.05/sec
if (is1080p) {
basePrice = 0.175
pricePerSecond = 0.05
} else {
// 720P: $0.04 at 1s, $0.05 at 2s, +$0.05/sec beyond 2s
if (duration <= 1) {
return formatCreditsLabel(0.04)
}
if (duration <= 2) {
return formatCreditsLabel(0.05)
}
const cost = 0.05 + 0.05 * (duration - 2)
return formatCreditsLabel(cost)
}
} else {
return formatCreditsRangeLabel(0.04, 1.0, {
note: '(varies with model, duration & resolution)'
})
}
if (!Number.isFinite(duration) || duration <= 0) {
return formatCreditsRangeLabel(0.04, 1.0, {
note: '(varies with model, duration & resolution)'
})
}
const cost = basePrice + pricePerSecond * (duration - 1)
return formatCreditsLabel(cost)
}
},
Vidu2ReferenceVideoNode: {
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
const audioWidget = node.widgets?.find(
(w) => w.name === 'audio'
) as IComboWidget
if (!durationWidget) {
return formatCreditsRangeLabel(0.125, 1.5, {
note: '(varies with duration, resolution & audio)'
})
}
const duration = parseFloat(String(durationWidget.value))
const resolution = String(resolutionWidget?.value ?? '').toLowerCase()
const is1080p = resolution.includes('1080')
// Check if audio is enabled (adds $0.75)
const audioValue = audioWidget?.value
const hasAudio =
audioValue !== undefined &&
audioValue !== null &&
String(audioValue).toLowerCase() !== 'false' &&
String(audioValue).toLowerCase() !== 'none' &&
audioValue !== ''
// Reference-to-Video uses Q2 model
// 720P: Starts at $0.125, +$0.025/sec
// 1080P: Starts at $0.375, +$0.05/sec
let basePrice: number
let pricePerSecond: number
if (is1080p) {
basePrice = 0.375
pricePerSecond = 0.05
} else {
// 720P default
basePrice = 0.125
pricePerSecond = 0.025
}
let cost = basePrice
if (Number.isFinite(duration) && duration > 0) {
cost = basePrice + pricePerSecond * (duration - 1)
}
// Audio adds $0.75 on top
if (hasAudio) {
cost += 0.075
}
return formatCreditsLabel(cost)
}
},
Vidu2StartEndToVideoNode: {
displayPrice: (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 formatCreditsRangeLabel(0.04, 1.0, {
note: '(varies with model, duration & resolution)'
})
}
const model = String(modelWidget.value).toLowerCase()
const duration = parseFloat(String(durationWidget.value))
const resolution = String(resolutionWidget.value).toLowerCase()
const is1080p = resolution.includes('1080')
let basePrice: number
let pricePerSecond: number
if (model.includes('q2-pro-fast')) {
// Q2-pro-fast: 720P $0.04+$0.01/sec, 1080P $0.08+$0.02/sec
basePrice = is1080p ? 0.08 : 0.04
pricePerSecond = is1080p ? 0.02 : 0.01
} else if (model.includes('q2-pro')) {
// Q2-pro: 720P $0.075+$0.05/sec, 1080P $0.275+$0.075/sec
basePrice = is1080p ? 0.275 : 0.075
pricePerSecond = is1080p ? 0.075 : 0.05
} else if (model.includes('q2-turbo')) {
// Q2-turbo: 720P special pricing, 1080P $0.175+$0.05/sec
if (is1080p) {
basePrice = 0.175
pricePerSecond = 0.05
} else {
// 720P: $0.04 at 1s, $0.05 at 2s, +$0.05/sec beyond 2s
if (!Number.isFinite(duration) || duration <= 1) {
return formatCreditsLabel(0.04)
}
if (duration <= 2) {
return formatCreditsLabel(0.05)
}
const cost = 0.05 + 0.05 * (duration - 2)
return formatCreditsLabel(cost)
}
} else {
return formatCreditsRangeLabel(0.04, 1.0, {
note: '(varies with model, duration & resolution)'
})
}
if (!Number.isFinite(duration) || duration <= 0) {
return formatCreditsLabel(basePrice)
}
const cost = basePrice + pricePerSecond * (duration - 1)
return formatCreditsLabel(cost)
}
}
}
@@ -2316,7 +2548,11 @@ export const useNodePricing = () => {
WanImageToVideoApi: ['duration', 'resolution'],
WanReferenceVideoApi: ['duration', 'size'],
LtxvApiTextToVideo: ['model', 'duration', 'resolution'],
LtxvApiImageToVideo: ['model', 'duration', 'resolution']
LtxvApiImageToVideo: ['model', 'duration', 'resolution'],
Vidu2TextToVideoNode: ['model', 'duration', 'resolution'],
Vidu2ImageToVideoNode: ['model', 'duration', 'resolution'],
Vidu2ReferenceVideoNode: ['audio', 'duration', 'resolution'],
Vidu2StartEndToVideoNode: ['model', 'duration', 'resolution']
}
return widgetMap[nodeType] || []
}