api_nodes: added prices for Ideogram V3 node (character reference) (#5241)

* api_nodes: added prices for Ideogram V3 node (character reference)

* Support watching changes on connections. (#5250)

* rename renderingSpeed default value from 'balanced' to 'default'

* added missing type

---------

Co-authored-by: AustinMroz <austin@comfy.org>
This commit is contained in:
Alexander Piskun
2025-09-01 20:54:36 +03:00
committed by GitHub
parent ddd7b4866f
commit 1e6ba5c689
3 changed files with 128 additions and 35 deletions

View File

@@ -1,4 +1,4 @@
import type { 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'
/** /**
@@ -179,6 +179,12 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
const numImagesWidget = node.widgets?.find( const numImagesWidget = node.widgets?.find(
(w) => w.name === 'num_images' (w) => w.name === 'num_images'
) as IComboWidget ) as IComboWidget
const characterInput = node.inputs?.find(
(i) => i.name === 'character_image'
) as INodeInputSlot
const hasCharacter =
typeof characterInput?.link !== 'undefined' &&
characterInput.link != null
if (!renderingSpeedWidget) if (!renderingSpeedWidget)
return '$0.03-0.08 x num_images/Run (varies with rendering speed & num_images)' return '$0.03-0.08 x num_images/Run (varies with rendering speed & num_images)'
@@ -188,11 +194,23 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
const renderingSpeed = String(renderingSpeedWidget.value) const renderingSpeed = String(renderingSpeedWidget.value)
if (renderingSpeed.toLowerCase().includes('quality')) { if (renderingSpeed.toLowerCase().includes('quality')) {
basePrice = 0.09 if (hasCharacter) {
} else if (renderingSpeed.toLowerCase().includes('balanced')) { basePrice = 0.2
basePrice = 0.06 } else {
basePrice = 0.09
}
} else if (renderingSpeed.toLowerCase().includes('default')) {
if (hasCharacter) {
basePrice = 0.15
} else {
basePrice = 0.06
}
} else if (renderingSpeed.toLowerCase().includes('turbo')) { } else if (renderingSpeed.toLowerCase().includes('turbo')) {
basePrice = 0.03 if (hasCharacter) {
basePrice = 0.1
} else {
basePrice = 0.03
}
} }
const totalCost = (basePrice * numImages).toFixed(2) const totalCost = (basePrice * numImages).toFixed(2)
@@ -1462,7 +1480,7 @@ export const useNodePricing = () => {
OpenAIGPTImage1: ['quality', 'n'], OpenAIGPTImage1: ['quality', 'n'],
IdeogramV1: ['num_images', 'turbo'], IdeogramV1: ['num_images', 'turbo'],
IdeogramV2: ['num_images', 'turbo'], IdeogramV2: ['num_images', 'turbo'],
IdeogramV3: ['rendering_speed', 'num_images'], IdeogramV3: ['rendering_speed', 'num_images', 'character_image'],
FluxProKontextProNode: [], FluxProKontextProNode: [],
FluxProKontextMaxNode: [], FluxProKontextMaxNode: [],
VeoVideoGenerationNode: ['duration_seconds'], VeoVideoGenerationNode: ['duration_seconds'],

View File

@@ -75,6 +75,29 @@ export const useComputedWithWidgetWatch = (
} }
}) })
}) })
if (widgetNames && widgetNames.length > widgetsToObserve.length) {
//Inputs have been included
const indexesToObserve = widgetNames
.map((name) =>
widgetsToObserve.some((w) => w.name == name)
? -1
: node.inputs.findIndex((i) => i.name == name)
)
.filter((i) => i >= 0)
node.onConnectionsChange = useChainCallback(
node.onConnectionsChange,
(_type: unknown, index: number, isConnected: boolean) => {
if (!indexesToObserve.includes(index)) return
widgetValues.value = {
...widgetValues.value,
[indexesToObserve[index]]: isConnected
}
if (triggerCanvasRedraw) {
node.graph?.setDirtyCanvas(true, true)
}
}
)
}
} }
// Returns a function that creates a computed that responds to widget changes. // Returns a function that creates a computed that responds to widget changes.

View File

@@ -8,7 +8,12 @@ import type { IComboWidget } from '@/lib/litegraph/src/types/widgets'
function createMockNode( function createMockNode(
nodeTypeName: string, nodeTypeName: string,
widgets: Array<{ name: string; value: any }> = [], widgets: Array<{ name: string; value: any }> = [],
isApiNode = true isApiNode = true,
inputs: Array<{
name: string
connected?: boolean
useLinksArray?: boolean
}> = []
): LGraphNode { ): LGraphNode {
const mockWidgets = widgets.map(({ name, value }) => ({ const mockWidgets = widgets.map(({ name, value }) => ({
name, name,
@@ -16,7 +21,16 @@ function createMockNode(
type: 'combo' type: 'combo'
})) as IComboWidget[] })) as IComboWidget[]
return { const mockInputs =
inputs.length > 0
? inputs.map(({ name, connected, useLinksArray }) =>
useLinksArray
? { name, links: connected ? [1] : [] }
: { name, link: connected ? 1 : null }
)
: undefined
const node: any = {
id: Math.random().toString(), id: Math.random().toString(),
widgets: mockWidgets, widgets: mockWidgets,
constructor: { constructor: {
@@ -25,7 +39,24 @@ function createMockNode(
api_node: isApiNode api_node: isApiNode
} }
} }
} as unknown as LGraphNode }
if (mockInputs) {
node.inputs = mockInputs
// Provide the common helpers some frontend code may call
node.findInputSlot = function (portName: string) {
return this.inputs?.findIndex((i: any) => i.name === portName) ?? -1
}
node.isInputConnected = function (idx: number) {
const port = this.inputs?.[idx]
if (!port) return false
if (typeof port.link !== 'undefined') return port.link != null
if (Array.isArray(port.links)) return port.links.length > 0
return false
}
}
return node as LGraphNode
} }
describe('useNodePricing', () => { describe('useNodePricing', () => {
@@ -363,34 +394,51 @@ describe('useNodePricing', () => {
}) })
describe('dynamic pricing - IdeogramV3', () => { describe('dynamic pricing - IdeogramV3', () => {
it('should return $0.09 for Quality rendering speed', () => { it('should return correct prices for IdeogramV3 node', () => {
const { getNodeDisplayPrice } = useNodePricing() const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('IdeogramV3', [
{ name: 'rendering_speed', value: 'Quality' }
])
const price = getNodeDisplayPrice(node) const testCases = [
expect(price).toBe('$0.09/Run') {
}) rendering_speed: 'Quality',
character_image: false,
expected: '$0.09/Run'
},
{
rendering_speed: 'Quality',
character_image: true,
expected: '$0.20/Run'
},
{
rendering_speed: 'Default',
character_image: false,
expected: '$0.06/Run'
},
{
rendering_speed: 'Default',
character_image: true,
expected: '$0.15/Run'
},
{
rendering_speed: 'Turbo',
character_image: false,
expected: '$0.03/Run'
},
{
rendering_speed: 'Turbo',
character_image: true,
expected: '$0.10/Run'
}
]
it('should return $0.06 for Balanced rendering speed', () => { testCases.forEach(({ rendering_speed, character_image, expected }) => {
const { getNodeDisplayPrice } = useNodePricing() const node = createMockNode(
const node = createMockNode('IdeogramV3', [ 'IdeogramV3',
{ name: 'rendering_speed', value: 'Balanced' } [{ name: 'rendering_speed', value: rendering_speed }],
]) true,
[{ name: 'character_image', connected: character_image }]
const price = getNodeDisplayPrice(node) )
expect(price).toBe('$0.06/Run') expect(getNodeDisplayPrice(node)).toBe(expected)
}) })
it('should return $0.03 for Turbo rendering speed', () => {
const { getNodeDisplayPrice } = useNodePricing()
const node = createMockNode('IdeogramV3', [
{ name: 'rendering_speed', value: 'Turbo' }
])
const price = getNodeDisplayPrice(node)
expect(price).toBe('$0.03/Run')
}) })
it('should return range when rendering_speed widget is missing', () => { it('should return range when rendering_speed widget is missing', () => {
@@ -935,7 +983,11 @@ describe('useNodePricing', () => {
const { getRelevantWidgetNames } = useNodePricing() const { getRelevantWidgetNames } = useNodePricing()
const widgetNames = getRelevantWidgetNames('IdeogramV3') const widgetNames = getRelevantWidgetNames('IdeogramV3')
expect(widgetNames).toEqual(['rendering_speed', 'num_images']) expect(widgetNames).toEqual([
'rendering_speed',
'num_images',
'character_image'
])
}) })
}) })