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] || [] }