diff --git a/default/content/Default_Comfy_Workflow.json b/default/content/Default_Comfy_Workflow.json new file mode 100644 index 000000000..2f8082d83 --- /dev/null +++ b/default/content/Default_Comfy_Workflow.json @@ -0,0 +1,86 @@ +{ + "3": { + "class_type": "KSampler", + "inputs": { + "cfg": "%scale%", + "denoise": 1, + "latent_image": [ + "5", + 0 + ], + "model": [ + "4", + 0 + ], + "negative": [ + "7", + 0 + ], + "positive": [ + "6", + 0 + ], + "sampler_name": "%sampler%", + "scheduler": "%scheduler%", + "seed": "%seed%", + "steps": "%steps%" + } + }, + "4": { + "class_type": "CheckpointLoaderSimple", + "inputs": { + "ckpt_name": "%model%" + } + }, + "5": { + "class_type": "EmptyLatentImage", + "inputs": { + "batch_size": 1, + "height": "%height%", + "width": "%width%" + } + }, + "6": { + "class_type": "CLIPTextEncode", + "inputs": { + "clip": [ + "4", + 1 + ], + "text": "%prompt%" + } + }, + "7": { + "class_type": "CLIPTextEncode", + "inputs": { + "clip": [ + "4", + 1 + ], + "text": "%negative_prompt%" + } + }, + "8": { + "class_type": "VAEDecode", + "inputs": { + "samples": [ + "3", + 0 + ], + "vae": [ + "4", + 2 + ] + } + }, + "9": { + "class_type": "SaveImage", + "inputs": { + "filename_prefix": "SillyTavern", + "images": [ + "8", + 0 + ] + } + } +} \ No newline at end of file diff --git a/default/content/index.json b/default/content/index.json index a6df71180..8d8fbb14f 100644 --- a/default/content/index.json +++ b/default/content/index.json @@ -22,5 +22,9 @@ { "filename": "user-default.png", "type": "avatar" + }, + { + "filename": "Default_Comfy_Workflow.json", + "type": "workflow" } ] diff --git a/public/scripts/extensions/stable-diffusion/comfyWorkflowEditor.html b/public/scripts/extensions/stable-diffusion/comfyWorkflowEditor.html index e8b438f84..1839c67e8 100644 --- a/public/scripts/extensions/stable-diffusion/comfyWorkflowEditor.html +++ b/public/scripts/extensions/stable-diffusion/comfyWorkflowEditor.html @@ -1,6 +1,6 @@
-

ComfyUI Workflow Editor

+

ComfyUI Workflow Editor:

