Merge pull request #3088 from ceruleandeep/feature/comfyGgufModels Feature/comfy gguf models

dde9f93d77e58f06d8907c8dd5006e8495a5303b

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
6 files changed, +184 -20Ignore whitespace
default/content/Char_Avatar_Comfy_Workflow.json+137 -0
@@ -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}
default/content/index.json+4 -0
@@ -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 },
public/scripts/extensions/stable-diffusion/comfyWorkflowEditor.html+1 -0
@@ -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>
public/scripts/extensions/stable-diffusion/index.js+8 -2
@@ -3269,6 +3269,10 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
32693269
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));
public/scripts/extensions/stable-diffusion/settings.html+1 -1
@@ -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>
321321
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>
src/endpoints/stable-diffusion.js+33 -17
@@ -7,7 +7,7 @@ import sanitize from 'sanitize-filename';
7import { sync as writeFileAtomicSync } from 'write-file-atomic';7import { sync as writeFileAtomicSync } from 'write-file-atomic';
8import FormData from 'form-data';8import FormData from 'form-data';
99
10import { getBasicAuthHeader, delay } from '../util.js';10import { delay, getBasicAuthHeader, tryParse } from '../util.js';
11import { jsonParser } from '../express-common.js';11import { jsonParser } from '../express-common.js';
12import { readSecret, SECRET_KEYS } from './secrets.js';12import { readSecret, SECRET_KEYS } from './secrets.js';
1313
@@ -19,7 +19,7 @@ import { readSecret, SECRET_KEYS } from './secrets.js';
19function getComfyWorkflows(directories) {19function getComfyWorkflows(directories) {
20 return fs20 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}
2525
@@ -67,8 +67,7 @@ router.post('/upscalers', jsonParser, async (request, response) => {
6767
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 }
7372
74 async function getLatentUpscalers() {73 async function getLatentUpscalers() {
@@ -88,8 +87,7 @@ router.post('/upscalers', jsonParser, async (request, response) => {
8887
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 }
9492
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 }
247244
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) => {
274271
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 }
280277
@@ -412,8 +409,19 @@ 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
416 } catch (error) {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);
424 } catch (error) {
417 console.log(error);425 console.log(error);
418 return response.sendStatus(500);426 return response.sendStatus(500);
419 }427 }
@@ -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 }
532541
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});
570586