Merge pull request #3088 from ceruleandeep/feature/comfyGgufModels Feature/comfy gguf models
Signed| @@ -0,0 +1,137 @@ | |||
| 1 | { | ||
| 2 | "3": { | ||
| 3 | "inputs": { | ||
| 4 | "seed": "%seed%", | ||
| 5 | "steps": "%steps%", | ||
| 6 | "cfg": "%scale%", | ||
| 7 | "sampler_name": "%sampler%", | ||
| 8 | "scheduler": "%scheduler%", | ||
| 9 | "denoise": "%denoise%", | ||
| 10 | "model": [ | ||
| 11 | "4", | ||
| 12 | 0 | ||
| 13 | ], | ||
| 14 | "positive": [ | ||
| 15 | "6", | ||
| 16 | 0 | ||
| 17 | ], | ||
| 18 | "negative": [ | ||
| 19 | "7", | ||
| 20 | 0 | ||
| 21 | ], | ||
| 22 | "latent_image": [ | ||
| 23 | "12", | ||
| 24 | 0 | ||
| 25 | ] | ||
| 26 | }, | ||
| 27 | "class_type": "KSampler", | ||
| 28 | "_meta": { | ||
| 29 | "title": "KSampler" | ||
| 30 | } | ||
| 31 | }, | ||
| 32 | "4": { | ||
| 33 | "inputs": { | ||
| 34 | "ckpt_name": "%model%" | ||
| 35 | }, | ||
| 36 | "class_type": "CheckpointLoaderSimple", | ||
| 37 | "_meta": { | ||
| 38 | "title": "Load Checkpoint" | ||
| 39 | } | ||
| 40 | }, | ||
| 41 | "6": { | ||
| 42 | "inputs": { | ||
| 43 | "text": "%prompt%", | ||
| 44 | "clip": [ | ||
| 45 | "4", | ||
| 46 | 1 | ||
| 47 | ] | ||
| 48 | }, | ||
| 49 | "class_type": "CLIPTextEncode", | ||
| 50 | "_meta": { | ||
| 51 | "title": "CLIP Text Encode (Prompt)" | ||
| 52 | } | ||
| 53 | }, | ||
| 54 | "7": { | ||
| 55 | "inputs": { | ||
| 56 | "text": "%negative_prompt%", | ||
| 57 | "clip": [ | ||
| 58 | "4", | ||
| 59 | 1 | ||
| 60 | ] | ||
| 61 | }, | ||
| 62 | "class_type": "CLIPTextEncode", | ||
| 63 | "_meta": { | ||
| 64 | "title": "CLIP Text Encode (Negative Prompt)" | ||
| 65 | } | ||
| 66 | }, | ||
| 67 | "8": { | ||
| 68 | "inputs": { | ||
| 69 | "samples": [ | ||
| 70 | "3", | ||
| 71 | 0 | ||
| 72 | ], | ||
| 73 | "vae": [ | ||
| 74 | "4", | ||
| 75 | 2 | ||
| 76 | ] | ||
| 77 | }, | ||
| 78 | "class_type": "VAEDecode", | ||
| 79 | "_meta": { | ||
| 80 | "title": "VAE Decode" | ||
| 81 | } | ||
| 82 | }, | ||
| 83 | "9": { | ||
| 84 | "inputs": { | ||
| 85 | "filename_prefix": "SillyTavern", | ||
| 86 | "images": [ | ||
| 87 | "8", | ||
| 88 | 0 | ||
| 89 | ] | ||
| 90 | }, | ||
| 91 | "class_type": "SaveImage", | ||
| 92 | "_meta": { | ||
| 93 | "title": "Save Image" | ||
| 94 | } | ||
| 95 | }, | ||
| 96 | "10": { | ||
| 97 | "inputs": { | ||
| 98 | "image": "%char_avatar%" | ||
| 99 | }, | ||
| 100 | "class_type": "ETN_LoadImageBase64", | ||
| 101 | "_meta": { | ||
| 102 | "title": "Load Image (Base64) [https://github.com/Acly/comfyui-tooling-nodes]" | ||
| 103 | } | ||
| 104 | }, | ||
| 105 | "12": { | ||
| 106 | "inputs": { | ||
| 107 | "pixels": [ | ||
| 108 | "13", | ||
| 109 | 0 | ||
| 110 | ], | ||
| 111 | "vae": [ | ||
| 112 | "4", | ||
| 113 | 2 | ||
| 114 | ] | ||
| 115 | }, | ||
| 116 | "class_type": "VAEEncode", | ||
| 117 | "_meta": { | ||
| 118 | "title": "VAE Encode" | ||
| 119 | } | ||
| 120 | }, | ||
| 121 | "13": { | ||
| 122 | "inputs": { | ||
| 123 | "upscale_method": "bicubic", | ||
| 124 | "width": "%width%", | ||
| 125 | "height": "%height%", | ||
| 126 | "crop": "center", | ||
| 127 | "image": [ | ||
| 128 | "10", | ||
| 129 | 0 | ||
| 130 | ] | ||
| 131 | }, | ||
| 132 | "class_type": "ImageScale", | ||
| 133 | "_meta": { | ||
| 134 | "title": "Upscale Image" | ||
| 135 | } | ||
| 136 | } | ||
| 137 | } | ||
| @@ -136,6 +136,10 @@ | |||
| 136 | "type": "workflow" | 136 | "type": "workflow" |
| 137 | }, | 137 | }, |
| 138 | { | 138 | { |
| 139 | "filename": "Char_Avatar_Comfy_Workflow.json", | ||
| 140 | "type": "workflow" | ||
| 141 | }, | ||
| 142 | { | ||
| 139 | "filename": "presets/kobold/Ace of Spades.json", | 143 | "filename": "presets/kobold/Ace of Spades.json", |
| 140 | "type": "kobold_preset" | 144 | "type": "kobold_preset" |
| 141 | }, | 145 | }, |
| @@ -17,6 +17,7 @@ | |||
| 17 | <li data-placeholder="scheduler" class="sd_comfy_workflow_editor_not_found">"%scheduler%"</li> | 17 | <li data-placeholder="scheduler" class="sd_comfy_workflow_editor_not_found">"%scheduler%"</li> |
| 18 | <li data-placeholder="steps" class="sd_comfy_workflow_editor_not_found">"%steps%"</li> | 18 | <li data-placeholder="steps" class="sd_comfy_workflow_editor_not_found">"%steps%"</li> |
| 19 | <li data-placeholder="scale" class="sd_comfy_workflow_editor_not_found">"%scale%"</li> | 19 | <li data-placeholder="scale" class="sd_comfy_workflow_editor_not_found">"%scale%"</li> |
| 20 | <li data-placeholder="denoise" class="sd_comfy_workflow_editor_not_found">"%denoise%"</li> | ||
| 20 | <li data-placeholder="clip_skip" class="sd_comfy_workflow_editor_not_found">"%clip_skip%"</li> | 21 | <li data-placeholder="clip_skip" class="sd_comfy_workflow_editor_not_found">"%clip_skip%"</li> |
| 21 | <li data-placeholder="width" class="sd_comfy_workflow_editor_not_found">"%width%"</li> | 22 | <li data-placeholder="width" class="sd_comfy_workflow_editor_not_found">"%width%"</li> |
| 22 | <li data-placeholder="height" class="sd_comfy_workflow_editor_not_found">"%height%"</li> | 23 | <li data-placeholder="height" class="sd_comfy_workflow_editor_not_found">"%height%"</li> |
| @@ -3269,6 +3269,10 @@ async function generateComfyImage(prompt, negativePrompt, signal) { | |||
| 3269 | 3269 | ||
| 3270 | const seed = extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : Math.round(Math.random() * Number.MAX_SAFE_INTEGER); | 3270 | const seed = extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : Math.round(Math.random() * Number.MAX_SAFE_INTEGER); |
| 3271 | workflow = workflow.replaceAll('"%seed%"', JSON.stringify(seed)); | 3271 | workflow = workflow.replaceAll('"%seed%"', JSON.stringify(seed)); |
| 3272 | |||
| 3273 | const denoising_strength = extension_settings.sd.denoising_strength === undefined ? 1.0 : extension_settings.sd.denoising_strength; | ||
| 3274 | workflow = workflow.replaceAll('"%denoise%"', JSON.stringify(denoising_strength)); | ||
| 3275 | |||
| 3272 | placeholders.forEach(ph => { | 3276 | placeholders.forEach(ph => { |
| 3273 | workflow = workflow.replaceAll(`"%${ph}%"`, JSON.stringify(extension_settings.sd[ph])); | 3277 | workflow = workflow.replaceAll(`"%${ph}%"`, JSON.stringify(extension_settings.sd[ph])); |
| 3274 | }); | 3278 | }); |
| @@ -3279,7 +3283,8 @@ async function generateComfyImage(prompt, negativePrompt, signal) { | |||
| 3279 | const response = await fetch(getUserAvatarUrl()); | 3283 | const response = await fetch(getUserAvatarUrl()); |
| 3280 | if (response.ok) { | 3284 | if (response.ok) { |
| 3281 | const avatarBlob = await response.blob(); | 3285 | const avatarBlob = await response.blob(); |
| 3282 | const avatarBase64 = await getBase64Async(avatarBlob); | 3286 | const avatarBase64DataUrl = await getBase64Async(avatarBlob); |
| 3287 | const avatarBase64 = avatarBase64DataUrl.split(',')[1]; | ||
| 3283 | workflow = workflow.replaceAll('"%user_avatar%"', JSON.stringify(avatarBase64)); | 3288 | workflow = workflow.replaceAll('"%user_avatar%"', JSON.stringify(avatarBase64)); |
| 3284 | } else { | 3289 | } else { |
| 3285 | workflow = workflow.replaceAll('"%user_avatar%"', JSON.stringify(PNG_PIXEL)); | 3290 | workflow = workflow.replaceAll('"%user_avatar%"', JSON.stringify(PNG_PIXEL)); |
| @@ -3289,7 +3294,8 @@ async function generateComfyImage(prompt, negativePrompt, signal) { | |||
| 3289 | const response = await fetch(getCharacterAvatarUrl()); | 3294 | const response = await fetch(getCharacterAvatarUrl()); |
| 3290 | if (response.ok) { | 3295 | if (response.ok) { |
| 3291 | const avatarBlob = await response.blob(); | 3296 | const avatarBlob = await response.blob(); |
| 3292 | const avatarBase64 = await getBase64Async(avatarBlob); | 3297 | const avatarBase64DataUrl = await getBase64Async(avatarBlob); |
| 3298 | const avatarBase64 = avatarBase64DataUrl.split(',')[1]; | ||
| 3293 | workflow = workflow.replaceAll('"%char_avatar%"', JSON.stringify(avatarBase64)); | 3299 | workflow = workflow.replaceAll('"%char_avatar%"', JSON.stringify(avatarBase64)); |
| 3294 | } else { | 3300 | } else { |
| 3295 | workflow = workflow.replaceAll('"%char_avatar%"', JSON.stringify(PNG_PIXEL)); | 3301 | workflow = workflow.replaceAll('"%char_avatar%"', JSON.stringify(PNG_PIXEL)); |
| @@ -319,7 +319,7 @@ | |||
| 319 | <input class="neo-range-input" type="number" id="sd_hr_scale_value" data-for="sd_hr_scale" min="{{hr_scale_min}}" max="{{hr_scale_max}}" step="{{hr_scale_step}}" value="{{hr_scale}}" > | 319 | <input class="neo-range-input" type="number" id="sd_hr_scale_value" data-for="sd_hr_scale" min="{{hr_scale_min}}" max="{{hr_scale_max}}" step="{{hr_scale_step}}" value="{{hr_scale}}" > |
| 320 | </div> | 320 | </div> |
| 321 | 321 | ||
| 322 | <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad"> | 322 | <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad,comfy"> |
| 323 | <small> | 323 | <small> |
| 324 | <span data-i18n="Denoising strength">Denoising strength</span> | 324 | <span data-i18n="Denoising strength">Denoising strength</span> |
| 325 | </small> | 325 | </small> |
| @@ -7,7 +7,7 @@ import sanitize from 'sanitize-filename'; | |||
| 7 | import { sync as writeFileAtomicSync } from 'write-file-atomic'; | 7 | import { sync as writeFileAtomicSync } from 'write-file-atomic'; |
| 8 | import FormData from 'form-data'; | 8 | import FormData from 'form-data'; |
| 9 | 9 | ||
| 10 | import { getBasicAuthHeader, delay } from '../util.js'; | 10 | import { delay, getBasicAuthHeader, tryParse } from '../util.js'; |
| 11 | import { jsonParser } from '../express-common.js'; | 11 | import { jsonParser } from '../express-common.js'; |
| 12 | import { readSecret, SECRET_KEYS } from './secrets.js'; | 12 | import { readSecret, SECRET_KEYS } from './secrets.js'; |
| 13 | 13 | ||
| @@ -19,7 +19,7 @@ import { readSecret, SECRET_KEYS } from './secrets.js'; | |||
| 19 | function getComfyWorkflows(directories) { | 19 | function getComfyWorkflows(directories) { |
| 20 | return fs | 20 | return fs |
| 21 | .readdirSync(directories.comfyWorkflows) | 21 | .readdirSync(directories.comfyWorkflows) |
| 22 | .filter(file => file[0] != '.' && file.toLowerCase().endsWith('.json')) | 22 | .filter(file => file[0] !== '.' && file.toLowerCase().endsWith('.json')) |
| 23 | .sort(Intl.Collator().compare); | 23 | .sort(Intl.Collator().compare); |
| 24 | } | 24 | } |
| 25 | 25 | ||
| @@ -67,8 +67,7 @@ router.post('/upscalers', jsonParser, async (request, response) => { | |||
| 67 | 67 | ||
| 68 | /** @type {any} */ | 68 | /** @type {any} */ |
| 69 | const data = await result.json(); | 69 | const data = await result.json(); |
| 70 | const names = data.map(x => x.name); | 70 | return data.map(x => x.name); |
| 71 | return names; | ||
| 72 | } | 71 | } |
| 73 | 72 | ||
| 74 | async function getLatentUpscalers() { | 73 | async function getLatentUpscalers() { |
| @@ -88,8 +87,7 @@ router.post('/upscalers', jsonParser, async (request, response) => { | |||
| 88 | 87 | ||
| 89 | /** @type {any} */ | 88 | /** @type {any} */ |
| 90 | const data = await result.json(); | 89 | const data = await result.json(); |
| 91 | const names = data.map(x => x.name); | 90 | return data.map(x => x.name); |
| 92 | return names; | ||
| 93 | } | 91 | } |
| 94 | 92 | ||
| 95 | const [upscalers, latentUpscalers] = await Promise.all([getUpscalerModels(), getLatentUpscalers()]); | 93 | const [upscalers, latentUpscalers] = await Promise.all([getUpscalerModels(), getLatentUpscalers()]); |
| @@ -241,8 +239,7 @@ router.post('/set-model', jsonParser, async (request, response) => { | |||
| 241 | 'Authorization': getBasicAuthHeader(request.body.auth), | 239 | 'Authorization': getBasicAuthHeader(request.body.auth), |
| 242 | }, | 240 | }, |
| 243 | }); | 241 | }); |
| 244 | const data = await result.json(); | 242 | return await result.json(); |
| 245 | return data; | ||
| 246 | } | 243 | } |
| 247 | 244 | ||
| 248 | const url = new URL(request.body.url); | 245 | const url = new URL(request.body.url); |
| @@ -274,7 +271,7 @@ router.post('/set-model', jsonParser, async (request, response) => { | |||
| 274 | 271 | ||
| 275 | const progress = progressState['progress']; | 272 | const progress = progressState['progress']; |
| 276 | const jobCount = progressState['state']['job_count']; | 273 | const jobCount = progressState['state']['job_count']; |
| 277 | if (progress == 0.0 && jobCount === 0) { | 274 | if (progress === 0.0 && jobCount === 0) { |
| 278 | break; | 275 | break; |
| 279 | } | 276 | } |
| 280 | 277 | ||
| @@ -412,7 +409,18 @@ comfy.post('/models', jsonParser, async (request, response) => { | |||
| 412 | } | 409 | } |
| 413 | /** @type {any} */ | 410 | /** @type {any} */ |
| 414 | const data = await result.json(); | 411 | const data = await result.json(); |
| 415 | return response.send(data.CheckpointLoaderSimple.input.required.ckpt_name[0].map(it => ({ value: it, text: it }))); | 412 | |
| 413 | const ckpts = data.CheckpointLoaderSimple.input.required.ckpt_name[0].map(it => ({ value: it, text: it })) || []; | ||
| 414 | const unets = data.UNETLoader.input.required.unet_name[0].map(it => ({ value: it, text: `UNet: ${it}` })) || []; | ||
| 415 | |||
| 416 | // load list of GGUF unets from diffusion_models if the loader node is available | ||
| 417 | const ggufs = data.UnetLoaderGGUF?.input.required.unet_name[0].map(it => ({ value: it, text: `GGUF: ${it}` })) || []; | ||
| 418 | const models = [...ckpts, ...unets, ...ggufs]; | ||
| 419 | |||
| 420 | // make the display names of the models somewhat presentable | ||
| 421 | models.forEach(it => it.text = it.text.replace(/\.[^.]*$/, '').replace(/_/g, ' ')); | ||
| 422 | |||
| 423 | return response.send(models); | ||
| 416 | } catch (error) { | 424 | } catch (error) { |
| 417 | console.log(error); | 425 | console.log(error); |
| 418 | return response.sendStatus(500); | 426 | return response.sendStatus(500); |
| @@ -527,7 +535,8 @@ comfy.post('/generate', jsonParser, async (request, response) => { | |||
| 527 | body: request.body.prompt, | 535 | body: request.body.prompt, |
| 528 | }); | 536 | }); |
| 529 | if (!promptResult.ok) { | 537 | if (!promptResult.ok) { |
| 530 | throw new Error('ComfyUI returned an error.'); | 538 | const text = await promptResult.text(); |
| 539 | throw new Error('ComfyUI returned an error.', { cause: tryParse(text) }); | ||
| 531 | } | 540 | } |
| 532 | 541 | ||
| 533 | /** @type {any} */ | 542 | /** @type {any} */ |
| @@ -550,7 +559,13 @@ comfy.post('/generate', jsonParser, async (request, response) => { | |||
| 550 | await delay(100); | 559 | await delay(100); |
| 551 | } | 560 | } |
| 552 | if (item.status.status_str === 'error') { | 561 | if (item.status.status_str === 'error') { |
| 553 | throw new Error('ComfyUI generation did not succeed.'); | 562 | // Report node tracebacks if available |
| 563 | const errorMessages = item.status?.messages | ||
| 564 | ?.filter(it => it[0] === 'execution_error') | ||
| 565 | .map(it => it[1]) | ||
| 566 | .map(it => `${it.node_type} [${it.node_id}] ${it.exception_type}: ${it.exception_message}`) | ||
| 567 | .join('\n') || ''; | ||
| 568 | throw new Error(`ComfyUI generation did not succeed.\n\n${errorMessages}`.trim()); | ||
| 554 | } | 569 | } |
| 555 | const imgInfo = Object.keys(item.outputs).map(it => item.outputs[it].images).flat()[0]; | 570 | const imgInfo = Object.keys(item.outputs).map(it => item.outputs[it].images).flat()[0]; |
| 556 | const imgUrl = new URL(request.body.url); | 571 | const imgUrl = new URL(request.body.url); |
| @@ -560,11 +575,12 @@ comfy.post('/generate', jsonParser, async (request, response) => { | |||
| 560 | if (!imgResponse.ok) { | 575 | if (!imgResponse.ok) { |
| 561 | throw new Error('ComfyUI returned an error.'); | 576 | throw new Error('ComfyUI returned an error.'); |
| 562 | } | 577 | } |
| 563 | const imgBuffer = await imgResponse.buffer(); | 578 | const imgBuffer = await imgResponse.arrayBuffer(); |
| 564 | return response.send(imgBuffer.toString('base64')); | 579 | return response.send(Buffer.from(imgBuffer).toString('base64')); |
| 565 | } catch (error) { | 580 | } catch (error) { |
| 566 | console.log(error); | 581 | console.log('ComfyUI error:', error); |
| 567 | return response.sendStatus(500); | 582 | response.status(500).send(error.message); |
| 583 | return response; | ||
| 568 | } | 584 | } |
| 569 | }); | 585 | }); |
| 570 | 586 | ||