From d6db75fcdd247ed0068ca651e799a4dd520d04cc Mon Sep 17 00:00:00 2001 From: Marwan Mostafa Date: Sat, 13 Sep 2025 13:17:15 +0300 Subject: [PATCH 01/12] ci(tests-ui): pin ComfyUI_devtools to 60a61dcf8c961d5eea669e9ab7bb8dc0ac0ee004 --- .github/workflows/i18n-custom-nodes.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/i18n-custom-nodes.yaml b/.github/workflows/i18n-custom-nodes.yaml index a5617c19642..5ff8e0ae74a 100644 --- a/.github/workflows/i18n-custom-nodes.yaml +++ b/.github/workflows/i18n-custom-nodes.yaml @@ -37,6 +37,7 @@ jobs: with: repository: Comfy-Org/ComfyUI_devtools path: ComfyUI/custom_nodes/ComfyUI_devtools + ref: 60a61dcf8c961d5eea669e9ab7bb8dc0ac0ee004 - name: Checkout custom node repository uses: actions/checkout@v4 with: From 6c140dc197a6be6b9a94cb84a571c5a30e54595c Mon Sep 17 00:00:00 2001 From: Marwan Mostafa Date: Sun, 14 Sep 2025 22:39:06 +0300 Subject: [PATCH 02/12] ci(tests-ui): update devtools SHA in test-ui.yaml --- .github/workflows/test-ui.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-ui.yaml b/.github/workflows/test-ui.yaml index eaaaefee093..d9e979c1656 100644 --- a/.github/workflows/test-ui.yaml +++ b/.github/workflows/test-ui.yaml @@ -32,7 +32,7 @@ jobs: with: repository: 'Comfy-Org/ComfyUI_devtools' path: 'ComfyUI/custom_nodes/ComfyUI_devtools' - ref: 'd05fd48dd787a4192e16802d4244cfcc0e2f9684' + ref: '60a61dcf8c961d5eea669e9ab7bb8dc0ac0ee004' - name: Install pnpm uses: pnpm/action-setup@v4 From 7c33349222e907aa1d5a20c0dff4396f95ffefe1 Mon Sep 17 00:00:00 2001 From: Marwan Mostafa Date: Tue, 7 Oct 2025 20:08:41 +0300 Subject: [PATCH 03/12] feat(pricing): deterministic OpenAIVideoSora2 pricing added (for 720p and 1080p for Sora-2 and Sora-2-pro --- src/composables/node/useNodePricing.ts | 34 +++++++ .../composables/node/useNodePricing.test.ts | 96 +++++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts index e85e6adb60c..a66bff28010 100644 --- a/src/composables/node/useNodePricing.ts +++ b/src/composables/node/useNodePricing.ts @@ -168,7 +168,37 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => { ? minStr : `$${minCost.toFixed(2)}-$${maxCost.toFixed(2)}/Run` } +const sora2PricingCalculator: PricingFunction = (node: LGraphNode): string => { + const modelW = node.widgets?.find((w) => w.name === 'model') as IComboWidget + const durationW = node.widgets?.find( + (w) => w.name === 'duration' || w.name === 'duration_s' + ) as IComboWidget + const resolutionW = node.widgets?.find( + (w) => w.name === 'resolution' + ) as IComboWidget + + if (!modelW || !durationW || !resolutionW) + return 'Set model, duration & resolution' + + const model = String(modelW.value).toLowerCase() + const duration = Number(durationW.value) + const resolution = String(resolutionW.value || '').toLowerCase() + if (!duration || Number.isNaN(duration)) return 'Set duration (4/8/12)' + if (!resolution) return 'Set resolution (720p or 1080p)' + + if (model.includes('sora-2-pro')) { + let perSec: number | null = null + if (resolution.includes('1080')) perSec = 0.5 + else if (resolution.includes('720')) perSec = 0.3 + else return 'Resolution must be 720p or 1080p' + return `$${(perSec * duration).toFixed(2)}/Run` + } + + // sora-2 (non-pro) → 720p only + if (!resolution.includes('720')) return 'sora-2 supports 720p only' + return `$${(0.1 * duration).toFixed(2)}/Run` +} /** * Static pricing data for API nodes, now supporting both strings and functions */ @@ -195,6 +225,9 @@ const apiNodeCosts: Record = FluxProKontextMaxNode: { displayPrice: '$0.08/Run' }, + OpenAIVideoSora2: { + displayPrice: sora2PricingCalculator + }, IdeogramV1: { displayPrice: (node: LGraphNode): string => { const numImagesWidget = node.widgets?.find( @@ -1589,6 +1622,7 @@ export const useNodePricing = () => { MinimaxHailuoVideoNode: ['resolution', 'duration'], OpenAIDalle3: ['size', 'quality'], OpenAIDalle2: ['size', 'n'], + OpenAIVideoSora2: ['model', 'resolution', 'duration'], OpenAIGPTImage1: ['quality', 'n'], IdeogramV1: ['num_images', 'turbo'], IdeogramV2: ['num_images', 'turbo'], diff --git a/tests-ui/tests/composables/node/useNodePricing.test.ts b/tests-ui/tests/composables/node/useNodePricing.test.ts index 6cd76cb75b0..56a9c9cde46 100644 --- a/tests-ui/tests/composables/node/useNodePricing.test.ts +++ b/tests-ui/tests/composables/node/useNodePricing.test.ts @@ -270,6 +270,102 @@ describe('useNodePricing', () => { }) }) + describe('dynamic pricing - OpenAIVideoSora2', () => { + it('should require model, duration & resolution when widgets are missing', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('OpenAIVideoSora2', []) + expect(getNodeDisplayPrice(node)).toBe('Set model, duration & resolution') + }) + it('should require duration when duration is invalid or zero', () => { + const { getNodeDisplayPrice } = useNodePricing() + // invalid (NaN) + const nodeNaN = createMockNode('OpenAIVideoSora2', [ + { name: 'model', value: 'sora-2-pro' }, + { name: 'duration', value: 'oops' }, + { name: 'resolution', value: '1080p' } + ]) + expect(getNodeDisplayPrice(nodeNaN)).toBe('Set duration (4/8/12)') + // zero + const nodeZero = createMockNode('OpenAIVideoSora2', [ + { name: 'model', value: 'sora-2-pro' }, + { name: 'duration', value: 0 }, + { name: 'resolution', value: '1080p' } + ]) + expect(getNodeDisplayPrice(nodeZero)).toBe('Set duration (4/8/12)') + }) + it('should require resolution when resolution is missing', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('OpenAIVideoSora2', [ + { name: 'model', value: 'sora-2-pro' }, + { name: 'duration', value: 8 } + ]) + expect(getNodeDisplayPrice(node)).toBe('Set resolution (720p or 1080p)') + }) + it('should compute pricing for sora-2-pro at 1080p', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('OpenAIVideoSora2', [ + { name: 'model', value: 'sora-2-pro' }, + { name: 'duration', value: 8 }, + { name: 'resolution', value: '1080p' } + ]) + expect(getNodeDisplayPrice(node)).toBe('$4.00/Run') // 0.50 * 8 + }) + it('should compute pricing for sora-2-pro at 720p', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('OpenAIVideoSora2', [ + { name: 'model', value: 'sora-2-pro' }, + { name: 'duration', value: 12 }, + { name: 'resolution', value: '720p' } + ]) + expect(getNodeDisplayPrice(node)).toBe('$3.60/Run') // 0.30 * 12 + }) + it('should reject unsupported resolution for sora-2-pro', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('OpenAIVideoSora2', [ + { name: 'model', value: 'sora-2-pro' }, + { name: 'duration', value: 8 }, + { name: 'resolution', value: '4k' } + ]) + expect(getNodeDisplayPrice(node)).toBe('Resolution must be 720p or 1080p') + }) + it('should compute pricing for plain sora-2 (720p only)', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('OpenAIVideoSora2', [ + { name: 'model', value: 'sora-2' }, + { name: 'duration', value: 10 }, + { name: 'resolution', value: '720p' } + ]) + expect(getNodeDisplayPrice(node)).toBe('$1.00/Run') // 0.10 * 10 + }) + it('should reject non-720p resolution for plain sora-2', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('OpenAIVideoSora2', [ + { name: 'model', value: 'sora-2' }, + { name: 'duration', value: 8 }, + { name: 'resolution', value: '1080p' } + ]) + expect(getNodeDisplayPrice(node)).toBe('sora-2 supports 720p only') + }) + it('should accept duration_s alias for duration', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('OpenAIVideoSora2', [ + { name: 'model', value: 'sora-2-pro' }, + { name: 'duration_s', value: 4 }, + { name: 'resolution', value: '1080p' } + ]) + expect(getNodeDisplayPrice(node)).toBe('$2.00/Run') // 0.50 * 4 + }) + it('should be case-insensitive for model and resolution', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('OpenAIVideoSora2', [ + { name: 'model', value: 'SoRa-2-PrO' }, + { name: 'duration', value: 12 }, + { name: 'resolution', value: '1080P' } + ]) + expect(getNodeDisplayPrice(node)).toBe('$6.00/Run') // 0.50 * 12 + }) + }) + describe('dynamic pricing - MinimaxHailuoVideoNode', () => { it('should return $0.28 for 6s duration and 768P resolution', () => { const { getNodeDisplayPrice } = useNodePricing() From fac7ac3f83b6269cce65506e0e1f2b5d699ffd70 Mon Sep 17 00:00:00 2001 From: Marwan Mostafa Date: Tue, 7 Oct 2025 20:29:33 +0300 Subject: [PATCH 04/12] Update OpenAIVideoSora2 tests and pricing logic for size instead of resolution --- src/composables/node/useNodePricing.ts | 21 +++--- .../composables/node/useNodePricing.test.ts | 72 +++++++++++-------- 2 files changed, 52 insertions(+), 41 deletions(-) diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts index a66bff28010..3ce5fa0fd4d 100644 --- a/src/composables/node/useNodePricing.ts +++ b/src/composables/node/useNodePricing.ts @@ -173,30 +173,27 @@ const sora2PricingCalculator: PricingFunction = (node: LGraphNode): string => { const durationW = node.widgets?.find( (w) => w.name === 'duration' || w.name === 'duration_s' ) as IComboWidget - const resolutionW = node.widgets?.find( - (w) => w.name === 'resolution' - ) as IComboWidget + const sizeW = node.widgets?.find((w) => w.name === 'size') as IComboWidget - if (!modelW || !durationW || !resolutionW) - return 'Set model, duration & resolution' + if (!modelW || !durationW || !sizeW) return 'Set model, duration & size' const model = String(modelW.value).toLowerCase() const duration = Number(durationW.value) - const resolution = String(resolutionW.value || '').toLowerCase() + const size = String(sizeW.value || '').toLowerCase() if (!duration || Number.isNaN(duration)) return 'Set duration (4/8/12)' - if (!resolution) return 'Set resolution (720p or 1080p)' + if (!size) return 'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)' if (model.includes('sora-2-pro')) { let perSec: number | null = null - if (resolution.includes('1080')) perSec = 0.5 - else if (resolution.includes('720')) perSec = 0.3 - else return 'Resolution must be 720p or 1080p' + if (size.includes('1080')) perSec = 0.5 + else if (size.includes('720')) perSec = 0.3 + else return 'Size must be 720x1280, 1280x720, 1024x1792, or 1792x1024' return `$${(perSec * duration).toFixed(2)}/Run` } // sora-2 (non-pro) → 720p only - if (!resolution.includes('720')) return 'sora-2 supports 720p only' + if (!size.includes('720')) return 'sora-2 supports only 720x1280 or 1280x720' return `$${(0.1 * duration).toFixed(2)}/Run` } /** @@ -1622,7 +1619,7 @@ export const useNodePricing = () => { MinimaxHailuoVideoNode: ['resolution', 'duration'], OpenAIDalle3: ['size', 'quality'], OpenAIDalle2: ['size', 'n'], - OpenAIVideoSora2: ['model', 'resolution', 'duration'], + OpenAIVideoSora2: ['model', 'size', 'duration'], OpenAIGPTImage1: ['quality', 'n'], IdeogramV1: ['num_images', 'turbo'], IdeogramV2: ['num_images', 'turbo'], diff --git a/tests-ui/tests/composables/node/useNodePricing.test.ts b/tests-ui/tests/composables/node/useNodePricing.test.ts index 56a9c9cde46..e4c84fc0d15 100644 --- a/tests-ui/tests/composables/node/useNodePricing.test.ts +++ b/tests-ui/tests/composables/node/useNodePricing.test.ts @@ -269,103 +269,117 @@ describe('useNodePricing', () => { expect(price).toBe('$0.04-0.12/Run (varies with size & quality)') }) }) - + // ============================== OpenAIVideoSora2 ============================== describe('dynamic pricing - OpenAIVideoSora2', () => { - it('should require model, duration & resolution when widgets are missing', () => { + it('should require model, duration & size when widgets are missing', () => { const { getNodeDisplayPrice } = useNodePricing() const node = createMockNode('OpenAIVideoSora2', []) - expect(getNodeDisplayPrice(node)).toBe('Set model, duration & resolution') + expect(getNodeDisplayPrice(node)).toBe('Set model, duration & size') }) + it('should require duration when duration is invalid or zero', () => { const { getNodeDisplayPrice } = useNodePricing() - // invalid (NaN) const nodeNaN = createMockNode('OpenAIVideoSora2', [ { name: 'model', value: 'sora-2-pro' }, { name: 'duration', value: 'oops' }, - { name: 'resolution', value: '1080p' } + { name: 'size', value: '720x1280' } ]) expect(getNodeDisplayPrice(nodeNaN)).toBe('Set duration (4/8/12)') - // zero + const nodeZero = createMockNode('OpenAIVideoSora2', [ { name: 'model', value: 'sora-2-pro' }, { name: 'duration', value: 0 }, - { name: 'resolution', value: '1080p' } + { name: 'size', value: '720x1280' } ]) expect(getNodeDisplayPrice(nodeZero)).toBe('Set duration (4/8/12)') }) - it('should require resolution when resolution is missing', () => { + + it('should require size when size is missing', () => { const { getNodeDisplayPrice } = useNodePricing() const node = createMockNode('OpenAIVideoSora2', [ { name: 'model', value: 'sora-2-pro' }, { name: 'duration', value: 8 } ]) - expect(getNodeDisplayPrice(node)).toBe('Set resolution (720p or 1080p)') + expect(getNodeDisplayPrice(node)).toBe( + 'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)' + ) }) - it('should compute pricing for sora-2-pro at 1080p', () => { + + it('should compute pricing for sora-2-pro with 1024x1792', () => { const { getNodeDisplayPrice } = useNodePricing() const node = createMockNode('OpenAIVideoSora2', [ { name: 'model', value: 'sora-2-pro' }, { name: 'duration', value: 8 }, - { name: 'resolution', value: '1080p' } + { name: 'size', value: '1024x1792' } ]) - expect(getNodeDisplayPrice(node)).toBe('$4.00/Run') // 0.50 * 8 + expect(getNodeDisplayPrice(node)).toBe('$4.00/Run') // 0.5 * 8 }) - it('should compute pricing for sora-2-pro at 720p', () => { + + it('should compute pricing for sora-2-pro with 720x1280', () => { const { getNodeDisplayPrice } = useNodePricing() const node = createMockNode('OpenAIVideoSora2', [ { name: 'model', value: 'sora-2-pro' }, { name: 'duration', value: 12 }, - { name: 'resolution', value: '720p' } + { name: 'size', value: '720x1280' } ]) - expect(getNodeDisplayPrice(node)).toBe('$3.60/Run') // 0.30 * 12 + expect(getNodeDisplayPrice(node)).toBe('$3.60/Run') // 0.3 * 12 }) - it('should reject unsupported resolution for sora-2-pro', () => { + + it('should reject unsupported size for sora-2-pro', () => { const { getNodeDisplayPrice } = useNodePricing() const node = createMockNode('OpenAIVideoSora2', [ { name: 'model', value: 'sora-2-pro' }, { name: 'duration', value: 8 }, - { name: 'resolution', value: '4k' } + { name: 'size', value: '640x640' } ]) - expect(getNodeDisplayPrice(node)).toBe('Resolution must be 720p or 1080p') + expect(getNodeDisplayPrice(node)).toBe( + 'Size must be 720x1280, 1280x720, 1024x1792, or 1792x1024' + ) }) - it('should compute pricing for plain sora-2 (720p only)', () => { + + it('should compute pricing for sora-2 (720x1280 only)', () => { const { getNodeDisplayPrice } = useNodePricing() const node = createMockNode('OpenAIVideoSora2', [ { name: 'model', value: 'sora-2' }, { name: 'duration', value: 10 }, - { name: 'resolution', value: '720p' } + { name: 'size', value: '720x1280' } ]) - expect(getNodeDisplayPrice(node)).toBe('$1.00/Run') // 0.10 * 10 + expect(getNodeDisplayPrice(node)).toBe('$1.00/Run') // 0.1 * 10 }) - it('should reject non-720p resolution for plain sora-2', () => { + + it('should reject non-720 sizes for sora-2', () => { const { getNodeDisplayPrice } = useNodePricing() const node = createMockNode('OpenAIVideoSora2', [ { name: 'model', value: 'sora-2' }, { name: 'duration', value: 8 }, - { name: 'resolution', value: '1080p' } + { name: 'size', value: '1024x1792' } ]) - expect(getNodeDisplayPrice(node)).toBe('sora-2 supports 720p only') + expect(getNodeDisplayPrice(node)).toBe( + 'sora-2 supports only 720x1280 or 1280x720' + ) }) it('should accept duration_s alias for duration', () => { const { getNodeDisplayPrice } = useNodePricing() const node = createMockNode('OpenAIVideoSora2', [ { name: 'model', value: 'sora-2-pro' }, { name: 'duration_s', value: 4 }, - { name: 'resolution', value: '1080p' } + { name: 'size', value: '1792x1024' } ]) - expect(getNodeDisplayPrice(node)).toBe('$2.00/Run') // 0.50 * 4 + expect(getNodeDisplayPrice(node)).toBe('$2.00/Run') // 0.5 * 4 }) - it('should be case-insensitive for model and resolution', () => { + + it('should be case-insensitive for model and size', () => { const { getNodeDisplayPrice } = useNodePricing() const node = createMockNode('OpenAIVideoSora2', [ { name: 'model', value: 'SoRa-2-PrO' }, { name: 'duration', value: 12 }, - { name: 'resolution', value: '1080P' } + { name: 'size', value: '1280x720' } ]) - expect(getNodeDisplayPrice(node)).toBe('$6.00/Run') // 0.50 * 12 + expect(getNodeDisplayPrice(node)).toBe('$3.60/Run') // 0.3 * 12 }) }) + // ============================== MinimaxHailuoVideoNode ============================== describe('dynamic pricing - MinimaxHailuoVideoNode', () => { it('should return $0.28 for 6s duration and 768P resolution', () => { const { getNodeDisplayPrice } = useNodePricing() From 075c06d88442c081b3096bebd34d0fe1f88d6a5b Mon Sep 17 00:00:00 2001 From: Marwan Mostafa Date: Tue, 7 Oct 2025 21:41:48 +0300 Subject: [PATCH 05/12] refactor: improve clarity in OpenAIVideoSora2 size checks , Replaced substring matching (size.includes('720')) with explicit --- .github/workflows/i18n-custom-nodes.yaml | 1 - .github/workflows/test-ui.yaml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/i18n-custom-nodes.yaml b/.github/workflows/i18n-custom-nodes.yaml index 5ff8e0ae74a..a5617c19642 100644 --- a/.github/workflows/i18n-custom-nodes.yaml +++ b/.github/workflows/i18n-custom-nodes.yaml @@ -37,7 +37,6 @@ jobs: with: repository: Comfy-Org/ComfyUI_devtools path: ComfyUI/custom_nodes/ComfyUI_devtools - ref: 60a61dcf8c961d5eea669e9ab7bb8dc0ac0ee004 - name: Checkout custom node repository uses: actions/checkout@v4 with: diff --git a/.github/workflows/test-ui.yaml b/.github/workflows/test-ui.yaml index d9e979c1656..eaaaefee093 100644 --- a/.github/workflows/test-ui.yaml +++ b/.github/workflows/test-ui.yaml @@ -32,7 +32,7 @@ jobs: with: repository: 'Comfy-Org/ComfyUI_devtools' path: 'ComfyUI/custom_nodes/ComfyUI_devtools' - ref: '60a61dcf8c961d5eea669e9ab7bb8dc0ac0ee004' + ref: 'd05fd48dd787a4192e16802d4244cfcc0e2f9684' - name: Install pnpm uses: pnpm/action-setup@v4 From acacf7089f2d9f670ad6d4dde9ca9b7803a4a1f4 Mon Sep 17 00:00:00 2001 From: Marwan Mostafa Date: Tue, 7 Oct 2025 21:44:10 +0300 Subject: [PATCH 06/12] refactor: improve clarity in OpenAIVideoSora2 size checks , Replaced substring matching (size.includes('720')) with explicit --- src/composables/node/useNodePricing.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts index 3ce5fa0fd4d..7338dd99de0 100644 --- a/src/composables/node/useNodePricing.ts +++ b/src/composables/node/useNodePricing.ts @@ -186,14 +186,15 @@ const sora2PricingCalculator: PricingFunction = (node: LGraphNode): string => { if (model.includes('sora-2-pro')) { let perSec: number | null = null - if (size.includes('1080')) perSec = 0.5 - else if (size.includes('720')) perSec = 0.3 + if (['1024x1792', '1792x1024'].includes(size)) perSec = 0.5 + else if (['720x1280', '1280x720'].includes(size)) perSec = 0.3 else return 'Size must be 720x1280, 1280x720, 1024x1792, or 1792x1024' return `$${(perSec * duration).toFixed(2)}/Run` } // sora-2 (non-pro) → 720p only - if (!size.includes('720')) return 'sora-2 supports only 720x1280 or 1280x720' + if (!['720x1280', '1280x720'].includes(size)) + return 'sora-2 supports only 720x1280 or 1280x720' return `$${(0.1 * duration).toFixed(2)}/Run` } /** From 2ddcc0ae2ebe5df51817d0e6cb3d469bca8e0962 Mon Sep 17 00:00:00 2001 From: Marwan Mostafa Date: Tue, 7 Oct 2025 23:11:57 +0300 Subject: [PATCH 07/12] Fix Sora2 pricing: explicit size validation and accurate missing-size message --- src/composables/node/useNodePricing.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts index 7338dd99de0..05476504b3b 100644 --- a/src/composables/node/useNodePricing.ts +++ b/src/composables/node/useNodePricing.ts @@ -175,25 +175,30 @@ const sora2PricingCalculator: PricingFunction = (node: LGraphNode): string => { ) as IComboWidget const sizeW = node.widgets?.find((w) => w.name === 'size') as IComboWidget - if (!modelW || !durationW || !sizeW) return 'Set model, duration & size' + // precise missing-widget messages (fixes the failing test) + if (!modelW || !durationW) return 'Set model, duration & size' + if (!sizeW) return 'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)' - const model = String(modelW.value).toLowerCase() + const model = String(modelW.value ?? '').toLowerCase() const duration = Number(durationW.value) - const size = String(sizeW.value || '').toLowerCase() + const size = String(sizeW.value ?? '').toLowerCase() if (!duration || Number.isNaN(duration)) return 'Set duration (4/8/12)' if (!size) return 'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)' + const SORA720 = ['720x1280', '1280x720'] + const SORA_PRO_HIRES = ['1024x1792', '1792x1024'] + if (model.includes('sora-2-pro')) { let perSec: number | null = null - if (['1024x1792', '1792x1024'].includes(size)) perSec = 0.5 - else if (['720x1280', '1280x720'].includes(size)) perSec = 0.3 + if (SORA_PRO_HIRES.includes(size)) perSec = 0.5 + else if (SORA720.includes(size)) perSec = 0.3 else return 'Size must be 720x1280, 1280x720, 1024x1792, or 1792x1024' return `$${(perSec * duration).toFixed(2)}/Run` } - // sora-2 (non-pro) → 720p only - if (!['720x1280', '1280x720'].includes(size)) + // plain sora-2: 720p only + if (!SORA720.includes(size)) return 'sora-2 supports only 720x1280 or 1280x720' return `$${(0.1 * duration).toFixed(2)}/Run` } From 8b84e4796a00a2f30912bee7f41de4a8dec3ddb7 Mon Sep 17 00:00:00 2001 From: Marwan Mostafa Date: Wed, 8 Oct 2025 00:11:51 +0300 Subject: [PATCH 08/12] Revert unintended changes to CI workflow, changes to Sora-2 Validation and pricingCalculator which are now pure functions --- .github/workflows/i18n-custom-nodes.yaml | 1 + .github/workflows/test-ui.yaml | 2 +- src/composables/node/useNodePricing.ts | 100 +++++++++++++++++------ 3 files changed, 78 insertions(+), 25 deletions(-) diff --git a/.github/workflows/i18n-custom-nodes.yaml b/.github/workflows/i18n-custom-nodes.yaml index a5617c19642..5ff8e0ae74a 100644 --- a/.github/workflows/i18n-custom-nodes.yaml +++ b/.github/workflows/i18n-custom-nodes.yaml @@ -37,6 +37,7 @@ jobs: with: repository: Comfy-Org/ComfyUI_devtools path: ComfyUI/custom_nodes/ComfyUI_devtools + ref: 60a61dcf8c961d5eea669e9ab7bb8dc0ac0ee004 - name: Checkout custom node repository uses: actions/checkout@v4 with: diff --git a/.github/workflows/test-ui.yaml b/.github/workflows/test-ui.yaml index eaaaefee093..d9e979c1656 100644 --- a/.github/workflows/test-ui.yaml +++ b/.github/workflows/test-ui.yaml @@ -32,7 +32,7 @@ jobs: with: repository: 'Comfy-Org/ComfyUI_devtools' path: 'ComfyUI/custom_nodes/ComfyUI_devtools' - ref: 'd05fd48dd787a4192e16802d4244cfcc0e2f9684' + ref: '60a61dcf8c961d5eea669e9ab7bb8dc0ac0ee004' - name: Install pnpm uses: pnpm/action-setup@v4 diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts index 05476504b3b..4f6e1066584 100644 --- a/src/composables/node/useNodePricing.ts +++ b/src/composables/node/useNodePricing.ts @@ -168,40 +168,92 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => { ? minStr : `$${minCost.toFixed(2)}-$${maxCost.toFixed(2)}/Run` } -const sora2PricingCalculator: PricingFunction = (node: LGraphNode): string => { - const modelW = node.widgets?.find((w) => w.name === 'model') as IComboWidget + +export const SORA720 = new Set(['720x1280', '1280x720']) +export const SORA_PRO_HIRES = new Set(['1024x1792', '1792x1024']) +export const ALL_SIZES = new Set([...SORA720, ...SORA_PRO_HIRES]) + +export const validateSora2Selection = ( + modelRaw: string, + duration: number, + sizeRaw: string +): string | undefined => { + const model = String(modelRaw ?? '').toLowerCase() + const size = String(sizeRaw ?? '').toLowerCase() + + if (!duration || Number.isNaN(duration)) { + return 'Set duration (4/8/12)' + } + + if (!size) { + return 'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)' + } + + if (!ALL_SIZES.has(size)) { + return 'Size must be 720x1280, 1280x720, 1024x1792, or 1792x1024' + } + + if (model.includes('sora-2-pro')) { + // pro is fine with either 720p or 1080; nothing else to validate + return undefined + } + + if (model.includes('sora-2')) { + if (!SORA720.has(size)) { + return 'sora-2 supports only 720x1280 or 1280x720' + } + return undefined + } + + return 'Unsupported model' +} + +export const perSecForSora2 = (modelRaw: string, sizeRaw: string): number => { + const model = String(modelRaw ?? '').toLowerCase() + const size = String(sizeRaw ?? '').toLowerCase() + + if (model.includes('sora-2-pro')) { + if (SORA_PRO_HIRES.has(size)) return 0.5 + if (SORA720.has(size)) return 0.3 + } + // plain sora-2 (validated to 720p already) + if (model.includes('sora-2')) { + return 0.1 + } + return SORA_PRO_HIRES.has(size) ? 0.5 : 0.1 +} + +export const formatRunPrice = (perSec: number, duration: number) => + `$${(perSec * duration).toFixed(2)}/Run` + +export const sora2PricingCalculator: PricingFunction = ( + node: LGraphNode +): string => { + const modelW = node.widgets?.find((w) => w.name === 'model') as + | IComboWidget + | undefined const durationW = node.widgets?.find( (w) => w.name === 'duration' || w.name === 'duration_s' - ) as IComboWidget - const sizeW = node.widgets?.find((w) => w.name === 'size') as IComboWidget + ) as IComboWidget | undefined + const sizeW = node.widgets?.find((w) => w.name === 'size') as + | IComboWidget + | undefined - // precise missing-widget messages (fixes the failing test) if (!modelW || !durationW) return 'Set model, duration & size' if (!sizeW) return 'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)' - const model = String(modelW.value ?? '').toLowerCase() + const model = String(modelW.value ?? '') const duration = Number(durationW.value) - const size = String(sizeW.value ?? '').toLowerCase() + const size = String(sizeW.value ?? '') - if (!duration || Number.isNaN(duration)) return 'Set duration (4/8/12)' - if (!size) return 'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)' + const validationError = validateSora2Selection(model, duration, size) + if (validationError) return validationError - const SORA720 = ['720x1280', '1280x720'] - const SORA_PRO_HIRES = ['1024x1792', '1792x1024'] - - if (model.includes('sora-2-pro')) { - let perSec: number | null = null - if (SORA_PRO_HIRES.includes(size)) perSec = 0.5 - else if (SORA720.includes(size)) perSec = 0.3 - else return 'Size must be 720x1280, 1280x720, 1024x1792, or 1792x1024' - return `$${(perSec * duration).toFixed(2)}/Run` - } - - // plain sora-2: 720p only - if (!SORA720.includes(size)) - return 'sora-2 supports only 720x1280 or 1280x720' - return `$${(0.1 * duration).toFixed(2)}/Run` + const perSec = perSecForSora2(model, size) + return formatRunPrice(perSec, duration) } + +export default sora2PricingCalculator /** * Static pricing data for API nodes, now supporting both strings and functions */ From 0c891da4ca16fa55b813be75b9bc095f1fe65792 Mon Sep 17 00:00:00 2001 From: Marwan Mostafa Date: Wed, 8 Oct 2025 00:28:58 +0300 Subject: [PATCH 09/12] Revert unintended changes to CI workflow, changes to Sora-2 Validation and pricingCalculator which are now pure functions --- src/composables/node/useNodePricing.ts | 48 +++++++++----------------- 1 file changed, 16 insertions(+), 32 deletions(-) diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts index 4f6e1066584..b048dc43fe0 100644 --- a/src/composables/node/useNodePricing.ts +++ b/src/composables/node/useNodePricing.ts @@ -169,46 +169,33 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => { : `$${minCost.toFixed(2)}-$${maxCost.toFixed(2)}/Run` } -export const SORA720 = new Set(['720x1280', '1280x720']) -export const SORA_PRO_HIRES = new Set(['1024x1792', '1792x1024']) -export const ALL_SIZES = new Set([...SORA720, ...SORA_PRO_HIRES]) +// ---- constants (file-local) ---- +const SORA720 = new Set(['720x1280', '1280x720']) +const SORA_PRO_HIRES = new Set(['1024x1792', '1792x1024']) +const ALL_SIZES = new Set([...SORA720, ...SORA_PRO_HIRES]) -export const validateSora2Selection = ( +function validateSora2Selection( modelRaw: string, duration: number, sizeRaw: string -): string | undefined => { +): string | undefined { const model = String(modelRaw ?? '').toLowerCase() const size = String(sizeRaw ?? '').toLowerCase() - if (!duration || Number.isNaN(duration)) { - return 'Set duration (4/8/12)' - } - - if (!size) { - return 'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)' - } - - if (!ALL_SIZES.has(size)) { + if (!duration || Number.isNaN(duration)) return 'Set duration (4/8/12)' + if (!size) return 'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)' + if (!ALL_SIZES.has(size)) return 'Size must be 720x1280, 1280x720, 1024x1792, or 1792x1024' - } - - if (model.includes('sora-2-pro')) { - // pro is fine with either 720p or 1080; nothing else to validate - return undefined - } + if (model.includes('sora-2-pro')) return undefined if (model.includes('sora-2')) { - if (!SORA720.has(size)) { - return 'sora-2 supports only 720x1280 or 1280x720' - } + if (!SORA720.has(size)) return 'sora-2 supports only 720x1280 or 1280x720' return undefined } return 'Unsupported model' } - -export const perSecForSora2 = (modelRaw: string, sizeRaw: string): number => { +function perSecForSora2(modelRaw: string, sizeRaw: string): number { const model = String(modelRaw ?? '').toLowerCase() const size = String(sizeRaw ?? '').toLowerCase() @@ -223,12 +210,11 @@ export const perSecForSora2 = (modelRaw: string, sizeRaw: string): number => { return SORA_PRO_HIRES.has(size) ? 0.5 : 0.1 } -export const formatRunPrice = (perSec: number, duration: number) => - `$${(perSec * duration).toFixed(2)}/Run` +function formatRunPrice(perSec: number, duration: number) { + return `$${(perSec * duration).toFixed(2)}/Run` +} -export const sora2PricingCalculator: PricingFunction = ( - node: LGraphNode -): string => { +const sora2PricingCalculator: PricingFunction = (node: LGraphNode): string => { const modelW = node.widgets?.find((w) => w.name === 'model') as | IComboWidget | undefined @@ -252,8 +238,6 @@ export const sora2PricingCalculator: PricingFunction = ( const perSec = perSecForSora2(model, size) return formatRunPrice(perSec, duration) } - -export default sora2PricingCalculator /** * Static pricing data for API nodes, now supporting both strings and functions */ From 7fdf3db153daa17279bf0bf66df5265a0d28bf25 Mon Sep 17 00:00:00 2001 From: Marwan Mostafa Date: Wed, 8 Oct 2025 01:56:23 +0300 Subject: [PATCH 10/12] Code clean-up, removed unneccessary commits from branching --- .github/workflows/i18n-custom-nodes.yaml | 3 +-- .github/workflows/test-ui.yaml | 2 +- src/composables/node/useNodePricing.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/i18n-custom-nodes.yaml b/.github/workflows/i18n-custom-nodes.yaml index 5ff8e0ae74a..01e020b2718 100644 --- a/.github/workflows/i18n-custom-nodes.yaml +++ b/.github/workflows/i18n-custom-nodes.yaml @@ -36,8 +36,7 @@ jobs: uses: actions/checkout@v4 with: repository: Comfy-Org/ComfyUI_devtools - path: ComfyUI/custom_nodes/ComfyUI_devtools - ref: 60a61dcf8c961d5eea669e9ab7bb8dc0ac0ee004 + path: ComfyUI/custom_nodes/ComfyUI_devtools - name: Checkout custom node repository uses: actions/checkout@v4 with: diff --git a/.github/workflows/test-ui.yaml b/.github/workflows/test-ui.yaml index d9e979c1656..eaaaefee093 100644 --- a/.github/workflows/test-ui.yaml +++ b/.github/workflows/test-ui.yaml @@ -32,7 +32,7 @@ jobs: with: repository: 'Comfy-Org/ComfyUI_devtools' path: 'ComfyUI/custom_nodes/ComfyUI_devtools' - ref: '60a61dcf8c961d5eea669e9ab7bb8dc0ac0ee004' + ref: 'd05fd48dd787a4192e16802d4244cfcc0e2f9684' - name: Install pnpm uses: pnpm/action-setup@v4 diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts index b048dc43fe0..82f56eaa133 100644 --- a/src/composables/node/useNodePricing.ts +++ b/src/composables/node/useNodePricing.ts @@ -182,7 +182,7 @@ function validateSora2Selection( const model = String(modelRaw ?? '').toLowerCase() const size = String(sizeRaw ?? '').toLowerCase() - if (!duration || Number.isNaN(duration)) return 'Set duration (4/8/12)' + if (!duration || Number.isNaN(duration)) return 'Set duration (4s /8s /12s)' if (!size) return 'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)' if (!ALL_SIZES.has(size)) return 'Size must be 720x1280, 1280x720, 1024x1792, or 1792x1024' From e20a5f09fcbf5aee1053158e3fe445e4e49cf4bd Mon Sep 17 00:00:00 2001 From: Marwan Mostafa Date: Wed, 8 Oct 2025 02:00:59 +0300 Subject: [PATCH 11/12] Removed extra spacing/typo --- .github/workflows/i18n-custom-nodes.yaml | 3 +- src/composables/node/useNodePricing.ts | 75 ++++++++++++------------ 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/.github/workflows/i18n-custom-nodes.yaml b/.github/workflows/i18n-custom-nodes.yaml index 01e020b2718..5ff8e0ae74a 100644 --- a/.github/workflows/i18n-custom-nodes.yaml +++ b/.github/workflows/i18n-custom-nodes.yaml @@ -36,7 +36,8 @@ jobs: uses: actions/checkout@v4 with: repository: Comfy-Org/ComfyUI_devtools - path: ComfyUI/custom_nodes/ComfyUI_devtools + path: ComfyUI/custom_nodes/ComfyUI_devtools + ref: 60a61dcf8c961d5eea669e9ab7bb8dc0ac0ee004 - name: Checkout custom node repository uses: actions/checkout@v4 with: diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts index 82f56eaa133..40a13fbbfb8 100644 --- a/src/composables/node/useNodePricing.ts +++ b/src/composables/node/useNodePricing.ts @@ -169,68 +169,66 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => { : `$${minCost.toFixed(2)}-$${maxCost.toFixed(2)}/Run` } -// ---- constants (file-local) ---- -const SORA720 = new Set(['720x1280', '1280x720']) -const SORA_PRO_HIRES = new Set(['1024x1792', '1792x1024']) -const ALL_SIZES = new Set([...SORA720, ...SORA_PRO_HIRES]) +// ---- constants ---- +const SORA_SIZES = { + BASIC: new Set(['720x1280', '1280x720']), + PRO: new Set(['1024x1792', '1792x1024']) +} +const ALL_SIZES = new Set([...SORA_SIZES.BASIC, ...SORA_SIZES.PRO]) +// ---- sora-2 pricing helpers ---- function validateSora2Selection( modelRaw: string, duration: number, sizeRaw: string ): string | undefined { - const model = String(modelRaw ?? '').toLowerCase() - const size = String(sizeRaw ?? '').toLowerCase() + const model = modelRaw?.toLowerCase() ?? '' + const size = sizeRaw?.toLowerCase() ?? '' - if (!duration || Number.isNaN(duration)) return 'Set duration (4s /8s /12s)' + if (!duration || Number.isNaN(duration)) return 'Set duration (4s / 8s / 12s)' if (!size) return 'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)' if (!ALL_SIZES.has(size)) - return 'Size must be 720x1280, 1280x720, 1024x1792, or 1792x1024' + return 'Invalid size. Must be 720x1280, 1280x720, 1024x1792, or 1792x1024.' + if (model.includes('sora-2-pro')) return undefined - if (model.includes('sora-2')) { - if (!SORA720.has(size)) return 'sora-2 supports only 720x1280 or 1280x720' - return undefined - } + if (model.includes('sora-2') && !SORA_SIZES.BASIC.has(size)) + return 'sora-2 supports only 720x1280 or 1280x720' - return 'Unsupported model' + if (!model.includes('sora-2')) return 'Unsupported model' + + return undefined } + function perSecForSora2(modelRaw: string, sizeRaw: string): number { - const model = String(modelRaw ?? '').toLowerCase() - const size = String(sizeRaw ?? '').toLowerCase() + const model = modelRaw?.toLowerCase() ?? '' + const size = sizeRaw?.toLowerCase() ?? '' if (model.includes('sora-2-pro')) { - if (SORA_PRO_HIRES.has(size)) return 0.5 - if (SORA720.has(size)) return 0.3 + return SORA_SIZES.PRO.has(size) ? 0.5 : 0.3 } - // plain sora-2 (validated to 720p already) - if (model.includes('sora-2')) { - return 0.1 - } - return SORA_PRO_HIRES.has(size) ? 0.5 : 0.1 + if (model.includes('sora-2')) return 0.1 + + return SORA_SIZES.PRO.has(size) ? 0.5 : 0.1 } function formatRunPrice(perSec: number, duration: number) { return `$${(perSec * duration).toFixed(2)}/Run` } +// ---- pricing calculator ---- const sora2PricingCalculator: PricingFunction = (node: LGraphNode): string => { - const modelW = node.widgets?.find((w) => w.name === 'model') as - | IComboWidget - | undefined - const durationW = node.widgets?.find( - (w) => w.name === 'duration' || w.name === 'duration_s' - ) as IComboWidget | undefined - const sizeW = node.widgets?.find((w) => w.name === 'size') as - | IComboWidget - | undefined - - if (!modelW || !durationW) return 'Set model, duration & size' - if (!sizeW) return 'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)' - - const model = String(modelW.value ?? '') - const duration = Number(durationW.value) - const size = String(sizeW.value ?? '') + const getWidgetValue = (name: string) => + String(node.widgets?.find((w) => w.name === name)?.value ?? '') + + const model = getWidgetValue('model') + const size = getWidgetValue('size') + const duration = Number( + node.widgets?.find((w) => ['duration', 'duration_s'].includes(w.name)) + ?.value + ) + + if (!model || !size || !duration) return 'Set model, duration & size' const validationError = validateSora2Selection(model, duration, size) if (validationError) return validationError @@ -238,6 +236,7 @@ const sora2PricingCalculator: PricingFunction = (node: LGraphNode): string => { const perSec = perSecForSora2(model, size) return formatRunPrice(perSec, duration) } + /** * Static pricing data for API nodes, now supporting both strings and functions */ From d70c93f9874c6f31534be7d07e507c2060b5ba9a Mon Sep 17 00:00:00 2001 From: Marwan Ahmed <155799754+marawan206@users.noreply.github.com> Date: Wed, 8 Oct 2025 02:28:52 +0300 Subject: [PATCH 12/12] Update i18n-custom-nodes.yaml --- .github/workflows/i18n-custom-nodes.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/i18n-custom-nodes.yaml b/.github/workflows/i18n-custom-nodes.yaml index 5ff8e0ae74a..a5617c19642 100644 --- a/.github/workflows/i18n-custom-nodes.yaml +++ b/.github/workflows/i18n-custom-nodes.yaml @@ -37,7 +37,6 @@ jobs: with: repository: Comfy-Org/ComfyUI_devtools path: ComfyUI/custom_nodes/ComfyUI_devtools - ref: 60a61dcf8c961d5eea669e9ab7bb8dc0ac0ee004 - name: Checkout custom node repository uses: actions/checkout@v4 with: