From 681d4c67586a088f73af14cc4c7f2ab3a1817c5f Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Mon, 4 Aug 2025 14:57:54 -0700 Subject: [PATCH 1/4] [Bug] SaveAnimatedPNG node does not display generated APNG (#4197) Co-authored-by: github-actions --- src/services/litegraphService.ts | 9 ++++++--- src/stores/imagePreviewStore.ts | 5 ++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/services/litegraphService.ts b/src/services/litegraphService.ts index 4a53ed448..c3d9994bb 100644 --- a/src/services/litegraphService.ts +++ b/src/services/litegraphService.ts @@ -847,10 +847,13 @@ export const useLitegraphService = () => { const isAnimatedWebp = this.animatedImages && - // @ts-expect-error fixme ts strict error - output.images.some((img) => img.filename?.includes('webp')) + output?.images?.some((img) => img.filename?.includes('webp')) + const isAnimatedPng = + this.animatedImages && + output?.images?.some((img) => img.filename?.includes('png')) const isVideo = - (this.animatedImages && !isAnimatedWebp) || isVideoNode(this) + (this.animatedImages && !isAnimatedWebp && !isAnimatedPng) || + isVideoNode(this) if (isVideo) { useNodeVideo(this).showPreview() } else { diff --git a/src/stores/imagePreviewStore.ts b/src/stores/imagePreviewStore.ts index b2c19b8f9..837895e40 100644 --- a/src/stores/imagePreviewStore.ts +++ b/src/stores/imagePreviewStore.ts @@ -21,7 +21,10 @@ const createOutputs = ( ): ExecutedWsMessage['output'] => { return { images: filenames.map((image) => ({ type, ...parseFilePath(image) })), - animated: filenames.map((image) => isAnimated && image.endsWith('.webp')) + animated: filenames.map( + (image) => + isAnimated && (image.endsWith('.webp') || image.endsWith('.png')) + ) } } From 1bf2470f8f3f52fee3eb39522cc554d0d81924a6 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Mon, 4 Aug 2025 15:05:00 -0700 Subject: [PATCH 2/4] [feat] Add dynamic price badge for Veo3VideoGenerationNode (#4682) Co-authored-by: Claude --- src/composables/node/useNodePricing.ts | 28 ++++++ .../composables/node/useNodePricing.test.ts | 87 +++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts index 36aaff5a7..a7bdcd309 100644 --- a/src/composables/node/useNodePricing.ts +++ b/src/composables/node/useNodePricing.ts @@ -919,6 +919,33 @@ const apiNodeCosts: Record = return `$${price.toFixed(2)}/Run` } }, + Veo3VideoGenerationNode: { + displayPrice: (node: LGraphNode): string => { + const modelWidget = node.widgets?.find( + (w) => w.name === 'model' + ) as IComboWidget + const generateAudioWidget = node.widgets?.find( + (w) => w.name === 'generate_audio' + ) as IComboWidget + + if (!modelWidget || !generateAudioWidget) { + return '$2.00-6.00/Run (varies with model & audio generation)' + } + + const model = String(modelWidget.value) + const generateAudio = + String(generateAudioWidget.value).toLowerCase() === 'true' + + if (model.includes('veo-3.0-fast-generate-001')) { + return generateAudio ? '$3.20/Run' : '$2.00/Run' + } else if (model.includes('veo-3.0-generate-001')) { + return generateAudio ? '$6.00/Run' : '$4.00/Run' + } + + // Default fallback + return '$2.00-6.00/Run' + } + }, LumaImageNode: { displayPrice: (node: LGraphNode): string => { const modelWidget = node.widgets?.find( @@ -1340,6 +1367,7 @@ export const useNodePricing = () => { FluxProKontextProNode: [], FluxProKontextMaxNode: [], VeoVideoGenerationNode: ['duration_seconds'], + Veo3VideoGenerationNode: ['model', 'generate_audio'], LumaVideoNode: ['model', 'resolution', 'duration'], LumaImageToVideoNode: ['model', 'resolution', 'duration'], LumaImageNode: ['model', 'aspect_ratio'], diff --git a/tests-ui/tests/composables/node/useNodePricing.test.ts b/tests-ui/tests/composables/node/useNodePricing.test.ts index 67f78d7d3..1633c79d1 100644 --- a/tests-ui/tests/composables/node/useNodePricing.test.ts +++ b/tests-ui/tests/composables/node/useNodePricing.test.ts @@ -393,6 +393,86 @@ describe('useNodePricing', () => { }) }) + describe('dynamic pricing - Veo3VideoGenerationNode', () => { + it('should return $2.00 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' }, + { name: 'generate_audio', value: false } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe('$2.00/Run') + }) + + it('should return $3.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('$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)' + ) + }) + + it('should return range when only model widget is present', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('Veo3VideoGenerationNode', [ + { name: 'model', value: 'veo-3.0-generate-001' } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe( + '$2.00-6.00/Run (varies with model & audio generation)' + ) + }) + + it('should return range when only generate_audio widget is present', () => { + const { getNodeDisplayPrice } = useNodePricing() + const node = createMockNode('Veo3VideoGenerationNode', [ + { name: 'generate_audio', value: true } + ]) + + const price = getNodeDisplayPrice(node) + expect(price).toBe( + '$2.00-6.00/Run (varies with model & audio generation)' + ) + }) + }) + describe('dynamic pricing - LumaVideoNode', () => { it('should return $2.19 for ray-flash-2 4K 5s', () => { const { getNodeDisplayPrice } = useNodePricing() @@ -736,6 +816,13 @@ describe('useNodePricing', () => { expect(widgetNames).toEqual(['duration_seconds']) }) + it('should return correct widget names for Veo3VideoGenerationNode', () => { + const { getRelevantWidgetNames } = useNodePricing() + + const widgetNames = getRelevantWidgetNames('Veo3VideoGenerationNode') + expect(widgetNames).toEqual(['model', 'generate_audio']) + }) + it('should return correct widget names for LumaVideoNode', () => { const { getRelevantWidgetNames } = useNodePricing() From a8bd66b18fdadb4cfe9da726ea46abfe8a21915c Mon Sep 17 00:00:00 2001 From: AustinMroz Date: Tue, 5 Aug 2025 15:39:13 -0500 Subject: [PATCH 3/4] Fix inconsistent subgraphNode usage (#4688) --- src/utils/executionUtil.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/utils/executionUtil.ts b/src/utils/executionUtil.ts index 727a631ca..ccd538b4c 100644 --- a/src/utils/executionUtil.ts +++ b/src/utils/executionUtil.ts @@ -3,11 +3,7 @@ import type { ExecutionId, LGraph } from '@comfyorg/litegraph' -import { - ExecutableNodeDTO, - LGraphEventMode, - SubgraphNode -} from '@comfyorg/litegraph' +import { ExecutableNodeDTO, LGraphEventMode } from '@comfyorg/litegraph' import type { ComfyApiWorkflow, @@ -63,12 +59,7 @@ export const graphToPrompt = async ( for (const node of graph.computeExecutionOrder(false)) { const dto: ExecutableLGraphNode = isGroupNode(node) ? new ExecutableGroupNodeDTO(node, [], nodeDtoMap) - : new ExecutableNodeDTO( - node, - [], - nodeDtoMap, - node instanceof SubgraphNode ? node : undefined - ) + : new ExecutableNodeDTO(node, [], nodeDtoMap) for (const innerNode of dto.getInnerNodes()) { nodeDtoMap.set(innerNode.id, innerNode) From 88aa6e894e05086f7c15ef164c777464f97ea63e Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Tue, 5 Aug 2025 15:22:00 -0700 Subject: [PATCH 4/4] [docs] Enhance CLAUDE.md files with quality control guidelines (#4690) Co-authored-by: Claude --- CLAUDE.md | 11 +++++++++++ src/CLAUDE.md | 19 ++++++++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index cdf9c920c..2e15e3b17 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,9 @@ - `npm run typecheck`: Type checking - `npm run lint`: Linting - `npm run format`: Prettier formatting +- `npm run test:component`: Run component tests with browser environment +- `npm run test:unit`: Run all unit tests +- `npm run test:unit -- tests-ui/tests/example.test.ts`: Run single test file ## Development Workflow @@ -48,3 +51,11 @@ When referencing Comfy-Org repos: 1. Check for local copy 2. Use GitHub API for branches/PRs/metadata 3. Curl GitHub website if needed + +## Common Pitfalls + +- NEVER use `any` type - use proper TypeScript types +- NEVER use `as any` type assertions - fix the underlying type issue +- NEVER use `--no-verify` flag when committing +- NEVER delete or disable tests to make them pass +- NEVER circumvent quality checks diff --git a/src/CLAUDE.md b/src/CLAUDE.md index 5aa141a1a..a4b273c7b 100644 --- a/src/CLAUDE.md +++ b/src/CLAUDE.md @@ -6,9 +6,22 @@ - Use `api.apiURL()` for backend endpoints - Use `api.fileURL()` for static files -- Examples: - - Backend: `api.apiURL('/prompt')` - - Static: `api.fileURL('/templates/default.json')` + +#### ✅ Correct Usage +```typescript +// Backend API call +const response = await api.get(api.apiURL('/prompt')) + +// Static file +const template = await fetch(api.fileURL('/templates/default.json')) +``` + +#### ❌ Incorrect Usage +```typescript +// WRONG - Direct URL construction +const response = await fetch('/api/prompt') +const template = await fetch('/templates/default.json') +``` ### Error Handling