Skip to content

Commit fad8dae

Browse files
Merge branch 'main' into bl-merge-lg-fe
2 parents 0ce5aeb + 88aa6e8 commit fad8dae

File tree

7 files changed

+155
-18
lines changed

7 files changed

+155
-18
lines changed

CLAUDE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
- `npm run typecheck`: Type checking
77
- `npm run lint`: Linting
88
- `npm run format`: Prettier formatting
9+
- `npm run test:component`: Run component tests with browser environment
10+
- `npm run test:unit`: Run all unit tests
11+
- `npm run test:unit -- tests-ui/tests/example.test.ts`: Run single test file
912

1013
## Development Workflow
1114

@@ -48,3 +51,11 @@ When referencing Comfy-Org repos:
4851
1. Check for local copy
4952
2. Use GitHub API for branches/PRs/metadata
5053
3. Curl GitHub website if needed
54+
55+
## Common Pitfalls
56+
57+
- NEVER use `any` type - use proper TypeScript types
58+
- NEVER use `as any` type assertions - fix the underlying type issue
59+
- NEVER use `--no-verify` flag when committing
60+
- NEVER delete or disable tests to make them pass
61+
- NEVER circumvent quality checks

src/CLAUDE.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,22 @@
66

77
- Use `api.apiURL()` for backend endpoints
88
- Use `api.fileURL()` for static files
9-
- Examples:
10-
- Backend: `api.apiURL('/prompt')`
11-
- Static: `api.fileURL('/templates/default.json')`
9+
10+
#### ✅ Correct Usage
11+
```typescript
12+
// Backend API call
13+
const response = await api.get(api.apiURL('/prompt'))
14+
15+
// Static file
16+
const template = await fetch(api.fileURL('/templates/default.json'))
17+
```
18+
19+
#### ❌ Incorrect Usage
20+
```typescript
21+
// WRONG - Direct URL construction
22+
const response = await fetch('/api/prompt')
23+
const template = await fetch('/templates/default.json')
24+
```
1225

1326
### Error Handling
1427

