mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-15 01:48:06 +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', {
|
||||
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
|
||||
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",
|
||||
"private": true,
|
||||
"version": "1.37.10",
|
||||
"version": "1.37.11",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"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 { 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 = {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
@@ -209,13 +220,24 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => {
|
||||
const resolutionWidget = node.widgets?.find(
|
||||
(w) => w.name === 'resolution'
|
||||
) as IComboWidget
|
||||
const generateAudioWidget = node.widgets?.find(
|
||||
(w) => w.name === 'generate_audio'
|
||||
) as IComboWidget | undefined
|
||||
|
||||
if (!modelWidget || !durationWidget || !resolutionWidget) return 'Token-based'
|
||||
|
||||
const model = String(modelWidget.value).toLowerCase()
|
||||
const resolution = String(resolutionWidget.value).toLowerCase()
|
||||
const seconds = parseFloat(String(durationWidget.value))
|
||||
const generateAudio =
|
||||
generateAudioWidget &&
|
||||
String(generateAudioWidget.value).toLowerCase() === 'true'
|
||||
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': {
|
||||
'480p': [0.23, 0.24],
|
||||
'720p': [0.51, 0.56],
|
||||
@@ -233,13 +255,15 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => {
|
||||
}
|
||||
}
|
||||
|
||||
const modelKey = model.includes('seedance-1-0-pro-fast')
|
||||
? 'seedance-1-0-pro-fast'
|
||||
: model.includes('seedance-1-0-pro')
|
||||
? 'seedance-1-0-pro'
|
||||
: model.includes('seedance-1-0-lite')
|
||||
? 'seedance-1-0-lite'
|
||||
: ''
|
||||
const modelKey = model.includes('seedance-1-5-pro')
|
||||
? 'seedance-1-5-pro'
|
||||
: model.includes('seedance-1-0-pro-fast')
|
||||
? 'seedance-1-0-pro-fast'
|
||||
: model.includes('seedance-1-0-pro')
|
||||
? 'seedance-1-0-pro'
|
||||
: model.includes('seedance-1-0-lite')
|
||||
? 'seedance-1-0-lite'
|
||||
: ''
|
||||
|
||||
const resKey = resolution.includes('1080')
|
||||
? '1080p'
|
||||
@@ -255,8 +279,10 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => {
|
||||
|
||||
const [min10s, max10s] = baseRange
|
||||
const scale = seconds / 10
|
||||
const minCost = min10s * scale
|
||||
const maxCost = max10s * scale
|
||||
const audioMultiplier =
|
||||
modelKey === 'seedance-1-5-pro' && generateAudio ? 2 : 1
|
||||
const minCost = min10s * scale * audioMultiplier
|
||||
const maxCost = max10s * scale * audioMultiplier
|
||||
|
||||
if (minCost === maxCost) return formatCreditsLabel(minCost)
|
||||
return formatCreditsRangeLabel(minCost, maxCost)
|
||||
@@ -525,6 +551,54 @@ const calculateTripo3DGenerationPrice = (
|
||||
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
|
||||
*/
|
||||
@@ -1812,6 +1886,27 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
|
||||
TripoRefineNode: {
|
||||
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
|
||||
GeminiNode: {
|
||||
displayPrice: (node: LGraphNode): string => {
|
||||
@@ -2527,6 +2622,9 @@ export const useNodePricing = () => {
|
||||
'animate_in_place'
|
||||
],
|
||||
TripoTextureNode: ['texture_quality'],
|
||||
// Meshy nodes
|
||||
MeshyImageToModelNode: ['should_texture'],
|
||||
MeshyMultiImageToModelNode: ['should_texture'],
|
||||
// Google/Gemini nodes
|
||||
GeminiNode: ['model'],
|
||||
GeminiImage2Node: ['resolution'],
|
||||
@@ -2540,9 +2638,24 @@ export const useNodePricing = () => {
|
||||
'sequential_image_generation',
|
||||
'max_images'
|
||||
],
|
||||
ByteDanceTextToVideoNode: ['model', 'duration', 'resolution'],
|
||||
ByteDanceImageToVideoNode: ['model', 'duration', 'resolution'],
|
||||
ByteDanceFirstLastFrameNode: ['model', 'duration', 'resolution'],
|
||||
ByteDanceTextToVideoNode: [
|
||||
'model',
|
||||
'duration',
|
||||
'resolution',
|
||||
'generate_audio'
|
||||
],
|
||||
ByteDanceImageToVideoNode: [
|
||||
'model',
|
||||
'duration',
|
||||
'resolution',
|
||||
'generate_audio'
|
||||
],
|
||||
ByteDanceFirstLastFrameNode: [
|
||||
'model',
|
||||
'duration',
|
||||
'resolution',
|
||||
'generate_audio'
|
||||
],
|
||||
ByteDanceImageReferenceNode: ['model', 'duration', 'resolution'],
|
||||
WanTextToVideoApi: ['duration', 'size'],
|
||||
WanImageToVideoApi: ['duration', 'resolution'],
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMuta
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
import { removeNodeTitleHeight } from '@/renderer/core/layout/utils/nodeSizeUtil'
|
||||
import { forEachNode } from '@/utils/graphTraversalUtil'
|
||||
|
||||
import { CanvasPointer } from './CanvasPointer'
|
||||
import type { ContextMenu } from './ContextMenu'
|
||||
@@ -4041,6 +4042,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
layoutStore.batchUpdateNodeBounds(newPositions)
|
||||
|
||||
this.selectItems(created)
|
||||
forEachNode(graph, (n) => n.onGraphConfigured?.())
|
||||
forEachNode(graph, (n) => n.onAfterGraphConfigured?.())
|
||||
|
||||
graph.afterChange()
|
||||
this.emitAfterChange()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { whenever } from '@vueuse/core'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { computed, nextTick, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useToastStore } from './toastStore'
|
||||
@@ -65,9 +65,12 @@ export function useFrontendVersionMismatchWarning(
|
||||
versionCompatibilityStore.dismissWarning()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
// Only set up the watcher if immediate is true
|
||||
if (immediate) {
|
||||
// Wait for next tick to ensure reactive updates from settings load have propagated
|
||||
await nextTick()
|
||||
|
||||
whenever(
|
||||
() => versionCompatibilityStore.shouldShowWarning,
|
||||
() => {
|
||||
|
||||
@@ -88,11 +88,16 @@ export const useVersionCompatibilityStore = defineStore(
|
||||
return Date.now() < dismissedUntil
|
||||
})
|
||||
|
||||
const warningsDisabled = computed(() =>
|
||||
settingStore.get('Comfy.VersionCompatibility.DisableWarnings')
|
||||
)
|
||||
|
||||
const shouldShowWarning = computed(() => {
|
||||
const warningsDisabled = settingStore.get(
|
||||
'Comfy.VersionCompatibility.DisableWarnings'
|
||||
return (
|
||||
hasVersionMismatch.value &&
|
||||
!isDismissed.value &&
|
||||
!warningsDisabled.value
|
||||
)
|
||||
return hasVersionMismatch.value && !isDismissed.value && !warningsDisabled
|
||||
})
|
||||
|
||||
const warningMessage = computed(() => {
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
</div>
|
||||
<div
|
||||
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 -->
|
||||
<div class="flex min-w-30 items-center gap-2">
|
||||
<span class="min-w-20 text-xs">
|
||||
<div class="flex shrink-0 items-center gap-1">
|
||||
<span class="text-xs">
|
||||
{{
|
||||
isRecording
|
||||
? t('g.listening', 'Listening...')
|
||||
@@ -27,11 +27,11 @@
|
||||
: ''
|
||||
}}
|
||||
</span>
|
||||
<span class="min-w-10 text-sm">{{ formatTime(timer) }}</span>
|
||||
<span class="text-sm">{{ formatTime(timer) }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 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
|
||||
v-for="(bar, index) in waveformBars"
|
||||
:key="index"
|
||||
@@ -45,7 +45,7 @@
|
||||
<button
|
||||
v-if="isRecording"
|
||||
: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"
|
||||
>
|
||||
<div class="size-2.5 rounded-sm bg-danger-100" />
|
||||
@@ -54,7 +54,7 @@
|
||||
<button
|
||||
v-else-if="!isRecording && recordedURL && !isPlaying"
|
||||
: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"
|
||||
>
|
||||
<i class="text-text-secondary icon-[lucide--play] size-4" />
|
||||
@@ -63,7 +63,7 @@
|
||||
<button
|
||||
v-else-if="isPlaying"
|
||||
: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"
|
||||
>
|
||||
<i class="text-text-secondary icon-[lucide--square] size-4" />
|
||||
|
||||
@@ -299,9 +299,10 @@ export const useNodeDefStore = defineStore('nodeDef', () => {
|
||||
|
||||
const nodeDefs = computed(() => {
|
||||
const subgraphStore = useSubgraphStore()
|
||||
// Blueprints first for discoverability in the node library sidebar
|
||||
return [
|
||||
...Object.values(nodeDefsByName.value),
|
||||
...subgraphStore.subgraphBlueprints
|
||||
...subgraphStore.subgraphBlueprints,
|
||||
...Object.values(nodeDefsByName.value)
|
||||
]
|
||||
})
|
||||
const nodeDataTypes = computed(() => {
|
||||
|
||||
Reference in New Issue
Block a user