mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	Add Stable Horde image gen
This commit is contained in:
		| @@ -3,9 +3,11 @@ import { | |||||||
|     saveSettingsDebounced, |     saveSettingsDebounced, | ||||||
|     systemUserName, |     systemUserName, | ||||||
|     hideSwipeButtons, |     hideSwipeButtons, | ||||||
|     showSwipeButtons |     showSwipeButtons, | ||||||
|  |     callPopup, | ||||||
|  |     getRequestHeaders | ||||||
| } from "../../../script.js"; | } from "../../../script.js"; | ||||||
| import { getApiUrl, getContext, extension_settings, defaultRequestArgs } from "../../extensions.js"; | import { getApiUrl, getContext, extension_settings, defaultRequestArgs, modules } from "../../extensions.js"; | ||||||
| import { stringFormat, initScrollHeight, resetScrollHeight } from "../../utils.js"; | import { stringFormat, initScrollHeight, resetScrollHeight } from "../../utils.js"; | ||||||
| export { MODULE_NAME }; | export { MODULE_NAME }; | ||||||
|  |  | ||||||
| @@ -94,6 +96,9 @@ const defaultSettings = { | |||||||
|     negative_prompt: 'lowres, bad anatomy, bad hands, text, error, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry', |     negative_prompt: 'lowres, bad anatomy, bad hands, text, error, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry', | ||||||
|     sampler: 'DDIM', |     sampler: 'DDIM', | ||||||
|     model: '', |     model: '', | ||||||
|  |  | ||||||
|  |     horde: false, | ||||||
|  |     horde_nsfw: false, | ||||||
| } | } | ||||||
|  |  | ||||||
| async function loadSettings() { | async function loadSettings() { | ||||||
| @@ -107,11 +112,10 @@ async function loadSettings() { | |||||||
|     $('#sd_negative_prompt').val(extension_settings.sd.negative_prompt).trigger('input'); |     $('#sd_negative_prompt').val(extension_settings.sd.negative_prompt).trigger('input'); | ||||||
|     $('#sd_width').val(extension_settings.sd.width).trigger('input'); |     $('#sd_width').val(extension_settings.sd.width).trigger('input'); | ||||||
|     $('#sd_height').val(extension_settings.sd.height).trigger('input'); |     $('#sd_height').val(extension_settings.sd.height).trigger('input'); | ||||||
|  |     $('#sd_horde').prop('checked', extension_settings.sd.horde); | ||||||
|  |     $('#sd_horde_nsfw').prop('checked', extension_settings.sd.horde_nsfw); | ||||||
|  |  | ||||||
|     await Promise.all([loadSamplers(), loadModels()]); |     await Promise.all([loadSamplers(), loadModels()]); | ||||||
|  |  | ||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| function onScaleInput() { | function onScaleInput() { | ||||||
| @@ -155,10 +159,29 @@ function onHeightInput() { | |||||||
|     saveSettingsDebounced(); |     saveSettingsDebounced(); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | async function onHordeInput() { | ||||||
|  |     extension_settings.sd.model = null; | ||||||
|  |     extension_settings.sd.sampler = null; | ||||||
|  |     extension_settings.sd.horde = !!$(this).prop('checked'); | ||||||
|  |     saveSettingsDebounced(); | ||||||
|  |     await Promise.all([loadModels(), loadSamplers()]); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function onHordeNsfwInput() { | ||||||
|  |     extension_settings.sd.horde_nsfw = !!$(this).prop('checked'); | ||||||
|  |     saveSettingsDebounced(); | ||||||
|  | } | ||||||
|  |  | ||||||
| async function onModelChange() { | async function onModelChange() { | ||||||
|     extension_settings.sd.model = $('#sd_model').find(':selected').val(); |     extension_settings.sd.model = $('#sd_model').find(':selected').val(); | ||||||
|     saveSettingsDebounced(); |     saveSettingsDebounced(); | ||||||
|  |  | ||||||
|  |     if (extension_settings.sd.horde == false) { | ||||||
|  |         await updateExtrasRemoteModel(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function updateExtrasRemoteModel() { | ||||||
|     const url = new URL(getApiUrl()); |     const url = new URL(getApiUrl()); | ||||||
|     url.pathname = '/api/image/model'; |     url.pathname = '/api/image/model'; | ||||||
|     const getCurrentModelResult = await fetch(url, { |     const getCurrentModelResult = await fetch(url, { | ||||||
| @@ -173,13 +196,14 @@ async function onModelChange() { | |||||||
| } | } | ||||||
|  |  | ||||||
| async function loadSamplers() { | async function loadSamplers() { | ||||||
|     const url = new URL(getApiUrl()); |     $('#sd_sampler').empty(); | ||||||
|     url.pathname = '/api/image/samplers'; |     let samplers = []; | ||||||
|     const result = await fetch(url, defaultRequestArgs); |  | ||||||
|  |  | ||||||
|     if (result.ok) { |     if (extension_settings.sd.horde) { | ||||||
|         const data = await result.json(); |         samplers = await loadHordeSamplers(); | ||||||
|         const samplers = data.samplers; |     } else { | ||||||
|  |         samplers = await loadExtrasSamplers(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     for (const sampler of samplers) { |     for (const sampler of samplers) { | ||||||
|         const option = document.createElement('option'); |         const option = document.createElement('option'); | ||||||
| @@ -189,9 +213,69 @@ async function loadSamplers() { | |||||||
|         $('#sd_sampler').append(option); |         $('#sd_sampler').append(option); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | async function loadHordeSamplers() { | ||||||
|  |     const result = await fetch('/horde_samplers', { | ||||||
|  |         method: 'POST', | ||||||
|  |         headers: getRequestHeaders(), | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     if (result.ok) { | ||||||
|  |         const data = await result.json(); | ||||||
|  |         return data; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return []; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function loadExtrasSamplers() { | ||||||
|  |     const url = new URL(getApiUrl()); | ||||||
|  |     url.pathname = '/api/image/samplers'; | ||||||
|  |     const result = await fetch(url, defaultRequestArgs); | ||||||
|  |  | ||||||
|  |     if (result.ok) { | ||||||
|  |         const data = await result.json(); | ||||||
|  |         return data.samplers; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return []; | ||||||
| } | } | ||||||
|  |  | ||||||
| async function loadModels() { | async function loadModels() { | ||||||
|  |     $('#sd_model').empty(); | ||||||
|  |     let models = []; | ||||||
|  |  | ||||||
|  |     if (extension_settings.sd.horde) { | ||||||
|  |         models = await loadHordeModels(); | ||||||
|  |     } else { | ||||||
|  |         models = await loadExtrasModels(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     for (const model of models) { | ||||||
|  |         const option = document.createElement('option'); | ||||||
|  |         option.innerText = model.text; | ||||||
|  |         option.value = model.value; | ||||||
|  |         option.selected = model.value === extension_settings.sd.model; | ||||||
|  |         $('#sd_model').append(option); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function loadHordeModels() { | ||||||
|  |     const result = await fetch('/horde_models', { | ||||||
|  |         method: 'POST', | ||||||
|  |         headers: getRequestHeaders(), | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     if (result.ok) { | ||||||
|  |         const data = await result.json(); | ||||||
|  |         const models = data.map(x => ({ value: x.name, text: `${x.name} (ETA: ${x.eta}s, Queue: ${x.queued}, Workers: ${x.count})` })); | ||||||
|  |         return models; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return []; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function loadExtrasModels() { | ||||||
|     const url = new URL(getApiUrl()); |     const url = new URL(getApiUrl()); | ||||||
|     url.pathname = '/api/image/model'; |     url.pathname = '/api/image/model'; | ||||||
|     const getCurrentModelResult = await fetch(url, defaultRequestArgs); |     const getCurrentModelResult = await fetch(url, defaultRequestArgs); | ||||||
| @@ -206,16 +290,11 @@ async function loadModels() { | |||||||
|  |  | ||||||
|     if (getModelsResult.ok) { |     if (getModelsResult.ok) { | ||||||
|         const data = await getModelsResult.json(); |         const data = await getModelsResult.json(); | ||||||
|         const models = data.models; |         const view_models = data.models.map(x => ({ value: x, text: x })); | ||||||
|  |         return view_models; | ||||||
|  |     } | ||||||
|  |  | ||||||
|         for (const model of models) { |     return []; | ||||||
|             const option = document.createElement('option'); |  | ||||||
|             option.innerText = model; |  | ||||||
|             option.value = model; |  | ||||||
|             option.selected = model === extension_settings.sd.model; |  | ||||||
|             $('#sd_model').append(option); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| function getGenerationType(prompt) { | function getGenerationType(prompt) { | ||||||
| @@ -257,6 +336,14 @@ async function generatePicture(_, trigger) { | |||||||
|         return; |         return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     if (!modules.includes('sd') && !extension_settings.sd.horde) { | ||||||
|  |         callPopup("Extensions API is not connected or doesn't provide SD module. Enable Stable Horde to generate images.", 'text'); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     extension_settings.sd.sampler = $('#sd_sampler').find(':selected').val(); | ||||||
|  |     extension_settings.sd.model = $('#sd_model').find(':selected').val(); | ||||||
|  |  | ||||||
|     trigger = trigger.trim(); |     trigger = trigger.trim(); | ||||||
|     const generationMode = getGenerationType(trigger); |     const generationMode = getGenerationType(trigger); | ||||||
|     console.log('Generation mode', generationMode, 'triggered with', trigger); |     console.log('Generation mode', generationMode, 'triggered with', trigger); | ||||||
| @@ -279,6 +366,22 @@ async function generatePicture(_, trigger) { | |||||||
|  |  | ||||||
|         console.log('Processed Stable Diffusion prompt:', prompt); |         console.log('Processed Stable Diffusion prompt:', prompt); | ||||||
|  |  | ||||||
|  |         if (extension_settings.sd.horde) { | ||||||
|  |             await generateHordeImage(prompt); | ||||||
|  |         } else { | ||||||
|  |             await generateExtrasImage(prompt); | ||||||
|  |         } | ||||||
|  |     } catch (err) { | ||||||
|  |         console.trace(err); | ||||||
|  |         throw new Error('SD prompt text generation failed.') | ||||||
|  |     } | ||||||
|  |     finally { | ||||||
|  |         context.activateSendButtons(); | ||||||
|  |         showSwipeButtons(); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function generateExtrasImage(prompt) { | ||||||
|     const url = new URL(getApiUrl()); |     const url = new URL(getApiUrl()); | ||||||
|     url.pathname = '/api/image'; |     url.pathname = '/api/image'; | ||||||
|     const result = await fetch(url, { |     const result = await fetch(url, { | ||||||
| @@ -295,22 +398,39 @@ async function generatePicture(_, trigger) { | |||||||
|             negative_prompt: extension_settings.sd.negative_prompt, |             negative_prompt: extension_settings.sd.negative_prompt, | ||||||
|             restore_faces: true, |             restore_faces: true, | ||||||
|             face_restoration_model: 'GFPGAN', |             face_restoration_model: 'GFPGAN', | ||||||
|  |  | ||||||
|         }), |         }), | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |  | ||||||
|     if (result.ok) { |     if (result.ok) { | ||||||
|         const data = await result.json(); |         const data = await result.json(); | ||||||
|         const base64Image = `data:image/jpeg;base64,${data.image}`; |         const base64Image = `data:image/jpeg;base64,${data.image}`; | ||||||
|         sendMessage(prompt, base64Image); |         sendMessage(prompt, base64Image); | ||||||
|     } |     } | ||||||
|     } catch (err) { |  | ||||||
|         console.trace(err); |  | ||||||
|         throw new Error('SD prompt text generation failed.') |  | ||||||
| } | } | ||||||
|     finally { |  | ||||||
|         context.activateSendButtons(); | async function generateHordeImage(prompt) { | ||||||
|         showSwipeButtons(); |     const result = await fetch('/horde_generateimage', { | ||||||
|  |         method: 'POST', | ||||||
|  |         headers: getRequestHeaders(), | ||||||
|  |         body: JSON.stringify({ | ||||||
|  |             prompt: prompt, | ||||||
|  |             sampler: extension_settings.sd.sampler, | ||||||
|  |             steps: extension_settings.sd.steps, | ||||||
|  |             scale: extension_settings.sd.scale, | ||||||
|  |             width: extension_settings.sd.width, | ||||||
|  |             height: extension_settings.sd.height, | ||||||
|  |             prompt_prefix: extension_settings.sd.prompt_prefix, | ||||||
|  |             negative_prompt: extension_settings.sd.negative_prompt, | ||||||
|  |             model: extension_settings.sd.model, | ||||||
|  |             nsfw: extension_settings.sd.horde_nsfw, | ||||||
|  |         }), | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     if (result.ok) { | ||||||
|  |         const data = await result.text(); | ||||||
|  |         const base64Image = `data:image/webp;base64,${data}`; | ||||||
|  |         sendMessage(prompt, base64Image); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -386,16 +506,6 @@ function addSDGenButtons() { | |||||||
| async function moduleWorker() { | async function moduleWorker() { | ||||||
|     const context = getContext(); |     const context = getContext(); | ||||||
|  |  | ||||||
|     /*     if (context.onlineStatus === 'no_connection') { |  | ||||||
|             $('#sd_gen').hide(200); |  | ||||||
|         } else if ($("#send_but").css('display') === 'flex') { |  | ||||||
|             $('#sd_gen').show(200); |  | ||||||
|             $("#sd_gen_wait").hide(200); |  | ||||||
|         } else { |  | ||||||
|             $('#sd_gen').hide(200); |  | ||||||
|             $("#sd_gen_wait").show(200); |  | ||||||
|         } */ |  | ||||||
|  |  | ||||||
|     context.onlineStatus === 'no_connection' |     context.onlineStatus === 'no_connection' | ||||||
|         ? $('#sd_gen').hide(200) |         ? $('#sd_gen').hide(200) | ||||||
|         : $('#sd_gen').show(200) |         : $('#sd_gen').show(200) | ||||||
| @@ -444,6 +554,16 @@ jQuery(async () => { | |||||||
|         </div> |         </div> | ||||||
|         <div class="inline-drawer-content"> |         <div class="inline-drawer-content"> | ||||||
|             <small><i>Use slash commands to generate images. Type <span class="monospace">/help</span> in chat for more details</i></small> |             <small><i>Use slash commands to generate images. Type <span class="monospace">/help</span> in chat for more details</i></small> | ||||||
|  |             <br> | ||||||
|  |             <small><i>Hint: Save an API key in Horde KoboldAI API settings to use it here.</i></small> | ||||||
|  |             <label class="checkbox_label"> | ||||||
|  |                 <input id="sd_horde" type="checkbox" /> | ||||||
|  |                 Use Stable Horde | ||||||
|  |             </label> | ||||||
|  |             <label style="margin-left:1em;" class="checkbox_label"> | ||||||
|  |                 <input id="sd_horde_nsfw" type="checkbox" /> | ||||||
|  |                 Allow NSFW images | ||||||
|  |             </label> | ||||||
|             <label for="sd_scale">CFG Scale (<span id="sd_scale_value"></span>)</label> |             <label for="sd_scale">CFG Scale (<span id="sd_scale_value"></span>)</label> | ||||||
|             <input id="sd_scale" type="range" min="${defaultSettings.scale_min}" max="${defaultSettings.scale_max}" step="${defaultSettings.scale_step}" value="${defaultSettings.scale}" /> |             <input id="sd_scale" type="range" min="${defaultSettings.scale_min}" max="${defaultSettings.scale_max}" step="${defaultSettings.scale_step}" value="${defaultSettings.scale}" /> | ||||||
|             <label for="sd_steps">Sampling steps (<span id="sd_steps_value"></span>)</label> |             <label for="sd_steps">Sampling steps (<span id="sd_steps_value"></span>)</label> | ||||||
| @@ -472,6 +592,8 @@ jQuery(async () => { | |||||||
|     $('#sd_negative_prompt').on('input', onNegativePromptInput); |     $('#sd_negative_prompt').on('input', onNegativePromptInput); | ||||||
|     $('#sd_width').on('input', onWidthInput); |     $('#sd_width').on('input', onWidthInput); | ||||||
|     $('#sd_height').on('input', onHeightInput); |     $('#sd_height').on('input', onHeightInput); | ||||||
|  |     $('#sd_horde').on('input', onHordeInput); | ||||||
|  |     $('#sd_horde_nsfw').on('input', onHordeNsfwInput); | ||||||
|  |  | ||||||
|     $('.sd_settings .inline-drawer-toggle').on('click', function () { |     $('.sd_settings .inline-drawer-toggle').on('click', function () { | ||||||
|         initScrollHeight($("#sd_prompt_prefix")); |         initScrollHeight($("#sd_prompt_prefix")); | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| .sd_settings label { | .sd_settings label:not(.checkbox_label) { | ||||||
|     display: block; |     display: block; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -16,7 +16,6 @@ | |||||||
|     display: flex; |     display: flex; | ||||||
|     align-items: center; |     align-items: center; | ||||||
|     justify-content: center; |     justify-content: center; | ||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| #sd_gen:hover { | #sd_gen:hover { | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								server.js
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								server.js
									
									
									
									
									
								
							| @@ -2925,10 +2925,21 @@ app.post('/viewsecrets', jsonParser, async (_, response) => { | |||||||
|     } |     } | ||||||
| }); | }); | ||||||
|  |  | ||||||
| app.post('/horde_generateimage', async (request, response) => { | app.post('/horde_samplers', jsonParser, async (_, response) => { | ||||||
|  |     const samplers = Object.values(ai_horde.ModelGenerationInputStableSamplers); | ||||||
|  |     response.send(samplers); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | app.post('/horde_models', jsonParser, async (_, response) => { | ||||||
|  |     const models = await ai_horde.getModels(); | ||||||
|  |     response.send(models); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | app.post('/horde_generateimage', jsonParser, async (request, response) => { | ||||||
|     const MAX_ATTEMPTS = 100; |     const MAX_ATTEMPTS = 100; | ||||||
|     const CHECK_INTERVAL = 3000; |     const CHECK_INTERVAL = 3000; | ||||||
|     const api_key_horde = readSecret(SECRET_KEYS.HORDE) || ANONYMOUS_KEY; |     const api_key_horde = readSecret(SECRET_KEYS.HORDE) || ANONYMOUS_KEY; | ||||||
|  |     console.log('Stable Horde request:', request.body); | ||||||
|     const generation = await ai_horde.postAsyncImageGenerate( |     const generation = await ai_horde.postAsyncImageGenerate( | ||||||
|         { |         { | ||||||
|             prompt: `${request.body.prompt_prefix} ${request.body.prompt} ### ${request.body.negative_prompt}`, |             prompt: `${request.body.prompt_prefix} ${request.body.prompt} ### ${request.body.negative_prompt}`, | ||||||
| @@ -2950,12 +2961,17 @@ app.post('/horde_generateimage', async (request, response) => { | |||||||
|     for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { |     for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { | ||||||
|         await delay(CHECK_INTERVAL); |         await delay(CHECK_INTERVAL); | ||||||
|         const check = await ai_horde.getImageGenerationCheck(generation.id); |         const check = await ai_horde.getImageGenerationCheck(generation.id); | ||||||
|  |         console.log(check); | ||||||
|      |      | ||||||
|         if (check.done) { |         if (check.done) { | ||||||
|             const result = await ai_horde.getImageGenerationStatus(generation.id); |             const result = await ai_horde.getImageGenerationStatus(generation.id); | ||||||
|             return response.send(result.generations[0].img); |             return response.send(result.generations[0].img); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         if (!check.is_possible) { | ||||||
|  |             return response.sendStatus(503); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         if (check.faulted) { |         if (check.faulted) { | ||||||
|             return response.sendStatus(500); |             return response.sendStatus(500); | ||||||
|         } |         } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user