@goodandready/dsh-image-gen 0.11.37 → 0.11.39

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,433 @@
1
+ // lib/comfy-catalog.js — Vetted ComfyUI workflow catalog with dependency validation (#433)
2
+ import { interpolateComfyWorkflow, validateComfyWorkflow } from './comfy-workflow-helpers.js'
3
+
4
+ /**
5
+ * Catalog of vetted ComfyUI workflow templates.
6
+ * Based on official Comfy-Org workflow templates under Apache-2.0 license.
7
+ */
8
+ export const COMFY_TEMPLATES = [
9
+ {
10
+ id: 'txt2img_sdxl',
11
+ name: 'SDXL Text to Image',
12
+ description: 'High-quality direct text-to-image synthesis using SDXL Base architecture.',
13
+ version: '1.0.0',
14
+ source: 'Comfy-Org/workflow_templates',
15
+ license: 'Apache-2.0',
16
+ category: 'generation',
17
+ requiredNodes: [
18
+ 'KSampler',
19
+ 'CheckpointLoaderSimple',
20
+ 'EmptyLatentImage',
21
+ 'CLIPTextEncode',
22
+ 'VAEDecode',
23
+ 'SaveImage',
24
+ ],
25
+ requiredModels: [
26
+ 'sd_xl_base_1.0.safetensors',
27
+ ],
28
+ inputs: [
29
+ { name: 'prompt', type: 'string', required: true, description: 'Positive prompt description' },
30
+ { name: 'negativePrompt', type: 'string', default: 'blurry, low quality, artifacts', description: 'Negative prompt' },
31
+ { name: 'width', type: 'number', default: 1024, description: 'Image width in pixels' },
32
+ { name: 'height', type: 'number', default: 1024, description: 'Image height in pixels' },
33
+ { name: 'steps', type: 'number', default: 25, description: 'Sampling steps' },
34
+ { name: 'cfg', type: 'number', default: 7.0, description: 'Classifier-free guidance scale' },
35
+ { name: 'seed', type: 'number', default: 0, description: 'RNG seed' },
36
+ ],
37
+ graph: {
38
+ '3': {
39
+ class_type: 'KSampler',
40
+ inputs: {
41
+ seed: '{{seed}}',
42
+ steps: '{{steps}}',
43
+ cfg: '{{cfg}}',
44
+ sampler_name: 'euler',
45
+ scheduler: 'normal',
46
+ denoise: 1,
47
+ model: ['4', 0],
48
+ positive: ['6', 0],
49
+ negative: ['7', 0],
50
+ latent_image: ['5', 0],
51
+ },
52
+ },
53
+ '4': {
54
+ class_type: 'CheckpointLoaderSimple',
55
+ inputs: { ckpt_name: '{{model}}' },
56
+ },
57
+ '5': {
58
+ class_type: 'EmptyLatentImage',
59
+ inputs: { width: '{{width}}', height: '{{height}}', batch_size: 1 },
60
+ },
61
+ '6': {
62
+ class_type: 'CLIPTextEncode',
63
+ inputs: { text: '{{prompt}}', clip: ['4', 1] },
64
+ },
65
+ '7': {
66
+ class_type: 'CLIPTextEncode',
67
+ inputs: { text: '{{negativePrompt}}', clip: ['4', 1] },
68
+ },
69
+ '8': {
70
+ class_type: 'VAEDecode',
71
+ inputs: { samples: ['3', 0], vae: ['4', 2] },
72
+ },
73
+ '9': {
74
+ class_type: 'SaveImage',
75
+ inputs: { filename_prefix: 'dsh_sdxl', images: ['8', 0] },
76
+ },
77
+ },
78
+ },
79
+ {
80
+ id: 'img2img_sdxl',
81
+ name: 'SDXL Image to Image Refinement',
82
+ description: 'Refines and restyles existing images with guided prompt and adjustable denoise strength.',
83
+ version: '1.0.0',
84
+ source: 'Comfy-Org/workflow_templates',
85
+ license: 'Apache-2.0',
86
+ category: 'editing',
87
+ requiredNodes: [
88
+ 'KSampler',
89
+ 'CheckpointLoaderSimple',
90
+ 'LoadImage',
91
+ 'VAEEncode',
92
+ 'CLIPTextEncode',
93
+ 'VAEDecode',
94
+ 'SaveImage',
95
+ ],
96
+ requiredModels: [
97
+ 'sd_xl_base_1.0.safetensors',
98
+ ],
99
+ inputs: [
100
+ { name: 'image', type: 'string', required: true, description: 'Base image path or base64' },
101
+ { name: 'prompt', type: 'string', required: true, description: 'Refinement guidance prompt' },
102
+ { name: 'negativePrompt', type: 'string', default: 'low quality, artifacts' },
103
+ { name: 'denoise', type: 'number', default: 0.65, description: 'Denoising transformation strength' },
104
+ { name: 'steps', type: 'number', default: 25 },
105
+ { name: 'cfg', type: 'number', default: 7.0 },
106
+ { name: 'seed', type: 'number', default: 0 },
107
+ ],
108
+ graph: {
109
+ '1': {
110
+ class_type: 'LoadImage',
111
+ inputs: { image: '{{image}}' },
112
+ },
113
+ '2': {
114
+ class_type: 'VAEEncode',
115
+ inputs: { pixels: ['1', 0], vae: ['4', 2] },
116
+ },
117
+ '3': {
118
+ class_type: 'KSampler',
119
+ inputs: {
120
+ seed: '{{seed}}',
121
+ steps: '{{steps}}',
122
+ cfg: '{{cfg}}',
123
+ sampler_name: 'euler',
124
+ scheduler: 'normal',
125
+ denoise: '{{denoise}}',
126
+ model: ['4', 0],
127
+ positive: ['6', 0],
128
+ negative: ['7', 0],
129
+ latent_image: ['2', 0],
130
+ },
131
+ },
132
+ '4': {
133
+ class_type: 'CheckpointLoaderSimple',
134
+ inputs: { ckpt_name: '{{model}}' },
135
+ },
136
+ '6': {
137
+ class_type: 'CLIPTextEncode',
138
+ inputs: { text: '{{prompt}}', clip: ['4', 1] },
139
+ },
140
+ '7': {
141
+ class_type: 'CLIPTextEncode',
142
+ inputs: { text: '{{negativePrompt}}', clip: ['4', 1] },
143
+ },
144
+ '8': {
145
+ class_type: 'VAEDecode',
146
+ inputs: { samples: ['3', 0], vae: ['4', 2] },
147
+ },
148
+ '9': {
149
+ class_type: 'SaveImage',
150
+ inputs: { filename_prefix: 'dsh_img2img', images: ['8', 0] },
151
+ },
152
+ },
153
+ },
154
+ {
155
+ id: 'face_instantid',
156
+ name: 'Facial Identity InstantID Transfer',
157
+ description: 'Transfers facial identity and likeness from reference photo onto synthesized character.',
158
+ version: '1.0.0',
159
+ source: 'InstantID/ComfyUI',
160
+ license: 'Apache-2.0',
161
+ category: 'identity',
162
+ requiredNodes: [
163
+ 'KSampler',
164
+ 'CheckpointLoaderSimple',
165
+ 'LoadImage',
166
+ 'ApplyInstantID',
167
+ 'InstantIDModelLoader',
168
+ 'SaveImage',
169
+ ],
170
+ requiredModels: [
171
+ 'ip-adapter-instantid_sdxl.bin',
172
+ ],
173
+ inputs: [
174
+ { name: 'face_image', type: 'string', required: true, description: 'Source portrait image for identity extraction' },
175
+ { name: 'prompt', type: 'string', required: true, description: 'Scene and clothing prompt' },
176
+ { name: 'face_weight', type: 'number', default: 0.8, description: 'Facial identity similarity weight (0.0 - 1.0)' },
177
+ { name: 'steps', type: 'number', default: 30 },
178
+ { name: 'seed', type: 'number', default: 0 },
179
+ ],
180
+ graph: {
181
+ '1': {
182
+ class_type: 'LoadImage',
183
+ inputs: { image: '{{face_image}}' },
184
+ },
185
+ '2': {
186
+ class_type: 'InstantIDModelLoader',
187
+ inputs: { instantid_file: 'ip-adapter-instantid_sdxl.bin' },
188
+ },
189
+ '3': {
190
+ class_type: 'ApplyInstantID',
191
+ inputs: {
192
+ instantid: ['2', 0],
193
+ image: ['1', 0],
194
+ model: ['4', 0],
195
+ positive: ['6', 0],
196
+ negative: ['7', 0],
197
+ weight: '{{face_weight}}',
198
+ },
199
+ },
200
+ '4': {
201
+ class_type: 'CheckpointLoaderSimple',
202
+ inputs: { ckpt_name: '{{model}}' },
203
+ },
204
+ '5': {
205
+ class_type: 'EmptyLatentImage',
206
+ inputs: { width: 1024, height: 1024, batch_size: 1 },
207
+ },
208
+ '6': {
209
+ class_type: 'CLIPTextEncode',
210
+ inputs: { text: '{{prompt}}', clip: ['4', 1] },
211
+ },
212
+ '7': {
213
+ class_type: 'CLIPTextEncode',
214
+ inputs: { text: '{{negativePrompt}}', clip: ['4', 1] },
215
+ },
216
+ '8': {
217
+ class_type: 'KSampler',
218
+ inputs: {
219
+ seed: '{{seed}}',
220
+ steps: '{{steps}}',
221
+ cfg: 7,
222
+ sampler_name: 'euler',
223
+ scheduler: 'normal',
224
+ denoise: 1,
225
+ model: ['3', 0],
226
+ positive: ['3', 1],
227
+ negative: ['3', 2],
228
+ latent_image: ['5', 0],
229
+ },
230
+ },
231
+ '9': {
232
+ class_type: 'VAEDecode',
233
+ inputs: { samples: ['8', 0], vae: ['4', 2] },
234
+ },
235
+ '10': {
236
+ class_type: 'SaveImage',
237
+ inputs: { filename_prefix: 'dsh_faceid', images: ['9', 0] },
238
+ },
239
+ },
240
+ },
241
+ {
242
+ id: 'upscale_esrgan',
243
+ name: 'Real-ESRGAN Model Upscaler',
244
+ description: 'High-fidelity 4x super-resolution restoration via dedicated Real-ESRGAN neural upscaler.',
245
+ version: '1.0.0',
246
+ source: 'Comfy-Org/workflow_templates',
247
+ license: 'Apache-2.0',
248
+ category: 'upscale',
249
+ requiredNodes: [
250
+ 'LoadImage',
251
+ 'UpscaleModelLoader',
252
+ 'ImageUpscaleWithModel',
253
+ 'SaveImage',
254
+ ],
255
+ requiredModels: [
256
+ 'RealESRGAN_x4plus.pth',
257
+ ],
258
+ inputs: [
259
+ { name: 'image', type: 'string', required: true, description: 'Source low-resolution image' },
260
+ { name: 'upscale_model', type: 'string', default: 'RealESRGAN_x4plus.pth', description: 'Upscaler model filename' },
261
+ ],
262
+ graph: {
263
+ '1': {
264
+ class_type: 'LoadImage',
265
+ inputs: { image: '{{image}}' },
266
+ },
267
+ '2': {
268
+ class_type: 'UpscaleModelLoader',
269
+ inputs: { model_name: '{{upscale_model}}' },
270
+ },
271
+ '3': {
272
+ class_type: 'ImageUpscaleWithModel',
273
+ inputs: {
274
+ upscale_model: ['2', 0],
275
+ image: ['1', 0],
276
+ },
277
+ },
278
+ '4': {
279
+ class_type: 'SaveImage',
280
+ inputs: { filename_prefix: 'dsh_upscale', images: ['3', 0] },
281
+ },
282
+ },
283
+ },
284
+ ]
285
+
286
+ /**
287
+ * Returns summary list of all vetted ComfyUI templates.
288
+ */
289
+ export function getComfyTemplates() {
290
+ return COMFY_TEMPLATES.map((t) => ({
291
+ id: t.id,
292
+ name: t.name,
293
+ description: t.description,
294
+ version: t.version,
295
+ source: t.source,
296
+ license: t.license,
297
+ category: t.category,
298
+ requiredNodes: [...t.requiredNodes],
299
+ requiredModels: [...t.requiredModels],
300
+ inputs: t.inputs.map((inp) => ({ ...inp })),
301
+ }))
302
+ }
303
+
304
+ /**
305
+ * Returns full template definition by ID or null.
306
+ */
307
+ export function getComfyTemplate(id) {
308
+ if (!id) return null
309
+ return COMFY_TEMPLATES.find((t) => t.id === id) || null
310
+ }
311
+
312
+ /**
313
+ * Validates whether the ComfyUI server satisfies the dependencies required by a workflow template (#433).
314
+ * Explicitly distinguishes server reachability failures from missing node/model dependencies.
315
+ *
316
+ * @param {string} baseURL ComfyUI server address (e.g. http://127.0.0.1:8188)
317
+ * @param {object} template Template definition from getComfyTemplate
318
+ * @param {object} options
319
+ * @returns {Promise<{ ok: boolean, missingNodes: string[], missingModels: string[], error?: string }>}
320
+ */
321
+ export function createComfyDependencyValidator({ fetchImpl = fetch, timeoutMs = 8000 } = {}) {
322
+ return async function validateComfyServerDependencies(baseURL, template) {
323
+ const base = String(baseURL || '').replace(/\/+$/, '')
324
+ if (!base) {
325
+ throw new Error('ComfyUI server URL is not configured')
326
+ }
327
+ if (!template) {
328
+ throw new Error('Template is required for dependency validation')
329
+ }
330
+
331
+ let objectInfo
332
+ try {
333
+ const controller = typeof AbortController !== 'undefined' ? new AbortController() : null
334
+ const timer = controller ? setTimeout(() => controller.abort(new Error('timeout')), timeoutMs) : null
335
+ const res = await fetchImpl(`${base}/object_info`, {
336
+ signal: controller ? controller.signal : undefined,
337
+ })
338
+ if (timer) clearTimeout(timer)
339
+ if (!res.ok) {
340
+ throw new Error(`HTTP ${res.status} from /object_info`)
341
+ }
342
+ objectInfo = await res.json()
343
+ } catch (err) {
344
+ // Re-throw server reachability error without masking it as a missing model error
345
+ const msg = err?.name === 'AbortError' ? 'connection timed out' : (err?.message || String(err))
346
+ throw new Error(`ComfyUI server is unreachable at ${base} (${msg})`)
347
+ }
348
+
349
+ if (!objectInfo || typeof objectInfo !== 'object') {
350
+ throw new Error(`ComfyUI server at ${base} returned invalid object_info`)
351
+ }
352
+
353
+ const availableNodes = new Set(Object.keys(objectInfo))
354
+ const missingNodes = (template.requiredNodes || []).filter((node) => !availableNodes.has(node))
355
+
356
+ // Model validation: inspect choices provided in CheckpointLoaderSimple or UpscaleModelLoader
357
+ const missingModels = []
358
+ if (template.requiredModels && template.requiredModels.length > 0) {
359
+ const ckptInfo = objectInfo.CheckpointLoaderSimple
360
+ const availableCkpts = Array.isArray(ckptInfo?.input?.required?.ckpt_name?.[0])
361
+ ? new Set(ckptInfo.input.required.ckpt_name[0])
362
+ : null
363
+
364
+ const upscaleInfo = objectInfo.UpscaleModelLoader
365
+ const availableUpscalers = Array.isArray(upscaleInfo?.input?.required?.model_name?.[0])
366
+ ? new Set(upscaleInfo.input.required.model_name[0])
367
+ : null
368
+
369
+ for (const model of template.requiredModels) {
370
+ if (model.endsWith('.pth') || model.endsWith('.pt')) {
371
+ if (availableUpscalers && !availableUpscalers.has(model)) {
372
+ missingModels.push(model)
373
+ }
374
+ } else if (model.endsWith('.safetensors') || model.endsWith('.ckpt')) {
375
+ if (availableCkpts && !availableCkpts.has(model)) {
376
+ missingModels.push(model)
377
+ }
378
+ }
379
+ }
380
+ }
381
+
382
+ if (missingNodes.length > 0 || missingModels.length > 0) {
383
+ const errors = []
384
+ if (missingNodes.length > 0) errors.push(`missing custom nodes: ${missingNodes.join(', ')}`)
385
+ if (missingModels.length > 0) errors.push(`missing model files: ${missingModels.join(', ')}`)
386
+ return {
387
+ ok: false,
388
+ missingNodes,
389
+ missingModels,
390
+ error: `ComfyUI dependencies missing for template "${template.name}": ${errors.join('; ')}`,
391
+ }
392
+ }
393
+
394
+ return {
395
+ ok: true,
396
+ missingNodes: [],
397
+ missingModels: [],
398
+ }
399
+ }
400
+ }
401
+
402
+ /**
403
+ * Prepares an executable ComfyUI workflow graph from a vetted template and user parameters.
404
+ *
405
+ * @param {string} templateId
406
+ * @param {object} params
407
+ * @returns {object} validated, interpolated workflow graph
408
+ */
409
+ export function prepareComfyWorkflowFromTemplate(templateId, params = {}) {
410
+ const template = getComfyTemplate(templateId)
411
+ if (!template) {
412
+ throw new Error(`ComfyUI template "${templateId}" not found`)
413
+ }
414
+
415
+ // Validate required inputs
416
+ for (const input of template.inputs) {
417
+ if (input.required && (params[input.name] === undefined || params[input.name] === null || params[input.name] === '')) {
418
+ throw new Error(`Template "${template.name}" requires input "${input.name}"`)
419
+ }
420
+ }
421
+
422
+ // Apply default inputs if omitted
423
+ const merged = {}
424
+ for (const input of template.inputs) {
425
+ if (input.default !== undefined) {
426
+ merged[input.name] = input.default
427
+ }
428
+ }
429
+ Object.assign(merged, params)
430
+
431
+ const graph = interpolateComfyWorkflow(template.graph, merged)
432
+ return validateComfyWorkflow(graph)
433
+ }
@@ -215,6 +215,11 @@ export const Config = z.object({
215
215
  .description('CFG scale for local generation.')
216
216
  .default(7.0)
217
217
  .volatile(),
218
+ comfyTemplate: z
219
+ .string()
220
+ .description('Vetted ComfyUI workflow template identifier (e.g. "txt2img_sdxl", "img2img_sdxl", "face_instantid", "upscale_esrgan").')
221
+ .default('')
222
+ .volatile(),
218
223
  comfyWorkflowJson: z
219
224
  .string()
220
225
  .description('Custom ComfyUI API workflow JSON template with {{prompt}}, {{seed}}, etc. placeholders.')
package/lib/index.js CHANGED
@@ -47,6 +47,9 @@ import { registerVaultRoutes, readBoundedRequestBody } from './vault.js'
47
47
  import { registerLiveProgressRoutes } from './live-progress.js'
48
48
  import { registerSettingsRoutes } from './settings-route.js'
49
49
  import { clearAllAnchors } from './anchor-helpers.js'
50
+ import { clearSpendReservations } from './cost-meter.js'
51
+ import { getAllRecipes, getRecipe } from './recipes.js'
52
+ import { getComfyTemplates, getComfyTemplate, createComfyDependencyValidator } from './comfy-catalog.js'
50
53
  import { resetLoopGuard, getLoopGuardState } from './loop-guard.js'
51
54
  import { Config, plainConfig, volatileConfig, publicConfig, ensureConfig } from './config-schema.js'
52
55
  import { resolveSource } from './resolve-source.js'
@@ -433,6 +436,35 @@ export function apply(ctx, rawConfig) {
433
436
  // Vault route (#159)
434
437
  registerVaultRoutes(ctx)
435
438
 
439
+ // Recipes & Comfy templates routes (#432, #433)
440
+ ctx.effect(() => ctx.webServer.register({
441
+ kind: 'exact',
442
+ path: '/dsh-image-gen/recipes',
443
+ handler: async (req, res) => {
444
+ if (req.method !== 'GET') {
445
+ res.writeHead(405, { 'Content-Type': 'application/json' })
446
+ res.end(JSON.stringify({ error: 'GET only' }))
447
+ return
448
+ }
449
+ res.writeHead(200, { 'Content-Type': 'application/json' })
450
+ res.end(JSON.stringify({ ok: true, recipes: getAllRecipes() }))
451
+ },
452
+ }), 'dsh-image-gen: recipes route')
453
+
454
+ ctx.effect(() => ctx.webServer.register({
455
+ kind: 'exact',
456
+ path: '/dsh-image-gen/comfy/templates',
457
+ handler: async (req, res) => {
458
+ if (req.method !== 'GET') {
459
+ res.writeHead(405, { 'Content-Type': 'application/json' })
460
+ res.end(JSON.stringify({ error: 'GET only' }))
461
+ return
462
+ }
463
+ res.writeHead(200, { 'Content-Type': 'application/json' })
464
+ res.end(JSON.stringify({ ok: true, templates: getComfyTemplates() }))
465
+ },
466
+ }), 'dsh-image-gen: comfy templates route')
467
+
436
468
  // Live events route (#379)
437
469
  registerLiveProgressRoutes(ctx)
438
470
 
@@ -474,5 +506,6 @@ export function apply(ctx, rawConfig) {
474
506
 
475
507
  ctx.effect(() => () => {
476
508
  toolController?.dispose?.()
509
+ clearSpendReservations()
477
510
  }, 'dsh-image-gen: tool registration cleanup')
478
511
  }
@@ -255,6 +255,15 @@ export function registerLiveProgressRoutes(ctx) {
255
255
 
256
256
  res.write(': connected\n\n')
257
257
 
258
+ const current = getLiveProgress(callId)
259
+ if (current) {
260
+ try {
261
+ res.write(`data: ${JSON.stringify(current)}\n\n`)
262
+ } catch (_err) {
263
+ // Ignore stream write failure on initial snapshot replay (#429)
264
+ }
265
+ }
266
+
258
267
  const unsubscribe = subscribeLiveProgress(callId, (data) => {
259
268
  try {
260
269
  res.write(`data: ${JSON.stringify(data)}\n\n`)
@@ -23,13 +23,13 @@ export function createCustomGenerator(deps, job) {
23
23
  throw new Error('Custom provider (OpenAI-compatible) does not support facial identity references (FaceID) — use ComfyUI (IP-Adapter/InstantID) or FAL')
24
24
  }
25
25
  const base = String(cfg.customBaseURL || '').replace(/\/+$/, '')
26
- if (!base) throw new Error('Custom image provider: base URL is not configured (Settings в†’ Image generation)')
26
+ if (!base) throw new Error('Custom image provider: base URL is not configured (Settings -> Image generation)')
27
27
  if (!cfg.customModel) throw new Error('Custom image provider: model is not configured')
28
28
 
29
29
  // Empty key reference indicates unauthenticated gateway.
30
30
  const key = cfg.customKeyEnv ? await resolveKey(cfg.customKeyEnv) : ''
31
31
  if (job.faceReference && !source) {
32
- throw new Error('Custom provider (OpenAI-compatible) does not support facial identity references (FaceID) — use ComfyUI (IP-Adapter/InstantID) or FAL')
32
+ throw new Error('Custom provider (OpenAI-compatible) does not support facial identity references (FaceID) — use ComfyUI (IP-Adapter/InstantID) or FAL')
33
33
  }
34
34
  const isEdit = Boolean(source && source.bytes)
35
35
  const headers = {}
@@ -9,6 +9,12 @@ import {
9
9
  interpolateComfyWorkflow,
10
10
  buildDefaultComfyWorkflow,
11
11
  } from '../../comfy-workflow-helpers.js'
12
+ import {
13
+ getComfyTemplate,
14
+ getComfyTemplates,
15
+ createComfyDependencyValidator,
16
+ prepareComfyWorkflowFromTemplate,
17
+ } from '../../comfy-catalog.js'
12
18
 
13
19
  function resolveWebSocket() {
14
20
  if (typeof window !== 'undefined' && window.WebSocket) return window.WebSocket
@@ -108,7 +114,35 @@ export function createLocalGenerator(deps, job) {
108
114
  }
109
115
  const promptId = `dsh-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
110
116
  let workflowGraph
111
- if (cfg.comfyWorkflowJson) {
117
+ const activeTemplateId = job.comfyTemplate || cfg.comfyTemplate
118
+ if (activeTemplateId) {
119
+ const template = getComfyTemplate(activeTemplateId)
120
+ if (!template) {
121
+ throw new Error(`Unknown ComfyUI template "${activeTemplateId}". Available templates: ${getComfyTemplates().map((t) => t.id).join(', ')}`)
122
+ }
123
+ const validator = createComfyDependencyValidator({ fetchImpl, timeoutMs: 8000 })
124
+ const depCheck = await validator(base, template)
125
+ if (!depCheck.ok) {
126
+ throw new Error(depCheck.error)
127
+ }
128
+ const faceData = job.faceReference && job.faceReference.bytes
129
+ ? Buffer.from(job.faceReference.bytes).toString('base64')
130
+ : (job.faceReference?.url || '')
131
+ workflowGraph = prepareComfyWorkflowFromTemplate(activeTemplateId, {
132
+ prompt: promptArg,
133
+ negativePrompt: negativePrompt || '',
134
+ seed: seedArg ?? 0,
135
+ width,
136
+ height,
137
+ steps: cfg.localSteps ?? 20,
138
+ cfg: cfg.localCfg ?? 7,
139
+ model: cfg.localModel,
140
+ image: source && source.bytes ? Buffer.from(source.bytes).toString('base64') : '',
141
+ face_image: faceData,
142
+ face_weight: job.faceStrength ?? 0.75,
143
+ ...(job.templateParams || {}),
144
+ })
145
+ } else if (cfg.comfyWorkflowJson) {
112
146
  const template = validateComfyWorkflow(cfg.comfyWorkflowJson)
113
147
  const faceData = job.faceReference && job.faceReference.bytes
114
148
  ? Buffer.from(job.faceReference.bytes).toString('base64')