diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts index 27355a2205..b2ad2ff995 100644 --- a/src/composables/node/useNodePricing.ts +++ b/src/composables/node/useNodePricing.ts @@ -109,6 +109,66 @@ const pixversePricingCalculator = (node: LGraphNode): string => { return '$0.9/Run' } +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> = { + 'seedance-1-0-pro': { + '480p': [0.23, 0.24], + '720p': [0.51, 0.56], + '1080p': [1.18, 1.22] + }, + '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') + ? '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 + + const minStr = `$${minCost.toFixed(2)}/Run` + const maxStr = `$${maxCost.toFixed(2)}/Run` + + return minStr === maxStr + ? minStr + : `$${minCost.toFixed(2)}-$${maxCost.toFixed(2)}/Run` +} + /** * Static pricing data for API nodes, now supporting both strings and functions */ @@ -1441,6 +1501,18 @@ const apiNodeCosts: Record = } return 'Token-based' } + }, + ByteDanceTextToVideoNode: { + displayPrice: byteDanceVideoPricingCalculator + }, + ByteDanceImageToVideoNode: { + displayPrice: byteDanceVideoPricingCalculator + }, + ByteDanceFirstLastFrameNode: { + displayPrice: byteDanceVideoPricingCalculator + }, + ByteDanceImageReferenceNode: { + displayPrice: byteDanceVideoPricingCalculator } } @@ -1531,7 +1603,11 @@ export const useNodePricing = () => { OpenAIChatNode: ['model'], // ByteDance ByteDanceImageNode: ['model'], - ByteDanceImageEditNode: ['model'] + ByteDanceImageEditNode: ['model'], + ByteDanceTextToVideoNode: ['model', 'duration', 'resolution'], + ByteDanceImageToVideoNode: ['model', 'duration', 'resolution'], + ByteDanceFirstLastFrameNode: ['model', 'duration', 'resolution'], + ByteDanceImageReferenceNode: ['model', 'duration', 'resolution'] } return widgetMap[nodeType] || [] } diff --git a/tests-ui/tests/composables/node/useNodePricing.test.ts b/tests-ui/tests/composables/node/useNodePricing.test.ts index f6f45e6789..dbd40a7311 100644 --- a/tests-ui/tests/composables/node/useNodePricing.test.ts +++ b/tests-ui/tests/composables/node/useNodePricing.test.ts @@ -1780,4 +1780,86 @@ describe('useNodePricing', () => { }) }) }) + + describe('dynamic pricing - ByteDance Seedance video nodes', () => { + it('should return base 10s range for PRO 1080p on ByteDanceTextToVideoNode', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('ByteDanceTextToVideoNode', [ + { name: 'model', value: 'seedance-1-0-pro' }, + { name: 'duration', value: '10' }, + { name: 'resolution', value: '1080p' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$1.18-$1.22/Run') + }) + + it('should scale to half for 5s PRO 1080p on ByteDanceTextToVideoNode', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('ByteDanceTextToVideoNode', [ + { name: 'model', value: 'seedance-1-0-pro' }, + { name: 'duration', value: '5' }, + { name: 'resolution', value: '1080p' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.59-$0.61/Run') + }) + + it('should scale for 8s PRO 480p on ByteDanceImageToVideoNode', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('ByteDanceImageToVideoNode', [ + { name: 'model', value: 'seedance-1-0-pro' }, + { name: 'duration', value: '8' }, + { name: 'resolution', value: '480p' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.18-$0.19/Run') + }) + + it('should scale correctly for 12s PRO 720p on ByteDanceFirstLastFrameNode', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('ByteDanceFirstLastFrameNode', [ + { name: 'model', value: 'seedance-1-0-pro' }, + { name: 'duration', value: '12' }, + { name: 'resolution', value: '720p' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.61-$0.67/Run') + }) + + it('should collapse to a single value when min and max round equal for LITE 480p 3s on ByteDanceImageReferenceNode', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('ByteDanceImageReferenceNode', [ + { name: 'model', value: 'seedance-1-0-lite' }, + { name: 'duration', value: '3' }, + { name: 'resolution', value: '480p' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$0.05/Run') // 0.17..0.18 scaled by 0.3 both round to 0.05 + }) + + it('should return Token-based when required widgets are missing', () => { + const { getNodeDisplayPrice } = useNodePricing() + const missingModel = createMockNode('ByteDanceFirstLastFrameNode', [ + { name: 'duration', value: '10' }, + { name: 'resolution', value: '1080p' } + ]) + const missingResolution = createMockNode('ByteDanceImageToVideoNode', [ + { name: 'model', value: 'seedance-1-0-pro' }, + { name: 'duration', value: '10' } + ]) + const missingDuration = createMockNode('ByteDanceTextToVideoNode', [ + { name: 'model', value: 'seedance-1-0-lite' }, + { name: 'resolution', value: '720p' } + ]) + + expect(getNodeDisplayPrice(missingModel)).toBe('Token-based') + expect(getNodeDisplayPrice(missingResolution)).toBe('Token-based') + expect(getNodeDisplayPrice(missingDuration)).toBe('Token-based') + }) + }) })