mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-23 07:50:15 +00:00
Compare commits
8 Commits
pr5-list-v
...
v1.37.11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bf6a19600 | ||
|
|
ed73ae7a61 | ||
|
|
4f059c993d | ||
|
|
64be98873b | ||
|
|
a963d587cf | ||
|
|
8ce1f6ccd3 | ||
|
|
af50d48dd8 | ||
|
|
c6dfaefbd3 |
@@ -194,7 +194,10 @@ test.describe('Image widget', () => {
|
|||||||
const comboEntry = comfyPage.page.getByRole('menuitem', {
|
const comboEntry = comfyPage.page.getByRole('menuitem', {
|
||||||
name: 'image32x32.webp'
|
name: 'image32x32.webp'
|
||||||
})
|
})
|
||||||
await comboEntry.click({ noWaitAfter: true })
|
await comboEntry.click()
|
||||||
|
|
||||||
|
// Stabilization for the image swap
|
||||||
|
await comfyPage.nextFrame()
|
||||||
|
|
||||||
// Expect the image preview to change automatically
|
// Expect the image preview to change automatically
|
||||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 46 KiB |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@comfyorg/comfyui-frontend",
|
"name": "@comfyorg/comfyui-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.37.10",
|
"version": "1.37.11",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||||
"homepage": "https://comfy.org",
|
"homepage": "https://comfy.org",
|
||||||
|
|||||||
@@ -2,6 +2,17 @@ import { formatCreditsFromUsd } from '@/base/credits/comfyCredits'
|
|||||||
import type { INodeInputSlot, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
import type { INodeInputSlot, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||||
import type { IComboWidget } from '@/lib/litegraph/src/types/widgets'
|
import type { IComboWidget } from '@/lib/litegraph/src/types/widgets'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meshy credit pricing constant.
|
||||||
|
* 1 Meshy credit = $0.04 USD
|
||||||
|
* Change this value to update all Meshy node prices.
|
||||||
|
*/
|
||||||
|
const MESHY_CREDIT_PRICE_USD = 0.04
|
||||||
|
|
||||||
|
/** Convert Meshy credits to USD */
|
||||||
|
const meshyCreditsToUsd = (credits: number): number =>
|
||||||
|
credits * MESHY_CREDIT_PRICE_USD
|
||||||
|
|
||||||
const DEFAULT_NUMBER_OPTIONS: Intl.NumberFormatOptions = {
|
const DEFAULT_NUMBER_OPTIONS: Intl.NumberFormatOptions = {
|
||||||
minimumFractionDigits: 0,
|
minimumFractionDigits: 0,
|
||||||
maximumFractionDigits: 0
|
maximumFractionDigits: 0
|
||||||
@@ -209,13 +220,24 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => {
|
|||||||
const resolutionWidget = node.widgets?.find(
|
const resolutionWidget = node.widgets?.find(
|
||||||
(w) => w.name === 'resolution'
|
(w) => w.name === 'resolution'
|
||||||
) as IComboWidget
|
) as IComboWidget
|
||||||
|
const generateAudioWidget = node.widgets?.find(
|
||||||
|
(w) => w.name === 'generate_audio'
|
||||||
|
) as IComboWidget | undefined
|
||||||
|
|
||||||
if (!modelWidget || !durationWidget || !resolutionWidget) return 'Token-based'
|
if (!modelWidget || !durationWidget || !resolutionWidget) return 'Token-based'
|
||||||
|
|
||||||
const model = String(modelWidget.value).toLowerCase()
|
const model = String(modelWidget.value).toLowerCase()
|
||||||
const resolution = String(resolutionWidget.value).toLowerCase()
|
const resolution = String(resolutionWidget.value).toLowerCase()
|
||||||
const seconds = parseFloat(String(durationWidget.value))
|
const seconds = parseFloat(String(durationWidget.value))
|
||||||
|
const generateAudio =
|
||||||
|
generateAudioWidget &&
|
||||||
|
String(generateAudioWidget.value).toLowerCase() === 'true'
|
||||||
const priceByModel: Record<string, Record<string, [number, number]>> = {
|
const priceByModel: Record<string, Record<string, [number, number]>> = {
|
||||||
|
'seedance-1-5-pro': {
|
||||||
|
'480p': [0.12, 0.12],
|
||||||
|
'720p': [0.26, 0.26],
|
||||||
|
'1080p': [0.58, 0.59]
|
||||||
|
},
|
||||||
'seedance-1-0-pro': {
|
'seedance-1-0-pro': {
|
||||||
'480p': [0.23, 0.24],
|
'480p': [0.23, 0.24],
|
||||||
'720p': [0.51, 0.56],
|
'720p': [0.51, 0.56],
|
||||||
@@ -233,13 +255,15 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const modelKey = model.includes('seedance-1-0-pro-fast')
|
const modelKey = model.includes('seedance-1-5-pro')
|
||||||
? 'seedance-1-0-pro-fast'
|
? 'seedance-1-5-pro'
|
||||||
: model.includes('seedance-1-0-pro')
|
: model.includes('seedance-1-0-pro-fast')
|
||||||
? 'seedance-1-0-pro'
|
? 'seedance-1-0-pro-fast'
|
||||||
: model.includes('seedance-1-0-lite')
|
: model.includes('seedance-1-0-pro')
|
||||||
? 'seedance-1-0-lite'
|
? 'seedance-1-0-pro'
|
||||||
: ''
|
: model.includes('seedance-1-0-lite')
|
||||||
|
? 'seedance-1-0-lite'
|
||||||
|
: ''
|
||||||
|
|
||||||
const resKey = resolution.includes('1080')
|
const resKey = resolution.includes('1080')
|
||||||
? '1080p'
|
? '1080p'
|
||||||
@@ -255,8 +279,10 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => {
|
|||||||
|
|
||||||
const [min10s, max10s] = baseRange
|
const [min10s, max10s] = baseRange
|
||||||
const scale = seconds / 10
|
const scale = seconds / 10
|
||||||
const minCost = min10s * scale
|
const audioMultiplier =
|
||||||
const maxCost = max10s * scale
|
modelKey === 'seedance-1-5-pro' && generateAudio ? 2 : 1
|
||||||
|
const minCost = min10s * scale * audioMultiplier
|
||||||
|
const maxCost = max10s * scale * audioMultiplier
|
||||||
|
|
||||||
if (minCost === maxCost) return formatCreditsLabel(minCost)
|
if (minCost === maxCost) return formatCreditsLabel(minCost)
|
||||||
return formatCreditsRangeLabel(minCost, maxCost)
|
return formatCreditsRangeLabel(minCost, maxCost)
|
||||||
@@ -525,6 +551,54 @@ const calculateTripo3DGenerationPrice = (
|
|||||||
return formatCreditsLabel(dollars)
|
return formatCreditsLabel(dollars)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meshy Image to 3D pricing calculator.
|
||||||
|
* Pricing based on should_texture widget:
|
||||||
|
* - Without texture: 20 credits
|
||||||
|
* - With texture: 30 credits
|
||||||
|
*/
|
||||||
|
const calculateMeshyImageToModelPrice = (node: LGraphNode): string => {
|
||||||
|
const shouldTextureWidget = node.widgets?.find(
|
||||||
|
(w) => w.name === 'should_texture'
|
||||||
|
) as IComboWidget
|
||||||
|
|
||||||
|
if (!shouldTextureWidget) {
|
||||||
|
return formatCreditsRangeLabel(
|
||||||
|
meshyCreditsToUsd(20),
|
||||||
|
meshyCreditsToUsd(30),
|
||||||
|
{ note: '(varies with texture)' }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldTexture = String(shouldTextureWidget.value).toLowerCase()
|
||||||
|
const credits = shouldTexture === 'true' ? 30 : 20
|
||||||
|
return formatCreditsLabel(meshyCreditsToUsd(credits))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meshy Multi-Image to 3D pricing calculator.
|
||||||
|
* Pricing based on should_texture widget:
|
||||||
|
* - Without texture: 5 credits
|
||||||
|
* - With texture: 15 credits
|
||||||
|
*/
|
||||||
|
const calculateMeshyMultiImageToModelPrice = (node: LGraphNode): string => {
|
||||||
|
const shouldTextureWidget = node.widgets?.find(
|
||||||
|
(w) => w.name === 'should_texture'
|
||||||
|
) as IComboWidget
|
||||||
|
|
||||||
|
if (!shouldTextureWidget) {
|
||||||
|
return formatCreditsRangeLabel(
|
||||||
|
meshyCreditsToUsd(5),
|
||||||
|
meshyCreditsToUsd(15),
|
||||||
|
{ note: '(varies with texture)' }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldTexture = String(shouldTextureWidget.value).toLowerCase()
|
||||||
|
const credits = shouldTexture === 'true' ? 15 : 5
|
||||||
|
return formatCreditsLabel(meshyCreditsToUsd(credits))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Static pricing data for API nodes, now supporting both strings and functions
|
* Static pricing data for API nodes, now supporting both strings and functions
|
||||||
*/
|
*/
|
||||||
@@ -1812,6 +1886,27 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
|||||||
TripoRefineNode: {
|
TripoRefineNode: {
|
||||||
displayPrice: formatCreditsLabel(0.3)
|
displayPrice: formatCreditsLabel(0.3)
|
||||||
},
|
},
|
||||||
|
MeshyTextToModelNode: {
|
||||||
|
displayPrice: formatCreditsLabel(meshyCreditsToUsd(20))
|
||||||
|
},
|
||||||
|
MeshyRefineNode: {
|
||||||
|
displayPrice: formatCreditsLabel(meshyCreditsToUsd(10))
|
||||||
|
},
|
||||||
|
MeshyImageToModelNode: {
|
||||||
|
displayPrice: calculateMeshyImageToModelPrice
|
||||||
|
},
|
||||||
|
MeshyMultiImageToModelNode: {
|
||||||
|
displayPrice: calculateMeshyMultiImageToModelPrice
|
||||||
|
},
|
||||||
|
MeshyRigModelNode: {
|
||||||
|
displayPrice: formatCreditsLabel(meshyCreditsToUsd(5))
|
||||||
|
},
|
||||||
|
MeshyAnimateModelNode: {
|
||||||
|
displayPrice: formatCreditsLabel(meshyCreditsToUsd(3))
|
||||||
|
},
|
||||||
|
MeshyTextureNode: {
|
||||||
|
displayPrice: formatCreditsLabel(meshyCreditsToUsd(10))
|
||||||
|
},
|
||||||
// Google/Gemini nodes
|
// Google/Gemini nodes
|
||||||
GeminiNode: {
|
GeminiNode: {
|
||||||
displayPrice: (node: LGraphNode): string => {
|
displayPrice: (node: LGraphNode): string => {
|
||||||
@@ -2527,6 +2622,9 @@ export const useNodePricing = () => {
|
|||||||
'animate_in_place'
|
'animate_in_place'
|
||||||
],
|
],
|
||||||
TripoTextureNode: ['texture_quality'],
|
TripoTextureNode: ['texture_quality'],
|
||||||
|
// Meshy nodes
|
||||||
|
MeshyImageToModelNode: ['should_texture'],
|
||||||
|
MeshyMultiImageToModelNode: ['should_texture'],
|
||||||
// Google/Gemini nodes
|
// Google/Gemini nodes
|
||||||
GeminiNode: ['model'],
|
GeminiNode: ['model'],
|
||||||
GeminiImage2Node: ['resolution'],
|
GeminiImage2Node: ['resolution'],
|
||||||
@@ -2540,9 +2638,24 @@ export const useNodePricing = () => {
|
|||||||
'sequential_image_generation',
|
'sequential_image_generation',
|
||||||
'max_images'
|
'max_images'
|
||||||
],
|
],
|
||||||
ByteDanceTextToVideoNode: ['model', 'duration', 'resolution'],
|
ByteDanceTextToVideoNode: [
|
||||||
ByteDanceImageToVideoNode: ['model', 'duration', 'resolution'],
|
'model',
|
||||||
ByteDanceFirstLastFrameNode: ['model', 'duration', 'resolution'],
|
'duration',
|
||||||
|
'resolution',
|
||||||
|
'generate_audio'
|
||||||
|
],
|
||||||
|
ByteDanceImageToVideoNode: [
|
||||||
|
'model',
|
||||||
|
'duration',
|
||||||
|
'resolution',
|
||||||
|
'generate_audio'
|
||||||
|
],
|
||||||
|
ByteDanceFirstLastFrameNode: [
|
||||||
|
'model',
|
||||||
|
'duration',
|
||||||
|
'resolution',
|
||||||
|
'generate_audio'
|
||||||
|
],
|
||||||
ByteDanceImageReferenceNode: ['model', 'duration', 'resolution'],
|
ByteDanceImageReferenceNode: ['model', 'duration', 'resolution'],
|
||||||
WanTextToVideoApi: ['duration', 'size'],
|
WanTextToVideoApi: ['duration', 'size'],
|
||||||
WanImageToVideoApi: ['duration', 'resolution'],
|
WanImageToVideoApi: ['duration', 'resolution'],
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMuta
|
|||||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||||
import { removeNodeTitleHeight } from '@/renderer/core/layout/utils/nodeSizeUtil'
|
import { removeNodeTitleHeight } from '@/renderer/core/layout/utils/nodeSizeUtil'
|
||||||
|
import { forEachNode } from '@/utils/graphTraversalUtil'
|
||||||
|
|
||||||
import { CanvasPointer } from './CanvasPointer'
|
import { CanvasPointer } from './CanvasPointer'
|
||||||
import type { ContextMenu } from './ContextMenu'
|
import type { ContextMenu } from './ContextMenu'
|
||||||
@@ -4041,6 +4042,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
|||||||
layoutStore.batchUpdateNodeBounds(newPositions)
|
layoutStore.batchUpdateNodeBounds(newPositions)
|
||||||
|
|
||||||
this.selectItems(created)
|
this.selectItems(created)
|
||||||
|
forEachNode(graph, (n) => n.onGraphConfigured?.())
|
||||||
|
forEachNode(graph, (n) => n.onAfterGraphConfigured?.())
|
||||||
|
|
||||||
graph.afterChange()
|
graph.afterChange()
|
||||||
this.emitAfterChange()
|
this.emitAfterChange()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { whenever } from '@vueuse/core'
|
import { whenever } from '@vueuse/core'
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed, nextTick, onMounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import { useToastStore } from './toastStore'
|
import { useToastStore } from './toastStore'
|
||||||
@@ -65,9 +65,12 @@ export function useFrontendVersionMismatchWarning(
|
|||||||
versionCompatibilityStore.dismissWarning()
|
versionCompatibilityStore.dismissWarning()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
// Only set up the watcher if immediate is true
|
// Only set up the watcher if immediate is true
|
||||||
if (immediate) {
|
if (immediate) {
|
||||||
|
// Wait for next tick to ensure reactive updates from settings load have propagated
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
whenever(
|
whenever(
|
||||||
() => versionCompatibilityStore.shouldShowWarning,
|
() => versionCompatibilityStore.shouldShowWarning,
|
||||||
() => {
|
() => {
|
||||||
|
|||||||
@@ -88,11 +88,16 @@ export const useVersionCompatibilityStore = defineStore(
|
|||||||
return Date.now() < dismissedUntil
|
return Date.now() < dismissedUntil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const warningsDisabled = computed(() =>
|
||||||
|
settingStore.get('Comfy.VersionCompatibility.DisableWarnings')
|
||||||
|
)
|
||||||
|
|
||||||
const shouldShowWarning = computed(() => {
|
const shouldShowWarning = computed(() => {
|
||||||
const warningsDisabled = settingStore.get(
|
return (
|
||||||
'Comfy.VersionCompatibility.DisableWarnings'
|
hasVersionMismatch.value &&
|
||||||
|
!isDismissed.value &&
|
||||||
|
!warningsDisabled.value
|
||||||
)
|
)
|
||||||
return hasVersionMismatch.value && !isDismissed.value && !warningsDisabled
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const warningMessage = computed(() => {
|
const warningMessage = computed(() => {
|
||||||
|
|||||||
@@ -12,11 +12,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="isRecording || isPlaying || recordedURL"
|
v-if="isRecording || isPlaying || recordedURL"
|
||||||
class="flex h-14 w-full items-center gap-4 rounded-lg px-4 bg-node-component-surface text-text-secondary"
|
class="flex h-14 w-full min-w-0 items-center gap-2 rounded-lg px-3 bg-node-component-surface text-text-secondary"
|
||||||
>
|
>
|
||||||
<!-- Recording Status -->
|
<!-- Recording Status -->
|
||||||
<div class="flex min-w-30 items-center gap-2">
|
<div class="flex shrink-0 items-center gap-1">
|
||||||
<span class="min-w-20 text-xs">
|
<span class="text-xs">
|
||||||
{{
|
{{
|
||||||
isRecording
|
isRecording
|
||||||
? t('g.listening', 'Listening...')
|
? t('g.listening', 'Listening...')
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
: ''
|
: ''
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span class="min-w-10 text-sm">{{ formatTime(timer) }}</span>
|
<span class="text-sm">{{ formatTime(timer) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Waveform Visualization -->
|
<!-- Waveform Visualization -->
|
||||||
<div class="flex h-8 flex-1 items-center gap-2 overflow-x-clip">
|
<div class="flex h-8 min-w-0 flex-1 items-center gap-2 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
v-for="(bar, index) in waveformBars"
|
v-for="(bar, index) in waveformBars"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
<button
|
<button
|
||||||
v-if="isRecording"
|
v-if="isRecording"
|
||||||
:title="t('g.stopRecording', 'Stop Recording')"
|
:title="t('g.stopRecording', 'Stop Recording')"
|
||||||
class="flex size-8 animate-pulse items-center justify-center rounded-full border-0 bg-smoke-500/33 transition-colors"
|
class="flex shrink-0 size-8 animate-pulse items-center justify-center rounded-full border-0 bg-smoke-500/33 transition-colors"
|
||||||
@click="handleStopRecording"
|
@click="handleStopRecording"
|
||||||
>
|
>
|
||||||
<div class="size-2.5 rounded-sm bg-danger-100" />
|
<div class="size-2.5 rounded-sm bg-danger-100" />
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
<button
|
<button
|
||||||
v-else-if="!isRecording && recordedURL && !isPlaying"
|
v-else-if="!isRecording && recordedURL && !isPlaying"
|
||||||
:title="t('g.playRecording') || 'Play Recording'"
|
:title="t('g.playRecording') || 'Play Recording'"
|
||||||
class="flex size-8 items-center justify-center rounded-full border-0 bg-smoke-500/33 transition-colors"
|
class="flex shrink-0 size-8 items-center justify-center rounded-full border-0 bg-smoke-500/33 transition-colors"
|
||||||
@click="handlePlayRecording"
|
@click="handlePlayRecording"
|
||||||
>
|
>
|
||||||
<i class="text-text-secondary icon-[lucide--play] size-4" />
|
<i class="text-text-secondary icon-[lucide--play] size-4" />
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
<button
|
<button
|
||||||
v-else-if="isPlaying"
|
v-else-if="isPlaying"
|
||||||
:title="t('g.stopPlayback') || 'Stop Playback'"
|
:title="t('g.stopPlayback') || 'Stop Playback'"
|
||||||
class="flex size-8 items-center justify-center rounded-full border-0 bg-smoke-500/33 transition-colors"
|
class="flex shrink-0 size-8 items-center justify-center rounded-full border-0 bg-smoke-500/33 transition-colors"
|
||||||
@click="handleStopPlayback"
|
@click="handleStopPlayback"
|
||||||
>
|
>
|
||||||
<i class="text-text-secondary icon-[lucide--square] size-4" />
|
<i class="text-text-secondary icon-[lucide--square] size-4" />
|
||||||
|
|||||||
@@ -299,9 +299,10 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
|||||||
|
|
||||||
const nodeDefs = computed(() => {
|
const nodeDefs = computed(() => {
|
||||||
const subgraphStore = useSubgraphStore()
|
const subgraphStore = useSubgraphStore()
|
||||||
|
// Blueprints first for discoverability in the node library sidebar
|
||||||
return [
|
return [
|
||||||
...Object.values(nodeDefsByName.value),
|
...subgraphStore.subgraphBlueprints,
|
||||||
...subgraphStore.subgraphBlueprints
|
...Object.values(nodeDefsByName.value)
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
const nodeDataTypes = computed(() => {
|
const nodeDataTypes = computed(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user