From f843d779c294b4f343cbf494c89e0729181b9e9a Mon Sep 17 00:00:00 2001
From: Alexander Piskun <13381981+bigcat88@users.noreply.github.com>
Date: Sat, 10 Jan 2026 20:51:16 +0200
Subject: [PATCH] feat(price-badges): add price badges for Vidu2 nodes (#7927)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Price badges for the new nodes.
## Screenshots (if applicable)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7927-feat-price-badges-add-price-badges-for-Vidu2-nodes-2e36d73d365081a0b6f4de768dce11e4)
by [Unito](https://www.unito.io)
---
src/composables/node/useNodePricing.ts | 238 ++++++++++++++++++++++++-
1 file changed, 237 insertions(+), 1 deletion(-)
diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts
index 641d38911..643dd676b 100644
--- a/src/composables/node/useNodePricing.ts
+++ b/src/composables/node/useNodePricing.ts
@@ -2161,6 +2161,238 @@ const apiNodeCosts: Record =
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] || []
}