Stable diffusion.cpp server support (#5074) * feat: Add stable-diffusion.cpp server endpoints * feat: Add stable-diffusion.cpp server settings * feat: Add stable-diffusion.cpp server * Add debug log for sdcpp payload * feat: add loadSdcppSamplers and loadSdcppSchedulers for sdcpp backend * feat: add sdcpp to sampler and scheduler source lists --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -79,6 +79,7 @@ const sources = { | ||
| 79 | 79 | extras: 'extras', |
| 80 | 80 | horde: 'horde', |
| 81 | 81 | auto: 'auto', |
| 82 | + sdcpp: 'sdcpp', | |
| 82 | 83 | novel: 'novel', |
| 83 | 84 | vlad: 'vlad', |
| 84 | 85 | openai: 'openai', |
| @@ -290,6 +291,9 @@ const defaultSettings = { | ||
| 290 | 291 | auto_url: 'http://localhost:7860', |
| 291 | 292 | auto_auth: '', |
| 292 | 293 | |
| 294 | + // stable-diffusion.cpp settings | |
| 295 | + sdcpp_url: 'http://127.0.0.1:1234', | |
| 296 | + | |
| 293 | 297 | vlad_url: 'http://localhost:7860', |
| 294 | 298 | vlad_auth: '', |
| 295 | 299 | |
| @@ -528,6 +532,7 @@ async function loadSettings() { | ||
| 528 | 532 | $('#sd_multimodal_captioning').prop('checked', extension_settings.sd.multimodal_captioning); |
| 529 | 533 | $('#sd_auto_url').val(extension_settings.sd.auto_url); |
| 530 | 534 | $('#sd_auto_auth').val(extension_settings.sd.auto_auth); |
| 535 | + $('#sd_sdcpp_url').val(extension_settings.sd.sdcpp_url); | |
| 531 | 536 | $('#sd_vlad_url').val(extension_settings.sd.vlad_url); |
| 532 | 537 | $('#sd_vlad_auth').val(extension_settings.sd.vlad_auth); |
| 533 | 538 | $('#sd_drawthings_url').val(extension_settings.sd.drawthings_url); |
| @@ -1156,6 +1161,11 @@ function onAutoAuthInput() { | ||
| 1156 | 1161 | saveSettingsDebounced(); |
| 1157 | 1162 | } |
| 1158 | 1163 | |
| 1164 | +function onSdcppUrlInput() { | |
| 1165 | + extension_settings.sd.sdcpp_url = $('#sd_sdcpp_url').val(); | |
| 1166 | + saveSettingsDebounced(); | |
| 1167 | +} | |
| 1168 | + | |
| 1159 | 1169 | function onVladUrlInput() { |
| 1160 | 1170 | extension_settings.sd.vlad_url = $('#sd_vlad_url').val(); |
| 1161 | 1171 | saveSettingsDebounced(); |
| @@ -1264,6 +1274,29 @@ async function validateAutoUrl() { | ||
| 1264 | 1274 | } |
| 1265 | 1275 | } |
| 1266 | 1276 | |
| 1277 | +async function validateSdcppUrl() { | |
| 1278 | + try { | |
| 1279 | + if (!extension_settings.sd.sdcpp_url) { | |
| 1280 | + throw new Error('URL is not set.'); | |
| 1281 | + } | |
| 1282 | + | |
| 1283 | + const result = await fetch('/api/sd/sdcpp/ping', { | |
| 1284 | + method: 'POST', | |
| 1285 | + headers: getRequestHeaders(), | |
| 1286 | + body: JSON.stringify({ url: extension_settings.sd.sdcpp_url }), | |
| 1287 | + }); | |
| 1288 | + | |
| 1289 | + if (!result.ok) { | |
| 1290 | + throw new Error('stable-diffusion.cpp server returned an error.'); | |
| 1291 | + } | |
| 1292 | + | |
| 1293 | + await loadSettingOptions(); | |
| 1294 | + toastr.success('stable-diffusion.cpp server connected.'); | |
| 1295 | + } catch (error) { | |
| 1296 | + toastr.error(`Could not validate stable-diffusion.cpp server: ${error.message}`); | |
| 1297 | + } | |
| 1298 | +} | |
| 1299 | + | |
| 1267 | 1300 | async function validateDrawthingsUrl() { |
| 1268 | 1301 | try { |
| 1269 | 1302 | if (!extension_settings.sd.drawthings_url) { |
| @@ -1557,6 +1590,9 @@ async function loadSamplers() { | ||
| 1557 | 1590 | case sources.auto: |
| 1558 | 1591 | samplers = await loadAutoSamplers(); |
| 1559 | 1592 | break; |
| 1593 | + case sources.sdcpp: | |
| 1594 | + samplers = await loadSdcppSamplers(); | |
| 1595 | + break; | |
| 1560 | 1596 | case sources.drawthings: |
| 1561 | 1597 | samplers = await loadDrawthingsSamplers(); |
| 1562 | 1598 | break; |
| @@ -1682,6 +1718,11 @@ async function loadAutoSamplers() { | ||
| 1682 | 1718 | } |
| 1683 | 1719 | } |
| 1684 | 1720 | |
| 1721 | +async function loadSdcppSamplers() { | |
| 1722 | + // The sdcpp server does not provide an API for samplers, so we return the known list. | |
| 1723 | + return ['euler', 'euler_a', 'heun', 'dpm2', 'dpm++2s_a', 'dpm++2m', 'dpm++2mv2', 'ipndm', 'ipndm_v', 'lcm', 'ddim_trailing', 'tcd']; | |
| 1724 | +} | |
| 1725 | + | |
| 1685 | 1726 | async function loadDrawthingsSamplers() { |
| 1686 | 1727 | // The app developer doesn't provide an API to get these yet |
| 1687 | 1728 | return [ |
| @@ -1771,6 +1812,9 @@ async function loadModels() { | ||
| 1771 | 1812 | case sources.auto: |
| 1772 | 1813 | models = await loadAutoModels(); |
| 1773 | 1814 | break; |
| 1815 | + case sources.sdcpp: | |
| 1816 | + models = [{ value: '', text: 'N/A' }]; | |
| 1817 | + break; | |
| 1774 | 1818 | case sources.drawthings: |
| 1775 | 1819 | models = await loadDrawthingsModels(); |
| 1776 | 1820 | break; |
| @@ -2379,6 +2423,9 @@ async function loadSchedulers() { | ||
| 2379 | 2423 | case sources.auto: |
| 2380 | 2424 | schedulers = await getAutoRemoteSchedulers(); |
| 2381 | 2425 | break; |
| 2426 | + case sources.sdcpp: | |
| 2427 | + schedulers = await loadSdcppSchedulers(); | |
| 2428 | + break; | |
| 2382 | 2429 | case sources.novel: |
| 2383 | 2430 | schedulers = loadNovelSchedulers(); |
| 2384 | 2431 | break; |
| @@ -2477,6 +2524,11 @@ async function loadComfySchedulers() { | ||
| 2477 | 2524 | } |
| 2478 | 2525 | } |
| 2479 | 2526 | |
| 2527 | +async function loadSdcppSchedulers() { | |
| 2528 | + // The sdcpp server does not provide an API for schedulers, so we return the known list. | |
| 2529 | + return ['discrete', 'karras', 'exponential', 'ays', 'gits', 'smoothstep', 'sgm_uniform', 'simple', 'kl_optimal', 'lcm']; | |
| 2530 | +} | |
| 2531 | + | |
| 2480 | 2532 | async function loadVaes() { |
| 2481 | 2533 | $('#sd_vae').empty(); |
| 2482 | 2534 | let vaes = []; |
| @@ -2491,6 +2543,9 @@ async function loadVaes() { | ||
| 2491 | 2543 | case sources.auto: |
| 2492 | 2544 | vaes = await loadAutoVaes(); |
| 2493 | 2545 | break; |
| 2546 | + case sources.sdcpp: | |
| 2547 | + vaes = ['N/A']; | |
| 2548 | + break; | |
| 2494 | 2549 | case sources.novel: |
| 2495 | 2550 | vaes = ['N/A']; |
| 2496 | 2551 | break; |
| @@ -3166,6 +3221,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP | ||
| 3166 | 3221 | case sources.auto: |
| 3167 | 3222 | result = await generateAutoImage(prefixedPrompt, negativePrompt, signal); |
| 3168 | 3223 | break; |
| 3224 | + case sources.sdcpp: | |
| 3225 | + result = await generateSdcppImage(prefixedPrompt, negativePrompt, signal); | |
| 3226 | + break; | |
| 3169 | 3227 | case sources.novel: |
| 3170 | 3228 | result = await generateNovelImage(prefixedPrompt, negativePrompt, signal); |
| 3171 | 3229 | break; |
| @@ -3654,6 +3712,55 @@ async function generateAutoImage(prompt, negativePrompt, signal) { | ||
| 3654 | 3712 | } |
| 3655 | 3713 | |
| 3656 | 3714 | /** |
| 3715 | + * Generates an image using stable-diffusion.cpp server API. | |
| 3716 | + * | |
| 3717 | + * @param {string} prompt - The main instruction used to guide the image generation. | |
| 3718 | + * @param {string} negativePrompt - The instruction used to restrict the image generation. | |
| 3719 | + * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. | |
| 3720 | + * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. | |
| 3721 | + */ | |
| 3722 | +async function generateSdcppImage(prompt, negativePrompt, signal) { | |
| 3723 | + const payload = { | |
| 3724 | + url: extension_settings.sd.sdcpp_url, | |
| 3725 | + prompt: prompt, | |
| 3726 | + negative_prompt: negativePrompt, | |
| 3727 | + steps: extension_settings.sd.steps, | |
| 3728 | + cfg_scale: extension_settings.sd.scale, | |
| 3729 | + width: extension_settings.sd.width, | |
| 3730 | + height: extension_settings.sd.height, | |
| 3731 | + batch_size: 1, | |
| 3732 | + seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, | |
| 3733 | + }; | |
| 3734 | + | |
| 3735 | + if (extension_settings.sd.sampler && extension_settings.sd.sampler !== 'N/A') { | |
| 3736 | + payload.sampler_name = extension_settings.sd.sampler; | |
| 3737 | + } | |
| 3738 | + | |
| 3739 | + if (extension_settings.sd.scheduler && extension_settings.sd.scheduler !== 'N/A') { | |
| 3740 | + payload.scheduler = extension_settings.sd.scheduler; | |
| 3741 | + } | |
| 3742 | + | |
| 3743 | + if (Number.isFinite(extension_settings.sd.clip_skip)) { | |
| 3744 | + payload.clip_skip = extension_settings.sd.clip_skip; | |
| 3745 | + } | |
| 3746 | + | |
| 3747 | + const result = await fetch('/api/sd/sdcpp/generate', { | |
| 3748 | + method: 'POST', | |
| 3749 | + headers: getRequestHeaders(), | |
| 3750 | + signal: signal, | |
| 3751 | + body: JSON.stringify(payload), | |
| 3752 | + }); | |
| 3753 | + | |
| 3754 | + if (result.ok) { | |
| 3755 | + const data = await result.json(); | |
| 3756 | + return { format: 'png', data: data.images?.[0] }; | |
| 3757 | + } else { | |
| 3758 | + const text = await result.text(); | |
| 3759 | + throw new Error(text); | |
| 3760 | + } | |
| 3761 | +} | |
| 3762 | + | |
| 3763 | +/** | |
| 3657 | 3764 | * Generates an image in Drawthings API using the provided prompt and configuration settings. |
| 3658 | 3765 | * |
| 3659 | 3766 | * @param {string} prompt - The main instruction used to guide the image generation. |
| @@ -4736,6 +4843,8 @@ function isValidState() { | ||
| 4736 | 4843 | return true; |
| 4737 | 4844 | case sources.auto: |
| 4738 | 4845 | return !!extension_settings.sd.auto_url; |
| 4846 | + case sources.sdcpp: | |
| 4847 | + return !!extension_settings.sd.sdcpp_url; | |
| 4739 | 4848 | case sources.drawthings: |
| 4740 | 4849 | return !!extension_settings.sd.drawthings_url; |
| 4741 | 4850 | case sources.vlad: |
| @@ -5418,6 +5527,8 @@ jQuery(async () => { | ||
| 5418 | 5527 | $('#sd_auto_validate').on('click', validateAutoUrl); |
| 5419 | 5528 | $('#sd_auto_url').on('input', onAutoUrlInput); |
| 5420 | 5529 | $('#sd_auto_auth').on('input', onAutoAuthInput); |
| 5530 | + $('#sd_sdcpp_validate').on('click', validateSdcppUrl); | |
| 5531 | + $('#sd_sdcpp_url').on('input', onSdcppUrlInput); | |
| 5421 | 5532 | $('#sd_drawthings_validate').on('click', validateDrawthingsUrl); |
| 5422 | 5533 | $('#sd_drawthings_url').on('input', onDrawthingsUrlInput); |
| 5423 | 5534 | $('#sd_drawthings_auth').on('input', onDrawthingsAuthInput); |
| @@ -55,6 +55,7 @@ | ||
| 55 | 55 | <option value="vlad">SD.Next (vladmandic)</option> |
| 56 | 56 | <option value="stability">Stability AI</option> |
| 57 | 57 | <option value="auto">Stable Diffusion Web UI (AUTOMATIC1111)</option> |
| 58 | + <option value="sdcpp">stable-diffusion.cpp server</option> | |
| 58 | 59 | <option value="horde">Stable Horde</option> |
| 59 | 60 | <option value="togetherai">TogetherAI</option> |
| 60 | 61 | <option value="xai">xAI (Grok)</option> |
| @@ -76,6 +77,19 @@ | ||
| 76 | 77 | <!-- (Original Text)<b>Important:</b> run SD Web UI with the <tt>--api</tt> flag! The server must be accessible from the SillyTavern host machine. --> |
| 77 | 78 | <i><b data-i18n="Important:">Important:</b></i><i data-i18n="sd_auto_auth_warning_1"> run SD Web UI with the </i><i><tt>--api</tt></i><i data-i18n="sd_auto_auth_warning_2"> flag! The server must be accessible from the SillyTavern host machine.</i> |
| 78 | 79 | </div> |
| 80 | + <div data-sd-source="sdcpp"> | |
| 81 | + <label for="sd_sdcpp_url">stable-diffusion.cpp URL</label> | |
| 82 | + <div class="flex-container flexnowrap"> | |
| 83 | + <input id="sd_sdcpp_url" type="text" class="text_pole" placeholder="Example: {{sdcpp_url}}" value="{{sdcpp_url}}" /> | |
| 84 | + <div id="sd_sdcpp_validate" class="menu_button menu_button_icon"> | |
| 85 | + <i class="fa-solid fa-check"></i> | |
| 86 | + <span data-i18n="Connect"> | |
| 87 | + Connect | |
| 88 | + </span> | |
| 89 | + </div> | |
| 90 | + </div> | |
| 91 | + <i data-i18n="The server must be accessible from the SillyTavern host machine.">The server must be accessible from the SillyTavern host machine.</i> | |
| 92 | + </div> | |
| 79 | 93 | <div data-sd-source="drawthings"> |
| 80 | 94 | <label for="sd_drawthings_url">DrawThings API URL</label> |
| 81 | 95 | <div class="flex-container flexnowrap"> |
| @@ -395,12 +409,12 @@ | ||
| 395 | 409 | </div> |
| 396 | 410 | |
| 397 | 411 | <div class="flex-container"> |
| 398 | 412 | <div class="flex1" data-sd-source="extras,horde,auto,drawthings,novel,vlad,comfy,sdcpp"> |
| 399 | 413 | <label for="sd_sampler" data-i18n="Sampling method">Sampling method</label> |
| 400 | 414 | <select id="sd_sampler"></select> |
| 401 | 415 | </div> |
| 402 | 416 | |
| 403 | 417 | <div class="flex1" data-sd-source="comfy,auto,novel,sdcpp"> |
| 404 | 418 | <label for="sd_scheduler" data-i18n="Scheduler">Scheduler</label> |
| 405 | 419 | <select id="sd_scheduler"></select> |
| 406 | 420 | </div> |
| @@ -483,7 +497,7 @@ | ||
| 483 | 497 | <input class="neo-range-input" type="number" id="sd_hr_second_pass_steps_value" data-for="sd_hr_second_pass_steps" max="{{hr_second_pass_steps_max}}" step="{{hr_second_pass_steps_step}}" value="{{hr_second_pass_steps}}" > |
| 484 | 498 | </div> |
| 485 | 499 | |
| 486 | 500 | <div class="alignitemscenter flex-container flexFlowColumn flexGrow flexShrink gap0 flexBasis48p" data-sd-source="auto,vlad,comfy,horde,drawthings,extras,sdcpp"> |
| 487 | 501 | <small> |
| 488 | 502 | <span data-i18n="CLIP Skip">CLIP Skip</span> |
| 489 | 503 | </small> |
| @@ -537,7 +551,7 @@ | ||
| 537 | 551 | </label> |
| 538 | 552 | </div> |
| 539 | 553 | |
| 540 | 554 | <div data-sd-source="novel,togetherai,pollinations,comfy,drawthings,vlad,auto,horde,extras,stability,bfl,sdcpp" class="marginTop5"> |
| 541 | 555 | <label for="sd_seed"> |
| 542 | 556 | <span data-i18n="Seed">Seed</span> |
| 543 | 557 | <small data-i18n="(-1 for random)">(-1 for random)</small> |
| @@ -801,6 +801,73 @@ together.post('/generate', async (request, response) => { | ||
| 801 | 801 | } |
| 802 | 802 | }); |
| 803 | 803 | |
| 804 | +const sdcpp = express.Router(); | |
| 805 | + | |
| 806 | +sdcpp.post('/ping', async (request, response) => { | |
| 807 | + try { | |
| 808 | + const url = new URL(request.body.url); | |
| 809 | + url.pathname = '/v1/images/generations'; | |
| 810 | + | |
| 811 | + const result = await fetch(url, { method: 'OPTIONS' }); | |
| 812 | + if (!result.ok) { | |
| 813 | + throw new Error('stable-diffusion.cpp server returned an error.'); | |
| 814 | + } | |
| 815 | + | |
| 816 | + return response.sendStatus(200); | |
| 817 | + } catch (error) { | |
| 818 | + console.error(error); | |
| 819 | + return response.sendStatus(500); | |
| 820 | + } | |
| 821 | +}); | |
| 822 | + | |
| 823 | +sdcpp.post('/generate', async (request, response) => { | |
| 824 | + try { | |
| 825 | + const url = new URL(request.body.url); | |
| 826 | + url.pathname = '/sdapi/v1/txt2img'; | |
| 827 | + | |
| 828 | + const payload = { | |
| 829 | + prompt: request.body.prompt, | |
| 830 | + negative_prompt: request.body.negative_prompt, | |
| 831 | + width: request.body.width, | |
| 832 | + height: request.body.height, | |
| 833 | + steps: request.body.steps, | |
| 834 | + cfg_scale: request.body.cfg_scale, | |
| 835 | + seed: request.body.seed, | |
| 836 | + batch_size: request.body.batch_size, | |
| 837 | + sampler_name: request.body.sampler_name, | |
| 838 | + scheduler: request.body.scheduler, | |
| 839 | + clip_skip: request.body.clip_skip, | |
| 840 | + }; | |
| 841 | + | |
| 842 | + for (const [key, value] of Object.entries(payload)) { | |
| 843 | + if (value === undefined || value === null || value === '') { | |
| 844 | + delete payload[key]; | |
| 845 | + } | |
| 846 | + } | |
| 847 | + | |
| 848 | + console.debug('stable-diffusion.cpp request:', payload); | |
| 849 | + | |
| 850 | + const result = await fetch(url, { | |
| 851 | + method: 'POST', | |
| 852 | + body: JSON.stringify(payload), | |
| 853 | + headers: { | |
| 854 | + 'Content-Type': 'application/json', | |
| 855 | + }, | |
| 856 | + }); | |
| 857 | + | |
| 858 | + if (!result.ok) { | |
| 859 | + const text = await result.text(); | |
| 860 | + throw new Error('stable-diffusion.cpp server returned an error.', { cause: text }); | |
| 861 | + } | |
| 862 | + | |
| 863 | + const data = await result.json(); | |
| 864 | + return response.send(data); | |
| 865 | + } catch (error) { | |
| 866 | + console.error(error); | |
| 867 | + return response.sendStatus(500); | |
| 868 | + } | |
| 869 | +}); | |
| 870 | + | |
| 804 | 871 | const drawthings = express.Router(); |
| 805 | 872 | |
| 806 | 873 | drawthings.post('/ping', async (request, response) => { |
| @@ -1917,6 +1984,7 @@ zai.post('/generate-video', async (request, response) => { | ||
| 1917 | 1984 | router.use('/comfy', comfy); |
| 1918 | 1985 | router.use('/comfyrunpod', comfyRunPod); |
| 1919 | 1986 | router.use('/together', together); |
| 1987 | +router.use('/sdcpp', sdcpp); | |
| 1920 | 1988 | router.use('/drawthings', drawthings); |
| 1921 | 1989 | router.use('/pollinations', pollinations); |
| 1922 | 1990 | router.use('/stability', stability); |