Skip to content

Commit 0f8a6bc

Browse files
[fix] Add missing Tripo nodes and improve Runway pricing logic
- Add TripoConvertModelNode ($0.10/Run) - Add TripoRetargetRiggedModelNode ($0.10/Run) - Add TripoMultiviewToModelNode with dynamic pricing ($0.20-$0.50/Run) - Fix Runway duration pricing to correctly handle 0 duration (now $0.00/Run instead of defaulting to 5 seconds) - Add helper function calculateRunwayDurationPrice for consistent behavior - Update tests to verify new pricing logic Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 2f904b9 commit 0f8a6bc

File tree

2 files changed

+153
-34
lines changed

2 files changed

+153
-34
lines changed

src/composables/node/useNodePricing.ts

Lines changed: 84 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,25 @@ function safePricingExecution(
3030
}
3131
}
3232

33+
/**
34+
* Helper function to calculate Runway duration-based pricing
35+
* @param node - The LiteGraph node
36+
* @returns Formatted price string
37+
*/
38+
const calculateRunwayDurationPrice = (node: LGraphNode): string => {
39+
const durationWidget = node.widgets?.find(
40+
(w) => w.name === 'duration'
41+
) as IComboWidget
42+
43+
if (!durationWidget) return '$0.05/second'
44+
45+
const duration = Number(durationWidget.value)
46+
// If duration is 0 or NaN, don't fall back to 5 seconds - just use 0
47+
const validDuration = isNaN(duration) ? 5 : duration
48+
const cost = (0.05 * validDuration).toFixed(2)
49+
return `$${cost}/Run`
50+
}
51+
3352
const pixversePricingCalculator = (node: LGraphNode): string => {
3453
const durationWidget = node.widgets?.find(
3554
(w) => w.name === 'duration_seconds'
@@ -1007,43 +1026,13 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
10071026
displayPrice: '$0.08/Run'
10081027
},
10091028
RunwayImageToVideoNodeGen3a: {
1010-
displayPrice: (node: LGraphNode): string => {
1011-
const durationWidget = node.widgets?.find(
1012-
(w) => w.name === 'duration'
1013-
) as IComboWidget
1014-
1015-
if (!durationWidget) return '$0.05/second'
1016-
1017-
const duration = Number(durationWidget.value) || 5
1018-
const cost = (0.05 * duration).toFixed(2)
1019-
return `$${cost}/Run`
1020-
}
1029+
displayPrice: calculateRunwayDurationPrice
10211030
},
10221031
RunwayImageToVideoNodeGen4: {
1023-
displayPrice: (node: LGraphNode): string => {
1024-
const durationWidget = node.widgets?.find(
1025-
(w) => w.name === 'duration'
1026-
) as IComboWidget
1027-
1028-
if (!durationWidget) return '$0.05/second'
1029-
1030-
const duration = Number(durationWidget.value) || 5
1031-
const cost = (0.05 * duration).toFixed(2)
1032-
return `$${cost}/Run`
1033-
}
1032+
displayPrice: calculateRunwayDurationPrice
10341033
},
10351034
RunwayFirstLastFrameNode: {
1036-
displayPrice: (node: LGraphNode): string => {
1037-
const durationWidget = node.widgets?.find(
1038-
(w) => w.name === 'duration'
1039-
) as IComboWidget
1040-
1041-
if (!durationWidget) return '$0.05/second'
1042-
1043-
const duration = Number(durationWidget.value) || 5
1044-
const cost = (0.05 * duration).toFixed(2)
1045-
return `$${cost}/Run`
1046-
}
1035+
displayPrice: calculateRunwayDurationPrice
10471036
},
10481037
// Rodin nodes - all have the same pricing structure
10491038
Rodin3D_Regular: {
@@ -1186,6 +1175,68 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
11861175
return textureQuality.includes('detailed') ? '$0.2/Run' : '$0.1/Run'
11871176
}
11881177
},
1178+
TripoConvertModelNode: {
1179+
displayPrice: '$0.10/Run'
1180+
},
1181+
TripoRetargetRiggedModelNode: {
1182+
displayPrice: '$0.10/Run'
1183+
},
1184+
TripoMultiviewToModelNode: {
1185+
displayPrice: (node: LGraphNode): string => {
1186+
const quadWidget = node.widgets?.find(
1187+
(w) => w.name === 'quad'
1188+
) as IComboWidget
1189+
const styleWidget = node.widgets?.find(
1190+
(w) => w.name === 'style'
1191+
) as IComboWidget
1192+
const textureWidget = node.widgets?.find(
1193+
(w) => w.name === 'texture'
1194+
) as IComboWidget
1195+
const textureQualityWidget = node.widgets?.find(
1196+
(w) => w.name === 'texture_quality'
1197+
) as IComboWidget
1198+
1199+
if (!quadWidget || !styleWidget || !textureWidget)
1200+
return '$0.2-0.5/Run (varies with quad, style, texture & quality)'
1201+
1202+
const quad = String(quadWidget.value).toLowerCase() === 'true'
1203+
const style = String(styleWidget.value).toLowerCase()
1204+
const texture = String(textureWidget.value).toLowerCase() === 'true'
1205+
const textureQuality = String(
1206+
textureQualityWidget?.value || 'standard'
1207+
).toLowerCase()
1208+
1209+
// Pricing logic based on CSV data for Multiview to Model (same as Image to Model)
1210+
if (style.includes('none')) {
1211+
if (!quad) {
1212+
if (!texture) return '$0.20/Run'
1213+
else return '$0.25/Run'
1214+
} else {
1215+
if (textureQuality.includes('detailed')) {
1216+
if (!texture) return '$0.40/Run'
1217+
else return '$0.45/Run'
1218+
} else {
1219+
if (!texture) return '$0.30/Run'
1220+
else return '$0.35/Run'
1221+
}
1222+
}
1223+
} else {
1224+
// any style
1225+
if (!quad) {
1226+
if (!texture) return '$0.25/Run'
1227+
else return '$0.30/Run'
1228+
} else {
1229+
if (textureQuality.includes('detailed')) {
1230+
if (!texture) return '$0.45/Run'
1231+
else return '$0.50/Run'
1232+
} else {
1233+
if (!texture) return '$0.35/Run'
1234+
else return '$0.40/Run'
1235+
}
1236+
}
1237+
}
1238+
}
1239+
},
11891240
// Google/Gemini nodes
11901241
GeminiNode: {
11911242
displayPrice: (node: LGraphNode): string => {

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

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1077,6 +1077,26 @@ describe('useNodePricing', () => {
10771077
const price = getNodeDisplayPrice(node)
10781078
expect(price).toBe('$0.05/second')
10791079
})
1080+
1081+
it('should handle zero duration for RunwayImageToVideoNodeGen3a', () => {
1082+
const { getNodeDisplayPrice } = useNodePricing()
1083+
const node = createMockNode('RunwayImageToVideoNodeGen3a', [
1084+
{ name: 'duration', value: 0 }
1085+
])
1086+
1087+
const price = getNodeDisplayPrice(node)
1088+
expect(price).toBe('$0.00/Run') // 0.05 * 0 = 0
1089+
})
1090+
1091+
it('should handle NaN duration for RunwayImageToVideoNodeGen3a', () => {
1092+
const { getNodeDisplayPrice } = useNodePricing()
1093+
const node = createMockNode('RunwayImageToVideoNodeGen3a', [
1094+
{ name: 'duration', value: 'invalid' }
1095+
])
1096+
1097+
const price = getNodeDisplayPrice(node)
1098+
expect(price).toBe('$0.25/Run') // Falls back to 5 seconds: 0.05 * 5
1099+
})
10801100
})
10811101

