@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.
- package/lib/client.js +994 -161
- package/lib/comfy-workflow-helpers.js +161 -0
- package/lib/fallback-router.js +25 -3
- package/lib/index.js +259 -138
- package/lib/provider-utils.js +0 -2
- package/lib/providers/backends/local.js +33 -11
- package/lib/providers.js +9 -30
- package/lib/register-tools.js +3 -1
- package/lib/settings-route.js +112 -0
- package/lib/theme-pair-helpers.js +83 -0
- package/lib/tools/anchor.js +1 -2
- package/lib/tools/editing.js +2 -34
- package/lib/tools/frontend.js +2 -35
- package/lib/tools/generation-pack.js +2 -4
- package/lib/tools/generation.js +2 -23
- package/lib/tools/inspect.js +6 -45
- package/lib/tools/pattern.js +10 -0
- package/lib/tools/processing-advanced.js +0 -32
- package/lib/tools/processing-basic.js +2 -25
- package/lib/tools/style-matrix.js +1 -2
- package/lib/tools/theme-pair.js +250 -0
- package/lib/tools/ui-asset.js +1 -1
- package/lib/updater.js +0 -9
- package/lib/vault.js +183 -0
- package/package.json +7 -1
|
@@ -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
|
+
}
|
package/lib/fallback-router.js
CHANGED
|
@@ -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
|
}
|