src/composables/node/useNodePricing.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,33 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
919919
return `$${price.toFixed(2)}/Run`
920920
}
921921
},
922+
Veo3VideoGenerationNode: {
923+
displayPrice: (node: LGraphNode): string => {
924+
const modelWidget = node.widgets?.find(
925+
(w) => w.name === 'model'
926+
) as IComboWidget
927+
const generateAudioWidget = node.widgets?.find(
928+
(w) => w.name === 'generate_audio'
929+
) as IComboWidget
930+
931+
if (!modelWidget || !generateAudioWidget) {
932+
return '$2.00-6.00/Run (varies with model & audio generation)'
933+
}
934+
935+
const model = String(modelWidget.value)
936+
const generateAudio =
937+
String(generateAudioWidget.value).toLowerCase() === 'true'
938+
939+
if (model.includes('veo-3.0-fast-generate-001')) {
940+
return generateAudio ? '$3.20/Run' : '$2.00/Run'
941+
} else if (model.includes('veo-3.0-generate-001')) {
942+
return generateAudio ? '$6.00/Run' : '$4.00/Run'
943+
}
944+
945+
// Default fallback
946+
return '$2.00-6.00/Run'
947+
}
948+
},
922949
LumaImageNode: {
923950
displayPrice: (node: LGraphNode): string => {
924951
const modelWidget = node.widgets?.find(
@@ -1340,6 +1367,7 @@ export const useNodePricing = () => {
13401367
FluxProKontextProNode: [],
13411368
FluxProKontextMaxNode: [],
13421369
VeoVideoGenerationNode: ['duration_seconds'],
1370+
Veo3VideoGenerationNode: ['model', 'generate_audio'],
13431371
LumaVideoNode: ['model', 'resolution', 'duration'],
13441372
LumaImageToVideoNode: ['model', 'resolution', 'duration'],
13451373
LumaImageNode: ['model', 'aspect_ratio'],

src/services/litegraphService.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -847,10 +847,13 @@ export const useLitegraphService = () => {
847847

848848
const isAnimatedWebp =
849849
this.animatedImages &&
850-
// @ts-expect-error fixme ts strict error
851-
output.images.some((img) => img.filename?.includes('webp'))
850+
output?.images?.some((img) => img.filename?.includes('webp'))
851+
const isAnimatedPng =
852+
this.animatedImages &&
853+
output?.images?.some((img) => img.filename?.includes('png'))
852854
const isVideo =
853-
(this.animatedImages && !isAnimatedWebp) || isVideoNode(this)
855+
(this.animatedImages && !isAnimatedWebp && !isAnimatedPng) ||
856+
isVideoNode(this)
854857
if (isVideo) {
855858
useNodeVideo(this).showPreview()
856859
} else {

src/stores/imagePreviewStore.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ const createOutputs = (
2121
): ExecutedWsMessage['output'] => {
2222
return {
2323
images: filenames.map((image) => ({ type, ...parseFilePath(image) })),
24-
animated: filenames.map((image) => isAnimated && image.endsWith('.webp'))
24+
animated: filenames.map(
25+
(image) =>
26+
isAnimated && (image.endsWith('.webp') || image.endsWith('.png'))
27+
)
2528
}
2629
}
2730

src/utils/executionUtil.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@ import type {
33
ExecutionId,
44
LGraph
55
} from '@/lib/litegraph/src/litegraph'
6-
import {
7-
ExecutableNodeDTO,
8-
LGraphEventMode,
9-
SubgraphNode
10-
} from '@/lib/litegraph/src/litegraph'
6+
import { ExecutableNodeDTO, LGraphEventMode } from '@/lib/litegraph/src/litegraph'
7+
118
import type {
129
ComfyApiWorkflow,
1310
ComfyWorkflowJSON
@@ -62,12 +59,7 @@ export const graphToPrompt = async (
6259
for (const node of graph.computeExecutionOrder(false)) {
6360
const dto: ExecutableLGraphNode = isGroupNode(node)
6461
? new ExecutableGroupNodeDTO(node, [], nodeDtoMap)
65-
: new ExecutableNodeDTO(
66-
node,
67-
[],
68-
nodeDtoMap,
69-
node instanceof SubgraphNode ? node : undefined
70-
)
62+
: new ExecutableNodeDTO(node, [], nodeDtoMap)
7163

7264
for (const innerNode of dto.getInnerNodes()) {
7365
nodeDtoMap.set(innerNode.id, innerNode)

tests-ui/tests/composables/node/useNodePricing.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,86 @@ describe('useNodePricing', () => {
393393
})
394394
})
395395

396+
describe('dynamic pricing - Veo3VideoGenerationNode', () => {
397+
it('should return $2.00 for veo-3.0-fast-generate-001 without audio', () => {
398+
const { getNodeDisplayPrice } = useNodePricing()
399+
const node = createMockNode('Veo3VideoGenerationNode', [
400+
{ name: 'model', value: 'veo-3.0-fast-generate-001' },
401+
{ name: 'generate_audio', value: false }
402+
])
403+
404+
const price = getNodeDisplayPrice(node)
405+
expect(price).toBe('$2.00/Run')
406+
})
407+
408+
it('should return $3.20 for veo-3.0-fast-generate-001 with audio', () => {
409+
const { getNodeDisplayPrice } = useNodePricing()
410+
const node = createMockNode('Veo3VideoGenerationNode', [
411+
{ name: 'model', value: 'veo-3.0-fast-generate-001' },
412+
{ name: 'generate_audio', value: true }
413+
])
414+
415+
const price = getNodeDisplayPrice(node)
416+
expect(price).toBe('$3.20/Run')
417+
})
418+
419+
it('should return $4.00 for veo-3.0-generate-001 without audio', () => {
420+
const { getNodeDisplayPrice } = useNodePricing()
421+
const node = createMockNode('Veo3VideoGenerationNode', [
422+
{ name: 'model', value: 'veo-3.0-generate-001' },
423+
{ name: 'generate_audio', value: false }
424+
])
425+
426+
const price = getNodeDisplayPrice(node)
427+
expect(price).toBe('$4.00/Run')
428+
})
429+
430+
it('should return $6.00 for veo-3.0-generate-001 with audio', () => {
431+
const { getNodeDisplayPrice } = useNodePricing()
432+
const node = createMockNode('Veo3VideoGenerationNode', [
433+
{ name: 'model', value: 'veo-3.0-generate-001' },
434+
{ name: 'generate_audio', value: true }
435+
])
436+
437+
const price = getNodeDisplayPrice(node)
438+
expect(price).toBe('$6.00/Run')
439+
})
440+
441+
it('should return range when widgets are missing', () => {
442+
const { getNodeDisplayPrice } = useNodePricing()
443+
const node = createMockNode('Veo3VideoGenerationNode', [])
444+
445+
const price = getNodeDisplayPrice(node)
446+
expect(price).toBe(
447+
'$2.00-6.00/Run (varies with model & audio generation)'
448+
)
449+
})
450+
451+
it('should return range when only model widget is present', () => {
452+
const { getNodeDisplayPrice } = useNodePricing()
453+
const node = createMockNode('Veo3VideoGenerationNode', [
454+
{ name: 'model', value: 'veo-3.0-generate-001' }
455+
])
456+
457+
const price = getNodeDisplayPrice(node)
458+
expect(price).toBe(
459+
'$2.00-6.00/Run (varies with model & audio generation)'
460+
)
461+
})
462+
463+
it('should return range when only generate_audio widget is present', () => {
464+
const { getNodeDisplayPrice } = useNodePricing()
465+
const node = createMockNode('Veo3VideoGenerationNode', [
466+
{ name: 'generate_audio', value: true }
467+
])
468+
469+
const price = getNodeDisplayPrice(node)
470+
expect(price).toBe(
471+
'$2.00-6.00/Run (varies with model & audio generation)'
472+
)
473+
})
474+
})
475+
396476
describe('dynamic pricing - LumaVideoNode', () => {
397477
it('should return $2.19 for ray-flash-2 4K 5s', () => {
398478
const { getNodeDisplayPrice } = useNodePricing()
@@ -736,6 +816,13 @@ describe('useNodePricing', () => {
736816
expect(widgetNames).toEqual(['duration_seconds'])
737817
})
738818

819+
it('should return correct widget names for Veo3VideoGenerationNode', () => {
820+
const { getRelevantWidgetNames } = useNodePricing()
821+
822+
const widgetNames = getRelevantWidgetNames('Veo3VideoGenerationNode')
823+
expect(widgetNames).toEqual(['model', 'generate_audio'])
824+
})
825+
739826
it('should return correct widget names for LumaVideoNode', () => {
740827
const { getRelevantWidgetNames } = useNodePricing()
741828

0 commit comments

Comments
 (0)