@goodandready/dsh-image-gen 0.10.35 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,161 @@
1
+ // comfy-workflow-helpers.js — ComfyUI workflow JSON parser and placeholder interpolator (#157)
2
+
3
+ /**
4
+ * Validates whether an input is a valid ComfyUI API workflow object.
5
+ * An API workflow is either { [nodeId]: { class_type, inputs } } or { prompt: { [nodeId]: ... } }.
6
+ *
7
+ * @param {any} raw
8
+ * @returns {object} normalized prompt graph { [nodeId]: { class_type, inputs } }
9
+ */
10
+ export function validateComfyWorkflow(raw) {
11
+ let graph = raw
12
+ if (typeof raw === 'string') {
13
+ const trimmed = raw.trim()
14
+ if (!trimmed) throw new Error('ComfyUI workflow JSON is empty')
15
+ try {
16
+ graph = JSON.parse(trimmed)
17
+ } catch (e) {
18
+ throw new Error(`ComfyUI workflow is not valid JSON: ${e.message}`)
19
+ }
20
+ }
21
+
22
+ if (!graph || typeof graph !== 'object' || Array.isArray(graph)) {
23
+ throw new Error('ComfyUI workflow must be a JSON object')
24
+ }
25
+
26
+ // Handle nested { prompt: { ... } } wrapper
27
+ if (graph.prompt && typeof graph.prompt === 'object' && !Array.isArray(graph.prompt)) {
28
+ graph = graph.prompt
29
+ }
30
+
31
+ const nodeIds = Object.keys(graph)
32
+ if (nodeIds.length === 0) {
33
+ throw new Error('ComfyUI workflow contains no nodes')
34
+ }
35
+
36
+ for (const id of nodeIds) {
37
+ const node = graph[id]
38
+ if (!node || typeof node !== 'object' || Array.isArray(node)) {
39
+ throw new Error(`ComfyUI node "${id}" must be an object`)
40
+ }
41
+ if (!node.class_type || typeof node.class_type !== 'string') {
42
+ throw new Error(`ComfyUI node "${id}" missing required "class_type" property`)
43
+ }
44
+ }
45
+
46
+ return graph
47
+ }
48
+
49
+ /**
50
+ * Recursively replaces placeholders in a ComfyUI workflow template.
51
+ * Supported placeholders:
52
+ * - {{prompt}}
53
+ * - {{negativePrompt}}
54
+ * - {{seed}} (converted to number when exact match)
55
+ * - {{width}} (converted to number when exact match)
56
+ * - {{height}} (converted to number when exact match)
57
+ * - {{steps}} (converted to number when exact match)
58
+ * - {{cfg}} (converted to number when exact match)
59
+ * - {{model}}
60
+ * - {{image}}
61
+ *
62
+ * @param {object} workflowGraph
63
+ * @param {object} ctx
64
+ * @returns {object} interpolated workflow clone
65
+ */
66
+ export function interpolateComfyWorkflow(workflowGraph, ctx) {
67
+ const promptVal = String(ctx.prompt ?? '')
68
+ const negVal = String(ctx.negativePrompt ?? '')
69
+ const seedVal = Number.isFinite(ctx.seed) ? ctx.seed : 0
70
+ const widthVal = Number.isFinite(ctx.width) ? ctx.width : 1024
71
+ const heightVal = Number.isFinite(ctx.height) ? ctx.height : 1024
72
+ const stepsVal = Number.isFinite(ctx.steps) ? ctx.steps : 20
73
+ const cfgVal = Number.isFinite(ctx.cfg) ? ctx.cfg : 7
74
+ const modelVal = String(ctx.model ?? 'v1-5-pruned-emaonly.safetensors')
75
+ const imageVal = String(ctx.image ?? '')
76
+
77
+ function transformValue(val) {
78
+ if (typeof val === 'string') {
79
+ const trimmed = val.trim()
80
+ // Numeric exact replacements
81
+ if (trimmed === '{{seed}}') return seedVal
82
+ if (trimmed === '{{width}}') return widthVal
83
+ if (trimmed === '{{height}}') return heightVal
84
+ if (trimmed === '{{steps}}') return stepsVal
85
+ if (trimmed === '{{cfg}}') return cfgVal
86
+
87
+ // General string replacements
88
+ return val
89
+ .replace(/\{\{prompt\}\}/g, promptVal)
90
+ .replace(/\{\{negativePrompt\}\}/g, negVal)
91
+ .replace(/\{\{negative_prompt\}\}/g, negVal)
92
+ .replace(/\{\{seed\}\}/g, String(seedVal))
93
+ .replace(/\{\{width\}\}/g, String(widthVal))
94
+ .replace(/\{\{height\}\}/g, String(heightVal))
95
+ .replace(/\{\{steps\}\}/g, String(stepsVal))
96
+ .replace(/\{\{cfg\}\}/g, String(cfgVal))
97
+ .replace(/\{\{model\}\}/g, modelVal)
98
+ .replace(/\{\{image\}\}/g, imageVal)
99
+ }
100
+ if (Array.isArray(val)) {
101
+ return val.map(transformValue)
102
+ }
103
+ if (val && typeof val === 'object') {
104
+ const out = {}
105
+ for (const [k, v] of Object.entries(val)) {
106
+ out[k] = transformValue(v)
107
+ }
108
+ return out
109
+ }
110
+ return val
111
+ }
112
+
113
+ return transformValue(workflowGraph)
114
+ }
115
+
116
+ /**
117
+ * Builds the default 7-node standard ComfyUI workflow graph.
118
+ */
119
+ export function buildDefaultComfyWorkflow({ prompt, negativePrompt, seed, width, height, model, steps, cfg }) {
120
+ return {
121
+ '3': {
122
+ class_type: 'KSampler',
123
+ inputs: {
124
+ seed: seed ?? 0,
125
+ steps: steps ?? 20,
126
+ cfg: cfg ?? 7,
127
+ sampler_name: 'euler',
128
+ scheduler: 'normal',
129
+ denoise: 1,
130
+ model: ['4', 0],
131
+ positive: ['6', 0],
132
+ negative: ['7', 0],
133
+ latent_image: ['5', 0],
134
+ },
135
+ },
136
+ '4': {
137
+ class_type: 'CheckpointLoaderSimple',
138
+ inputs: { ckpt_name: model || 'v1-5-pruned-emaonly.safetensors' },
139
+ },
140
+ '5': {
141
+ class_type: 'EmptyLatentImage',
142
+ inputs: { width, height, batch_size: 1 },
143
+ },
144
+ '6': {
145
+ class_type: 'CLIPTextEncode',
146
+ inputs: { text: prompt || '', clip: ['4', 1] },
147
+ },
148
+ '7': {
149
+ class_type: 'CLIPTextEncode',
150
+ inputs: { text: negativePrompt || '', clip: ['4', 1] },
151
+ },
152
+ '8': {
153
+ class_type: 'VAEDecode',
154
+ inputs: { samples: ['3', 0], vae: ['4', 2] },
155
+ },
156
+ '9': {
157
+ class_type: 'SaveImage',
158
+ inputs: { filename_prefix: 'dsh', images: ['8', 0] },
159
+ },
160
+ }
161
+ }
@@ -35,6 +35,25 @@ export function isFatalPromptError(error) {
35
35
  export function isRetryableProviderError(error) {
36
36
  if (isFatalPromptError(error)) return false
37
37
  const msg = (error?.message || String(error || '')).toLowerCase()
38
+
39
+ // Non-retryable: authentication, authorization, or invalid request / client configuration errors
40
+ if (
41
+ msg.includes('401') ||
42
+ msg.includes('403') ||
43
+ msg.includes('unauthorized') ||
44
+ msg.includes('forbidden') ||
45
+ msg.includes('invalid api key') ||
46
+ msg.includes('invalid key') ||
47
+ msg.includes('api key missing') ||
48
+ msg.includes('api_key_invalid') ||
49
+ msg.includes('bad request') ||
50
+ msg.includes('invalid argument') ||
51
+ msg.includes('invalid parameter') ||
52
+ msg.includes('400')
53
+ ) {
54
+ return false
55
+ }
56
+
38
57
  return (
39
58
  msg.includes('429') ||
40
59
  msg.includes('rate limit') ||
@@ -57,10 +76,9 @@ export function isRetryableProviderError(error) {
57
76
  msg.includes('balance is insufficient') ||
58
77
  msg.includes('quota') ||
59
78
  msg.includes('credit') ||
60
- msg.includes('unauthorized') ||
61
- msg.includes('401') ||
62
- msg.includes('invalid api key') ||
63
79
  msg.includes('econnrefused') ||
80
+ msg.includes('econnreset') ||
81
+ msg.includes('etimedout') ||
64
82
  msg.includes('enotfound') ||
65
83
  msg.includes('fetch failed') ||
66
84
  msg.includes('network')
@@ -150,6 +168,10 @@ export async function executeWithFallback(generators, chain, seed, prompt, optio
150
168
  throw new Error(`Content or policy error on ${providerKey} (not cascading): ${formatted}`)
151
169
  }
152
170
 
171
+ if (!isRetryableProviderError(err)) {
172
+ throw new Error(`Non-retryable provider error on ${providerKey} (not cascading): ${formatted}`)
173
+ }
174
+
153
175
  if (logger && typeof logger.warn === 'function') {
154
176
  logger.warn(`[dsh-image-gen] Provider ${providerKey} failed: ${formatted}. Trying next candidate...`)
155
177
  }