merge main into rh-test

This commit is contained in:
bymyself
2025-09-28 15:33:29 -07:00
parent 1c0f151d02
commit ff0c15b119
1317 changed files with 85439 additions and 18373 deletions

View File

@@ -8,7 +8,12 @@ import type { IComboWidget } from '@/lib/litegraph/src/types/widgets'
function createMockNode(
nodeTypeName: string,
widgets: Array<{ name: string; value: any }> = [],
isApiNode = true
isApiNode = true,
inputs: Array<{
name: string
connected?: boolean
useLinksArray?: boolean
}> = []
): LGraphNode {
const mockWidgets = widgets.map(({ name, value }) => ({
name,
@@ -16,7 +21,16 @@ function createMockNode(
type: 'combo'
})) as IComboWidget[]
return {
const mockInputs =
inputs.length > 0
? inputs.map(({ name, connected, useLinksArray }) =>
useLinksArray
? { name, links: connected ? [1] : [] }
: { name, link: connected ? 1 : null }
)
: undefined
const node: any = {
id: Math.random().toString(),
widgets: mockWidgets,
constructor: {
@@ -25,7 +39,24 @@ function createMockNode(
api_node: isApiNode
}
}
} as unknown as LGraphNode
}
if (mockInputs) {
node.inputs = mockInputs
// Provide the common helpers some frontend code may call
node.findInputSlot = function (portName: string) {
return this.inputs?.findIndex((i: any) => i.name === portName) ?? -1
}
node.isInputConnected = function (idx: number) {
const port = this.inputs?.[idx]
if (!port) return false
if (typeof port.link !== 'undefined') return port.link != null
if (Array.isArray(port.links)) return port.links.length > 0
return false
}
}
return node as LGraphNode
}
describe('useNodePricing', () => {
@@ -363,34 +394,51 @@ describe('useNodePricing', () => {
})
describe('dynamic pricing - IdeogramV3', () => {
it('should return $0.09 for Quality rendering speed', () => {
it('should return correct prices for IdeogramV3 node', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('IdeogramV3', [
{ name: 'rendering_speed', value: 'Quality' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.09/Run')
})
const testCases = [
{
rendering_speed: 'Quality',
character_image: false,
expected: '$0.09/Run'
},
{
rendering_speed: 'Quality',
character_image: true,
expected: '$0.20/Run'
},
{
rendering_speed: 'Default',
character_image: false,
expected: '$0.06/Run'
},
{
rendering_speed: 'Default',
character_image: true,
expected: '$0.15/Run'
},
{
rendering_speed: 'Turbo',
character_image: false,
expected: '$0.03/Run'
},
{
rendering_speed: 'Turbo',
character_image: true,
expected: '$0.10/Run'
}
]
it('should return $0.06 for Balanced rendering speed', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('IdeogramV3', [
{ name: 'rendering_speed', value: 'Balanced' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.06/Run')
})
it('should return $0.03 for Turbo rendering speed', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('IdeogramV3', [
{ name: 'rendering_speed', value: 'Turbo' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.03/Run')
testCases.forEach(({ rendering_speed, character_image, expected }) => {
const node = createMockNode(
'IdeogramV3',
[{ name: 'rendering_speed', value: rendering_speed }],
true,
[{ name: 'character_image', connected: character_image }]
)
expect(getNodeDisplayPrice(node)).toBe(expected)
})
})
it('should return range when rendering_speed widget is missing', () => {
@@ -457,7 +505,7 @@ describe('useNodePricing', () => {
})
describe('dynamic pricing - Veo3VideoGenerationNode', () => {
it('should return $2.00 for veo-3.0-fast-generate-001 without audio', () => {
it('should return $0.80 for veo-3.0-fast-generate-001 without audio', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('Veo3VideoGenerationNode', [
{ name: 'model', value: 'veo-3.0-fast-generate-001' },
@@ -465,49 +513,49 @@ describe('useNodePricing', () => {
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$2.00/Run')
expect(price).toBe('$0.80/Run')
})
it('should return $3.20 for veo-3.0-fast-generate-001 with audio', () => {
it('should return $1.20 for veo-3.0-fast-generate-001 with audio', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('Veo3VideoGenerationNode', [
{ name: 'model', value: 'veo-3.0-fast-generate-001' },
{ name: 'generate_audio', value: true }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.20/Run')
})
it('should return $1.60 for veo-3.0-generate-001 without audio', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('Veo3VideoGenerationNode', [
{ name: 'model', value: 'veo-3.0-generate-001' },
{ name: 'generate_audio', value: false }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.60/Run')
})
it('should return $3.20 for veo-3.0-generate-001 with audio', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('Veo3VideoGenerationNode', [
{ name: 'model', value: 'veo-3.0-generate-001' },
{ name: 'generate_audio', value: true }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$3.20/Run')
})
it('should return $4.00 for veo-3.0-generate-001 without audio', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('Veo3VideoGenerationNode', [
{ name: 'model', value: 'veo-3.0-generate-001' },
{ name: 'generate_audio', value: false }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$4.00/Run')
})
it('should return $6.00 for veo-3.0-generate-001 with audio', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('Veo3VideoGenerationNode', [
{ name: 'model', value: 'veo-3.0-generate-001' },
{ name: 'generate_audio', value: true }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$6.00/Run')
})
it('should return range when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('Veo3VideoGenerationNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe(
'$2.00-6.00/Run (varies with model & audio generation)'
'$0.80-3.20/Run (varies with model & audio generation)'
)
})
@@ -519,7 +567,7 @@ describe('useNodePricing', () => {
const price = getNodeDisplayPrice(node)
expect(price).toBe(
'$2.00-6.00/Run (varies with model & audio generation)'
'$0.80-3.20/Run (varies with model & audio generation)'
)
})
@@ -531,7 +579,7 @@ describe('useNodePricing', () => {
const price = getNodeDisplayPrice(node)
expect(price).toBe(
'$2.00-6.00/Run (varies with model & audio generation)'
'$0.80-3.20/Run (varies with model & audio generation)'
)
})
})
@@ -935,7 +983,11 @@ describe('useNodePricing', () => {
const { getRelevantWidgetNames } = useNodePricing()
const widgetNames = getRelevantWidgetNames('IdeogramV3')
expect(widgetNames).toEqual(['rendering_speed', 'num_images'])
expect(widgetNames).toEqual([
'rendering_speed',
'num_images',
'character_image'
])
})
})
@@ -1728,4 +1780,273 @@ describe('useNodePricing', () => {
})
})
})
describe('dynamic pricing - ByteDanceSeedreamNode', () => {
it('should return fallback when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('ByteDanceSeedreamNode', [])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.03/Run ($0.03 for one output image)')
})
it('should return $0.03/Run when sequential generation is disabled', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('ByteDanceSeedreamNode', [
{ name: 'sequential_image_generation', value: 'disabled' },
{ name: 'max_images', value: 5 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.03/Run')
})
it('should multiply by max_images when sequential generation is enabled', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('ByteDanceSeedreamNode', [
{ name: 'sequential_image_generation', value: 'enabled' },
{ name: 'max_images', value: 4 }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.12/Run ($0.03 for one output image)')
})
})
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')
})
})
describe('dynamic pricing - WanTextToVideoApi', () => {
it('should return $1.50 for 10s at 1080p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: '10' },
{ name: 'size', value: '1080p: 4:3 (1632x1248)' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.50/Run') // 0.15 * 10
})
it('should return $0.50 for 5s at 720p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: 5 },
{ name: 'size', value: '720p: 16:9 (1280x720)' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.50/Run') // 0.10 * 5
})
it('should return $0.15 for 3s at 480p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: '3' },
{ name: 'size', value: '480p: 1:1 (624x624)' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.15/Run') // 0.05 * 3
})
it('should fall back when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const missingBoth = createMockNode('WanTextToVideoApi', [])
const missingSize = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: '5' }
])
const missingDuration = createMockNode('WanTextToVideoApi', [
{ name: 'size', value: '1080p' }
])
expect(getNodeDisplayPrice(missingBoth)).toBe('$0.05-0.15/second')
expect(getNodeDisplayPrice(missingSize)).toBe('$0.05-0.15/second')
expect(getNodeDisplayPrice(missingDuration)).toBe('$0.05-0.15/second')
})
it('should fall back on invalid duration', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: 'invalid' },
{ name: 'size', value: '1080p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05-0.15/second')
})
it('should fall back on unknown resolution', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanTextToVideoApi', [
{ name: 'duration', value: '10' },
{ name: 'size', value: '2K' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05-0.15/second')
})
})
describe('dynamic pricing - WanImageToVideoApi', () => {
it('should return $0.80 for 8s at 720p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: 8 },
{ name: 'resolution', value: '720p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.80/Run') // 0.10 * 8
})
it('should return $0.60 for 12s at 480P', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: '12' },
{ name: 'resolution', value: '480P' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.60/Run') // 0.05 * 12
})
it('should return $1.50 for 10s at 1080p', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: '10' },
{ name: 'resolution', value: '1080p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$1.50/Run') // 0.15 * 10
})
it('should handle "5s" string duration at 1080P', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: '5s' },
{ name: 'resolution', value: '1080P' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.75/Run') // 0.15 * 5
})
it('should fall back when widgets are missing', () => {
const { getNodeDisplayPrice } = useNodePricing()
const missingBoth = createMockNode('WanImageToVideoApi', [])
const missingRes = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: '5' }
])
const missingDuration = createMockNode('WanImageToVideoApi', [
{ name: 'resolution', value: '1080p' }
])
expect(getNodeDisplayPrice(missingBoth)).toBe('$0.05-0.15/second')
expect(getNodeDisplayPrice(missingRes)).toBe('$0.05-0.15/second')
expect(getNodeDisplayPrice(missingDuration)).toBe('$0.05-0.15/second')
})
it('should fall back on invalid duration', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: 'invalid' },
{ name: 'resolution', value: '720p' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05-0.15/second')
})
it('should fall back on unknown resolution', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('WanImageToVideoApi', [
{ name: 'duration', value: '10' },
{ name: 'resolution', value: 'weird-res' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.05-0.15/second')
})
})
})