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 diff --git a/src/composables/node/useNodePricing.ts b/src/composables/node/useNodePricing.ts index 8d521b81e..f61dccc70 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/src/services/litegraphService.ts b/src/services/litegraphService.ts index 3570d5985..82648164a 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 2af468d6f..44e9f665a 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')) + ) } } diff --git a/src/utils/executionUtil.ts b/src/utils/executionUtil.ts index f00ca0920..e116efb89 100644 --- a/src/utils/executionUtil.ts +++ b/src/utils/executionUtil.ts @@ -3,11 +3,8 @@ import type { ExecutionId, LGraph } from '@/lib/litegraph/src/litegraph' -import { - ExecutableNodeDTO, - LGraphEventMode, - SubgraphNode -} from '@/lib/litegraph/src/litegraph' +import { ExecutableNodeDTO, LGraphEventMode } from '@/lib/litegraph/src/litegraph' + import type { ComfyApiWorkflow, ComfyWorkflowJSON @@ -62,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) diff --git a/tests-ui/tests/composables/node/useNodePricing.test.ts b/tests-ui/tests/composables/node/useNodePricing.test.ts index 6ca4ef918..cdb6454f1 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()