@@ -12,6 +12,7 @@
  • "%prompt%"
  • "%negative_prompt%"
  • "%model%"
  • +
  • "%vae%"
  • "%sampler%"
  • "%scheduler%"
  • "%steps%"
  • diff --git a/public/scripts/extensions/stable-diffusion/index.js b/public/scripts/extensions/stable-diffusion/index.js index c9e2fc29e..4e2b1ccc9 100644 --- a/public/scripts/extensions/stable-diffusion/index.js +++ b/public/scripts/extensions/stable-diffusion/index.js @@ -19,7 +19,7 @@ import { } from "../../../script.js"; import { getApiUrl, getContext, extension_settings, doExtrasFetch, modules, renderExtensionTemplate } from "../../extensions.js"; import { selected_group } from "../../group-chats.js"; -import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async } from "../../utils.js"; +import { stringFormat, initScrollHeight, resetScrollHeight, getCharaFilename, saveBase64AsFile, getBase64Async, delay } from "../../utils.js"; import { getMessageTimeStamp, humanizedDateTime } from "../../RossAscends-mods.js"; import { SECRET_KEYS, secret_state } from "../../secrets.js"; import { getNovelUnlimitedImageGeneration, getNovelAnlas, loadNovelSubscriptionData } from "../../nai-settings.js"; @@ -186,6 +186,7 @@ const defaultSettings = { negative_prompt: defaultNegative, sampler: 'DDIM', model: '', + vae: '', // Automatic1111/Horde exclusives restore_faces: false, @@ -242,94 +243,7 @@ const defaultSettings = { // ComyUI settings comfy_url: 'http://127.0.0.1:8188', - comfy_workflow: ` - { - "3": { - "class_type": "KSampler", - "inputs": { - "cfg": "%scale%", - "denoise": 1, - "latent_image": [ - "5", - 0 - ], - "model": [ - "4", - 0 - ], - "negative": [ - "7", - 0 - ], - "positive": [ - "6", - 0 - ], - "sampler_name": "%sampler%", - "scheduler": "%scheduler%", - "seed": 8566257, - "steps": "%steps%" - } - }, - "4": { - "class_type": "CheckpointLoaderSimple", - "inputs": { - "ckpt_name": "%model%" - } - }, - "5": { - "class_type": "EmptyLatentImage", - "inputs": { - "batch_size": 1, - "height": "%height%", - "width": "%width%" - } - }, - "6": { - "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "4", - 1 - ], - "text": "%prompt%" - } - }, - "7": { - "class_type": "CLIPTextEncode", - "inputs": { - "clip": [ - "4", - 1 - ], - "text": "%negative_prompt%" - } - }, - "8": { - "class_type": "VAEDecode", - "inputs": { - "samples": [ - "3", - 0 - ], - "vae": [ - "4", - 2 - ] - } - }, - "9": { - "class_type": "SaveImage", - "inputs": { - "filename_prefix": "SillyTavern", - "images": [ - "8", - 0 - ] - } - } - } - `, + comfy_workflow: 'Default_Comfy_Workflow.json', } function processTriggers(chat, _, abort) { @@ -480,7 +394,17 @@ async function loadSettings() { toggleSourceControls(); addPromptTemplates(); - await Promise.all([loadSamplers(), loadModels(), loadSchedulers()]); + await loadSettingOptions(); +} + +async function loadSettingOptions() { + return Promise.all([ + loadSamplers(), + loadModels(), + loadSchedulers(), + loadVaes(), + loadComfyWorkflows() + ]); } function addPromptTemplates() { @@ -846,9 +770,8 @@ function onComfyUrlInput() { saveSettingsDebounced(); } -function onComfyPromptInput() { - extension_settings.sd.comfy_prompt = $('#sd_comfy_prompt').val(); - resetScrollHeight($(this)); +function onComfyWorkflowChange() { + extension_settings.sd.comfy_workflow = $('#sd_comfy_workflow').find(':selected').val(); saveSettingsDebounced(); } @@ -920,6 +843,8 @@ async function validateComfyUrl() { await loadSamplers(); await loadSchedulers(); await loadModels(); + await loadVaes(); + await loadComfyWorkflows(); toastr.success('ComfyUI API connected.'); } catch (error) { toastr.error(`Could not validate ComfyUI API: ${error.message}`); @@ -966,6 +891,10 @@ async function getAutoRemoteModel() { } } +async function onVaeChange() { + extension_settings.sd.vae = $('#sd_vae').find(':selected').val(); +} + async function getAutoRemoteUpscalers() { try { const result = await fetch('/api/sd/upscalers', { @@ -1479,6 +1408,95 @@ async function loadComfySchedulers() { } } +async function loadVaes() { + $('#sd_vae').empty(); + let vaes = []; + + switch (extension_settings.sd.source) { + case sources.extras: + vaes = ['N/A']; + break; + case sources.horde: + vaes = ['N/A']; + break; + case sources.auto: + vaes = ['N/A']; + break; + case sources.novel: + vaes = ['N/A']; + break; + case sources.vlad: + vaes = ['N/A']; + break; + case sources.openai: + vaes = ['N/A']; + break; + case sources.comfy: + vaes = await loadComfyVaes(); + break; + } + + for (const vae of vaes) { + const option = document.createElement('option'); + option.innerText = vae; + option.value = vae; + option.selected = vae === extension_settings.sd.vae; + $('#sd_vae').append(option); + } +} + +async function loadComfyVaes() { + if (!extension_settings.sd.comfy_url) { + return []; + } + + try { + const result = await fetch(`/api/sd/comfy/vaes`, { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ + url: extension_settings.sd.comfy_url, + }) + }); + if (!result.ok) { + throw new Error('ComfyUI returned an error.'); + } + return await result.json(); + } catch (error) { + return []; + } +} + +async function loadComfyWorkflows() { + if (!extension_settings.sd.comfy_url) { + return; + } + + try { + $('#sd_comfy_workflow').empty(); + const result = await fetch(`/api/sd/comfy/workflows`, { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ + url: extension_settings.sd.comfy_url, + }) + }); + if (!result.ok) { + throw new Error('ComfyUI returned an error.'); + } + const workflows = await result.json(); + for (const workflow of workflows) { + const option = document.createElement('option'); + option.innerText = workflow; + option.value = workflow; + option.selected = workflow === extension_settings.sd.comfy_workflow; + $('#sd_comfy_workflow').append(option); + } + } catch (error) { + return; + } +} + function getGenerationType(prompt) { let mode = generationMode.FREE; @@ -2043,6 +2061,7 @@ async function generateComfyImage(prompt) { const placeholders = [ 'negative_prompt', 'model', + 'vae', 'sampler', 'scheduler', 'steps', @@ -2051,7 +2070,18 @@ async function generateComfyImage(prompt) { 'height', ]; - let workflow = extension_settings.sd.comfy_workflow.replace('"%prompt%"', JSON.stringify(prompt)); + const workflowResponse = await fetch('/api/sd/comfy/workflow', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ + file_name: extension_settings.sd.comfy_workflow, + }), + }); + if (!workflowResponse.ok) { + const text = await workflowResponse.text(); + toastr.error(`Failed to load workflow.\n\n${text}`); + } + let workflow = (await workflowResponse.json()).replace('"%prompt%"', JSON.stringify(prompt)); workflow = workflow.replace('"%seed%"', JSON.stringify(Math.round(Math.random() * Number.MAX_SAFE_INTEGER))); placeholders.forEach(ph => { workflow = workflow.replace(`"%${ph}%"`, JSON.stringify(extension_settings.sd[ph])); @@ -2073,25 +2103,90 @@ async function generateComfyImage(prompt) { } async function onComfyOpenWorkflowEditorClick() { + let workflow = await (await fetch(`/api/sd/comfy/workflow`, { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ + file_name: extension_settings.sd.comfy_workflow, + }), + })).json(); const editorHtml = $(await $.get('scripts/extensions/stable-diffusion/comfyWorkflowEditor.html')); const popupResult = callPopup(editorHtml, "confirm", undefined, { okButton: "Save", wide: true, large: true, rows: 1 }); const checkPlaceholders = () => { - const workflow = $('#sd_comfy_workflow_editor_workflow').val().toString(); + workflow = $('#sd_comfy_workflow_editor_workflow').val().toString(); $('.sd_comfy_workflow_editor_placeholder_list > li[data-placeholder]').each(function (idx) { const key = this.getAttribute('data-placeholder'); const found = workflow.search(`"%${key}%"`) != -1; this.classList[found ? 'remove' : 'add']('sd_comfy_workflow_editor_not_found'); }); }; - $('#sd_comfy_workflow_editor_workflow').val(extension_settings.sd.comfy_workflow); + $('#sd_comfy_workflow_editor_name').text(extension_settings.sd.comfy_workflow); + $('#sd_comfy_workflow_editor_workflow').val(workflow); checkPlaceholders(); $('#sd_comfy_workflow_editor_workflow').on('input', checkPlaceholders); if (await popupResult) { - extension_settings.sd.comfy_workflow = $('#sd_comfy_workflow_editor_workflow').val().toString(); - saveSettingsDebounced(); + const response = await fetch(`/api/sd/comfy/save-workflow`, { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ + file_name: extension_settings.sd.comfy_workflow, + workflow: $('#sd_comfy_workflow_editor_workflow').val().toString(), + }), + }); + if (!response.ok) { + const text = await response.text(); + toastr.error(`Failed to save workflow.\n\n${text}`); + } } } +async function onComfyNewWorkflowClick() { + let name = await callPopup('

    Workflow name:

    ', 'input'); + if (!name) { + return; + } + if (!name.toLowerCase().endsWith('.json')) { + name += '.json'; + } + extension_settings.sd.comfy_workflow = name; + const response = await fetch(`/api/sd/comfy/save-workflow`, { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ + file_name: extension_settings.sd.comfy_workflow, + workflow: '', + }), + }); + if (!response.ok) { + const text = await response.text(); + toastr.error(`Failed to save workflow.\n\n${text}`); + } + saveSettingsDebounced(); + await loadComfyWorkflows(); + await delay(200); + await onComfyOpenWorkflowEditorClick(); +} + +async function onComfyDeleteWorkflowClick() { + const confirm = await callPopup('Delete the workflow? This action is irreversible.', 'confirm'); + if (!confirm) { + return; + } + const response = await fetch('/api/sd/comfy/delete-workflow', { + method: 'POST', + headers: getRequestHeaders(), + body: JSON.stringify({ + file_name: extension_settings.sd.comfy_workflow, + }), + }); + if (!response.ok) { + const text = await response.text(); + toastr.error(`Failed to save workflow.\n\n${text}`); + } + await loadComfyWorkflows(); + onComfyWorkflowChange(); +} + async function sendMessage(prompt, image, generationType) { const context = getContext(); const messageText = `[${context.name2} sends a picture that contains: ${prompt}]`; @@ -2300,6 +2395,7 @@ jQuery(async () => { $('#sd_scale').on('input', onScaleInput); $('#sd_steps').on('input', onStepsInput); $('#sd_model').on('change', onModelChange); + $('#sd_vae').on('change', onVaeChange); $('#sd_sampler').on('change', onSamplerChange); $('#sd_scheduler').on('change', onSchedulerChange); $('#sd_prompt_prefix').on('input', onPromptPrefixInput); @@ -2328,7 +2424,10 @@ jQuery(async () => { $('#sd_novel_view_anlas').on('click', onViewAnlasClick); $('#sd_comfy_validate').on('click', validateComfyUrl); $('#sd_comfy_url').on('input', onComfyUrlInput); + $('#sd_comfy_workflow').on('change', onComfyWorkflowChange); $('#sd_comfy_open_workflow_editor').on('click', onComfyOpenWorkflowEditorClick); + $('#sd_comfy_new_workflow').on('click', onComfyNewWorkflowClick); + $('#sd_comfy_delete_workflow').on('click', onComfyDeleteWorkflowClick); $('#sd_expand').on('input', onExpandInput); $('#sd_style').on('change', onStyleSelect); $('#sd_save_style').on('click', onSaveStyleClick); diff --git a/public/scripts/extensions/stable-diffusion/settings.html b/public/scripts/extensions/stable-diffusion/settings.html index cb0a20cd4..d3d7e6e20 100644 --- a/public/scripts/extensions/stable-diffusion/settings.html +++ b/public/scripts/extensions/stable-diffusion/settings.html @@ -128,11 +128,20 @@
    -

    Important: The server must be accessible from the SillyTavern host machine.

    + +
    + + + + +
    @@ -150,6 +159,10 @@
    +
    + + +