10821102
describe('Rodin nodes', () => {
@@ -1220,6 +1240,54 @@ describe('useNodePricing', () => {
12201240
expect(getNodeDisplayPrice(node)).toBe(expected)
12211241
})
12221242
})
1243+
1244+
it('should return static price for TripoConvertModelNode', () => {
1245+
const { getNodeDisplayPrice } = useNodePricing()
1246+
const node = createMockNode('TripoConvertModelNode')
1247+
1248+
const price = getNodeDisplayPrice(node)
1249+
expect(price).toBe('$0.10/Run')
1250+
})
1251+
1252+
it('should return static price for TripoRetargetRiggedModelNode', () => {
1253+
const { getNodeDisplayPrice } = useNodePricing()
1254+
const node = createMockNode('TripoRetargetRiggedModelNode')
1255+
1256+
const price = getNodeDisplayPrice(node)
1257+
expect(price).toBe('$0.10/Run')
1258+
})
1259+
1260+
it('should return dynamic pricing for TripoMultiviewToModelNode', () => {
1261+
const { getNodeDisplayPrice } = useNodePricing()
1262+
1263+
// Test basic case - no style, no quad, no texture
1264+
const basicNode = createMockNode('TripoMultiviewToModelNode', [
1265+
{ name: 'quad', value: false },
1266+
{ name: 'style', value: 'none' },
1267+
{ name: 'texture', value: false },
1268+
{ name: 'texture_quality', value: 'standard' }
1269+
])
1270+
expect(getNodeDisplayPrice(basicNode)).toBe('$0.20/Run')
1271+
1272+
// Test high-end case - any style, quad, texture, detailed
1273+
const highEndNode = createMockNode('TripoMultiviewToModelNode', [
1274+
{ name: 'quad', value: true },
1275+
{ name: 'style', value: 'stylized' },
1276+
{ name: 'texture', value: true },
1277+
{ name: 'texture_quality', value: 'detailed' }
1278+
])
1279+
expect(getNodeDisplayPrice(highEndNode)).toBe('$0.50/Run')
1280+
})
1281+
1282+
it('should return fallback for TripoMultiviewToModelNode without widgets', () => {
1283+
const { getNodeDisplayPrice } = useNodePricing()
1284+
const node = createMockNode('TripoMultiviewToModelNode', [])
1285+
1286+
const price = getNodeDisplayPrice(node)
1287+
expect(price).toBe(
1288+
'$0.2-0.5/Run (varies with quad, style, texture & quality)'
1289+
)
1290+
})
12231291
})
12241292

12251293
describe('Gemini and OpenAI Chat nodes', () => {
@@ -1340,7 +1408,7 @@ describe('useNodePricing', () => {
13401408

13411409
// Test edge cases
13421410
const testCases = [
1343-
{ duration: 0, expected: '$0.25/Run' }, // Falls back to 5 seconds (0 || 5)
1411+
{ duration: 0, expected: '$0.00/Run' }, // Now correctly handles 0 duration
13441412
{ duration: 1, expected: '$0.05/Run' },
13451413
{ duration: 30, expected: '$1.50/Run' }
13461414
]

0 commit comments

Comments
 (0)