mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-27 09:45:13 +00:00
Merge branch 'main' into bl-merge-lg-fe
This commit is contained in:
11
CLAUDE.md
11
CLAUDE.md
@@ -6,6 +6,9 @@
|
|||||||
- `npm run typecheck`: Type checking
|
- `npm run typecheck`: Type checking
|
||||||
- `npm run lint`: Linting
|
- `npm run lint`: Linting
|
||||||
- `npm run format`: Prettier formatting
|
- `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
|
## Development Workflow
|
||||||
|
|
||||||
@@ -48,3 +51,11 @@ When referencing Comfy-Org repos:
|
|||||||
1. Check for local copy
|
1. Check for local copy
|
||||||
2. Use GitHub API for branches/PRs/metadata
|
2. Use GitHub API for branches/PRs/metadata
|
||||||
3. Curl GitHub website if needed
|
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
|
||||||
|
|||||||
@@ -6,9 +6,22 @@
|
|||||||
|
|
||||||
- Use `api.apiURL()` for backend endpoints
|
- Use `api.apiURL()` for backend endpoints
|
||||||
- Use `api.fileURL()` for static files
|
- Use `api.fileURL()` for static files
|
||||||
- Examples:
|
|
||||||
- Backend: `api.apiURL('/prompt')`
|
#### ✅ Correct Usage
|
||||||
- Static: `api.fileURL('/templates/default.json')`
|
```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
|
### Error Handling
|
||||||
|
|
||||||
|
|||||||
@@ -919,6 +919,33 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
|||||||
return `$${price.toFixed(2)}/Run`
|
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: {
|
LumaImageNode: {
|
||||||
displayPrice: (node: LGraphNode): string => {
|
displayPrice: (node: LGraphNode): string => {
|
||||||
const modelWidget = node.widgets?.find(
|
const modelWidget = node.widgets?.find(
|
||||||
@@ -1340,6 +1367,7 @@ export const useNodePricing = () => {
|
|||||||
FluxProKontextProNode: [],
|
FluxProKontextProNode: [],
|
||||||
FluxProKontextMaxNode: [],
|
FluxProKontextMaxNode: [],
|
||||||
VeoVideoGenerationNode: ['duration_seconds'],
|
VeoVideoGenerationNode: ['duration_seconds'],
|
||||||
|
Veo3VideoGenerationNode: ['model', 'generate_audio'],
|
||||||
LumaVideoNode: ['model', 'resolution', 'duration'],
|
LumaVideoNode: ['model', 'resolution', 'duration'],
|
||||||
LumaImageToVideoNode: ['model', 'resolution', 'duration'],
|
LumaImageToVideoNode: ['model', 'resolution', 'duration'],
|
||||||
LumaImageNode: ['model', 'aspect_ratio'],
|
LumaImageNode: ['model', 'aspect_ratio'],
|
||||||
|
|||||||
@@ -847,10 +847,13 @@ export const useLitegraphService = () => {
|
|||||||
|
|
||||||
const isAnimatedWebp =
|
const isAnimatedWebp =
|
||||||
this.animatedImages &&
|
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 =
|
const isVideo =
|
||||||
(this.animatedImages && !isAnimatedWebp) || isVideoNode(this)
|
(this.animatedImages && !isAnimatedWebp && !isAnimatedPng) ||
|
||||||
|
isVideoNode(this)
|
||||||
if (isVideo) {
|
if (isVideo) {
|
||||||
useNodeVideo(this).showPreview()
|
useNodeVideo(this).showPreview()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ const createOutputs = (
|
|||||||
): ExecutedWsMessage['output'] => {
|
): ExecutedWsMessage['output'] => {
|
||||||
return {
|
return {
|
||||||
images: filenames.map((image) => ({ type, ...parseFilePath(image) })),
|
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'))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,8 @@ import type {
|
|||||||
ExecutionId,
|
ExecutionId,
|
||||||
LGraph
|
LGraph
|
||||||
} from '@/lib/litegraph/src/litegraph'
|
} from '@/lib/litegraph/src/litegraph'
|
||||||
import {
|
import { ExecutableNodeDTO, LGraphEventMode } from '@/lib/litegraph/src/litegraph'
|
||||||
ExecutableNodeDTO,
|
|
||||||
LGraphEventMode,
|
|
||||||
SubgraphNode
|
|
||||||
} from '@/lib/litegraph/src/litegraph'
|
|
||||||
import type {
|
import type {
|
||||||
ComfyApiWorkflow,
|
ComfyApiWorkflow,
|
||||||
ComfyWorkflowJSON
|
ComfyWorkflowJSON
|
||||||
@@ -62,12 +59,7 @@ export const graphToPrompt = async (
|
|||||||
for (const node of graph.computeExecutionOrder(false)) {
|
for (const node of graph.computeExecutionOrder(false)) {
|
||||||
const dto: ExecutableLGraphNode = isGroupNode(node)
|
const dto: ExecutableLGraphNode = isGroupNode(node)
|
||||||
? new ExecutableGroupNodeDTO(node, [], nodeDtoMap)
|
? new ExecutableGroupNodeDTO(node, [], nodeDtoMap)
|
||||||
: new ExecutableNodeDTO(
|
: new ExecutableNodeDTO(node, [], nodeDtoMap)
|
||||||
node,
|
|
||||||
[],
|
|
||||||
nodeDtoMap,
|
|
||||||
node instanceof SubgraphNode ? node : undefined
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const innerNode of dto.getInnerNodes()) {
|
for (const innerNode of dto.getInnerNodes()) {
|
||||||
nodeDtoMap.set(innerNode.id, innerNode)
|
nodeDtoMap.set(innerNode.id, innerNode)
|
||||||
|
|||||||
@@ -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', () => {
|
describe('dynamic pricing - LumaVideoNode', () => {
|
||||||
it('should return $2.19 for ray-flash-2 4K 5s', () => {
|
it('should return $2.19 for ray-flash-2 4K 5s', () => {
|
||||||
const { getNodeDisplayPrice } = useNodePricing()
|
const { getNodeDisplayPrice } = useNodePricing()
|
||||||
@@ -736,6 +816,13 @@ describe('useNodePricing', () => {
|
|||||||
expect(widgetNames).toEqual(['duration_seconds'])
|
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', () => {
|
it('should return correct widget names for LumaVideoNode', () => {
|
||||||
const { getRelevantWidgetNames } = useNodePricing()
|
const { getRelevantWidgetNames } = useNodePricing()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user