Comfyui serverless runpod image generation (#4891) * Add comfyrunpod generation endpoints. Get a bunch of settings added. Probably get runpod api secret stored properly. sending to runpod. Correct workflow wrapping for runpod. * Reuse comfyui workflow setting block for runpod. * Update API key url to point to runpod settings * Remove debug print. * Combine normal and runpod comfyui generation functions * Remove unused server endpoints for runpod. * Remove debug prints from server. * Remove redundant switch case. * Check for url to see if image generation is possible for comfy and comfy runpod. * lint * Correct/update doc comments. * Make runpod serverless a sub-type under comfyui source in the UI. * Set comfy_type in stable-diffusion defaultSettings * Allow loading comfy workflows even if url isnt set yet. * Change default runpod url to template example * Rename runpod api key to comfy_runpod * Handle default switches separately from standard now that default setting is ensured. * Update runpod api key name on server. * Move example runpod url to placeholder. * lint * Remove incorrect comment. * Hide toasts remote models toasts * React to key updates * Add type annotation for data and improve workflow handling in generate endpoint --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -93,6 +93,10 @@ const sources = { | |||
| 93 | zai: 'zai', | 93 | zai: 'zai', |
| 94 | openrouter: 'openrouter', | 94 | openrouter: 'openrouter', |
| 95 | }; | 95 | }; |
| 96 | const comfyTypes = { | ||
| 97 | standard: 'standard', | ||
| 98 | runpod_serverless: 'runpod_serverless', | ||
| 99 | }; | ||
| 96 | 100 | ||
| 97 | const initiators = { | 101 | const initiators = { |
| 98 | command: 'command', | 102 | command: 'command', |
| @@ -322,9 +326,13 @@ const defaultSettings = { | |||
| 322 | styles: defaultStyles, | 326 | styles: defaultStyles, |
| 323 | 327 | ||
| 324 | // ComyUI settings | 328 | // ComyUI settings |
| 329 | comfy_type: 'standard', | ||
| 330 | |||
| 325 | comfy_url: 'http://127.0.0.1:8188', | 331 | comfy_url: 'http://127.0.0.1:8188', |
| 326 | comfy_workflow: 'Default_Comfy_Workflow.json', | 332 | comfy_workflow: 'Default_Comfy_Workflow.json', |
| 327 | 333 | ||
| 334 | comfy_runpod_url: '', | ||
| 335 | |||
| 328 | // Pollinations settings | 336 | // Pollinations settings |
| 329 | pollinations_enhance: false, | 337 | pollinations_enhance: false, |
| 330 | 338 | ||
| @@ -437,6 +445,10 @@ function toggleSourceControls() { | |||
| 437 | const source = $(this).data('sd-source').split(','); | 445 | const source = $(this).data('sd-source').split(','); |
| 438 | $(this).toggle(source.includes(extension_settings.sd.source)); | 446 | $(this).toggle(source.includes(extension_settings.sd.source)); |
| 439 | }); | 447 | }); |
| 448 | $('.sd_settings [data-sd-comfy-type]').each(function () { | ||
| 449 | const source = $(this).data('sd-comfy-type').split(','); | ||
| 450 | $(this).toggle(source.includes(extension_settings.sd.comfy_type)); | ||
| 451 | }); | ||
| 440 | } | 452 | } |
| 441 | 453 | ||
| 442 | async function loadSettings() { | 454 | async function loadSettings() { |
| @@ -517,8 +529,10 @@ async function loadSettings() { | |||
| 517 | $('#sd_openai_style').val(extension_settings.sd.openai_style); | 529 | $('#sd_openai_style').val(extension_settings.sd.openai_style); |
| 518 | $('#sd_openai_quality').val(extension_settings.sd.openai_quality); | 530 | $('#sd_openai_quality').val(extension_settings.sd.openai_quality); |
| 519 | $('#sd_openai_duration').val(extension_settings.sd.openai_duration); | 531 | $('#sd_openai_duration').val(extension_settings.sd.openai_duration); |
| 532 | $('#sd_comfy_type').val(extension_settings.sd.comfy_type); | ||
| 520 | $('#sd_comfy_url').val(extension_settings.sd.comfy_url); | 533 | $('#sd_comfy_url').val(extension_settings.sd.comfy_url); |
| 521 | $('#sd_comfy_prompt').val(extension_settings.sd.comfy_prompt); | 534 | $('#sd_comfy_prompt').val(extension_settings.sd.comfy_prompt); |
| 535 | $('#sd_comfy_runpod_url').val(extension_settings.sd.comfy_runpod_url); | ||
| 522 | $('#sd_snap').prop('checked', extension_settings.sd.snap); | 536 | $('#sd_snap').prop('checked', extension_settings.sd.snap); |
| 523 | $('#sd_clip_skip').val(extension_settings.sd.clip_skip); | 537 | $('#sd_clip_skip').val(extension_settings.sd.clip_skip); |
| 524 | $('#sd_clip_skip_value').val(extension_settings.sd.clip_skip); | 538 | $('#sd_clip_skip_value').val(extension_settings.sd.clip_skip); |
| @@ -1016,6 +1030,11 @@ async function onSourceChange() { | |||
| 1016 | await loadSettingOptions(); | 1030 | await loadSettingOptions(); |
| 1017 | } | 1031 | } |
| 1018 | 1032 | ||
| 1033 | async function onComfyTypeChange() { | ||
| 1034 | extension_settings.sd.comfy_type = $('#sd_comfy_type').find(':selected').val(); | ||
| 1035 | await onSourceChange(); | ||
| 1036 | } | ||
| 1037 | |||
| 1019 | function onFunctionToolInput() { | 1038 | function onFunctionToolInput() { |
| 1020 | extension_settings.sd.function_tool = !!$(this).prop('checked'); | 1039 | extension_settings.sd.function_tool = !!$(this).prop('checked'); |
| 1021 | saveSettingsDebounced(); | 1040 | saveSettingsDebounced(); |
| @@ -1166,11 +1185,15 @@ function onHrSecondPassStepsInput() { | |||
| 1166 | } | 1185 | } |
| 1167 | 1186 | ||
| 1168 | function onComfyUrlInput() { | 1187 | function onComfyUrlInput() { |
| 1169 | // Remove trailing slashes | ||
| 1170 | extension_settings.sd.comfy_url = String($('#sd_comfy_url').val()); | 1188 | extension_settings.sd.comfy_url = String($('#sd_comfy_url').val()); |
| 1171 | saveSettingsDebounced(); | 1189 | saveSettingsDebounced(); |
| 1172 | } | 1190 | } |
| 1173 | 1191 | ||
| 1192 | function onComfyRunPodUrlInput() { | ||
| 1193 | extension_settings.sd.comfy_runpod_url = String($('#sd_comfy_runpod_url').val()); | ||
| 1194 | saveSettingsDebounced(); | ||
| 1195 | } | ||
| 1196 | |||
| 1174 | function onHFModelInput() { | 1197 | function onHFModelInput() { |
| 1175 | extension_settings.sd.huggingface_model_id = $('#sd_huggingface_model_id').val(); | 1198 | extension_settings.sd.huggingface_model_id = $('#sd_huggingface_model_id').val(); |
| 1176 | saveSettingsDebounced(); | 1199 | saveSettingsDebounced(); |
| @@ -1296,6 +1319,30 @@ async function validateComfyUrl() { | |||
| 1296 | } | 1319 | } |
| 1297 | } | 1320 | } |
| 1298 | 1321 | ||
| 1322 | async function validateComfyRunPodUrl() { | ||
| 1323 | try { | ||
| 1324 | if (!extension_settings.sd.comfy_runpod_url) { | ||
| 1325 | throw new Error('URL is not set.'); | ||
| 1326 | } | ||
| 1327 | |||
| 1328 | const result = await fetch('/api/sd/comfyrunpod/ping', { | ||
| 1329 | method: 'POST', | ||
| 1330 | headers: getRequestHeaders(), | ||
| 1331 | body: JSON.stringify({ | ||
| 1332 | url: extension_settings.sd.comfy_runpod_url, | ||
| 1333 | }), | ||
| 1334 | }); | ||
| 1335 | if (!result.ok) { | ||
| 1336 | throw new Error('ComfyUI RunPod returned an error.'); | ||
| 1337 | } | ||
| 1338 | |||
| 1339 | await loadSettingOptions(); | ||
| 1340 | toastr.success('ComfyUI RunPod API connected.'); | ||
| 1341 | } catch (error) { | ||
| 1342 | toastr.error(`Could not validate ComfyUI RunPod API: ${error.message}`); | ||
| 1343 | } | ||
| 1344 | } | ||
| 1345 | |||
| 1299 | async function onModelChange() { | 1346 | async function onModelChange() { |
| 1300 | const selectedModel = $('#sd_model').find(':selected'); | 1347 | const selectedModel = $('#sd_model').find(':selected'); |
| 1301 | extension_settings.sd.model = selectedModel.val(); | 1348 | extension_settings.sd.model = selectedModel.val(); |
| @@ -1309,27 +1356,13 @@ async function onModelChange() { | |||
| 1309 | 1356 | ||
| 1310 | switchModelSpecificControls(extension_settings.sd.model); | 1357 | switchModelSpecificControls(extension_settings.sd.model); |
| 1311 | 1358 | ||
| 1312 | const cloudSources = [ | 1359 | const updateRemoteModelSources = [ |
| 1313 | sources.horde, | 1360 | sources.auto, |
| 1314 | sources.novel, | 1361 | sources.vlad, |
| 1315 | sources.openai, | 1362 | sources.extras, |
| 1316 | sources.aimlapi, | ||
| 1317 | sources.togetherai, | ||
| 1318 | sources.pollinations, | ||
| 1319 | sources.stability, | ||
| 1320 | sources.huggingface, | ||
| 1321 | sources.electronhub, | ||
| 1322 | sources.nanogpt, | ||
| 1323 | sources.bfl, | ||
| 1324 | sources.falai, | ||
| 1325 | sources.xai, | ||
| 1326 | sources.google, | ||
| 1327 | sources.chutes, | ||
| 1328 | sources.zai, | ||
| 1329 | sources.openrouter, | ||
| 1330 | ]; | 1363 | ]; |
| 1331 | 1364 | ||
| 1332 | if (cloudSources.includes(extension_settings.sd.source)) { | 1365 | if (!updateRemoteModelSources.includes(extension_settings.sd.source)) { |
| 1333 | return; | 1366 | return; |
| 1334 | } | 1367 | } |
| 1335 | 1368 | ||
| @@ -1685,6 +1718,9 @@ async function loadNovelSamplers() { | |||
| 1685 | } | 1718 | } |
| 1686 | 1719 | ||
| 1687 | async function loadComfySamplers() { | 1720 | async function loadComfySamplers() { |
| 1721 | if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) { | ||
| 1722 | return ['N/A']; | ||
| 1723 | } | ||
| 1688 | if (!extension_settings.sd.comfy_url) { | 1724 | if (!extension_settings.sd.comfy_url) { |
| 1689 | return []; | 1725 | return []; |
| 1690 | } | 1726 | } |
| @@ -2279,6 +2315,12 @@ function loadNovelSchedulers() { | |||
| 2279 | } | 2315 | } |
| 2280 | 2316 | ||
| 2281 | async function loadComfyModels() { | 2317 | async function loadComfyModels() { |
| 2318 | if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) { | ||
| 2319 | $('#sd_runpod_key').toggleClass('success', !!secret_state[SECRET_KEYS.COMFY_RUNPOD]); | ||
| 2320 | return [ | ||
| 2321 | { value: '', text: 'N/A' }, | ||
| 2322 | ]; | ||
| 2323 | } | ||
| 2282 | if (!extension_settings.sd.comfy_url) { | 2324 | if (!extension_settings.sd.comfy_url) { |
| 2283 | return []; | 2325 | return []; |
| 2284 | } | 2326 | } |
| @@ -2388,6 +2430,9 @@ async function loadSchedulers() { | |||
| 2388 | } | 2430 | } |
| 2389 | 2431 | ||
| 2390 | async function loadComfySchedulers() { | 2432 | async function loadComfySchedulers() { |
| 2433 | if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) { | ||
| 2434 | return ['N/A']; | ||
| 2435 | } | ||
| 2391 | if (!extension_settings.sd.comfy_url) { | 2436 | if (!extension_settings.sd.comfy_url) { |
| 2392 | return []; | 2437 | return []; |
| 2393 | } | 2438 | } |
| @@ -2521,6 +2566,9 @@ async function loadAutoVaes() { | |||
| 2521 | } | 2566 | } |
| 2522 | 2567 | ||
| 2523 | async function loadComfyVaes() { | 2568 | async function loadComfyVaes() { |
| 2569 | if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) { | ||
| 2570 | return ['N/A']; | ||
| 2571 | } | ||
| 2524 | if (!extension_settings.sd.comfy_url) { | 2572 | if (!extension_settings.sd.comfy_url) { |
| 2525 | return []; | 2573 | return []; |
| 2526 | } | 2574 | } |
| @@ -2543,10 +2591,6 @@ async function loadComfyVaes() { | |||
| 2543 | } | 2591 | } |
| 2544 | 2592 | ||
| 2545 | async function loadComfyWorkflows() { | 2593 | async function loadComfyWorkflows() { |
| 2546 | if (!extension_settings.sd.comfy_url) { | ||
| 2547 | return; | ||
| 2548 | } | ||
| 2549 | |||
| 2550 | try { | 2594 | try { |
| 2551 | $('#sd_comfy_workflow').empty(); | 2595 | $('#sd_comfy_workflow').empty(); |
| 2552 | const result = await fetch('/api/sd/comfy/workflows', { | 2596 | const result = await fetch('/api/sd/comfy/workflows', { |
| @@ -3040,7 +3084,16 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP | |||
| 3040 | result = await generateAimlapiImage(prefixedPrompt, signal); | 3084 | result = await generateAimlapiImage(prefixedPrompt, signal); |
| 3041 | break; | 3085 | break; |
| 3042 | case sources.comfy: | 3086 | case sources.comfy: |
| 3043 | result = await generateComfyImage(prefixedPrompt, negativePrompt, signal); | 3087 | switch (extension_settings.sd.comfy_type) { |
| 3088 | case comfyTypes.runpod_serverless: | ||
| 3089 | result = await generateComfyRunPodImage(prefixedPrompt, negativePrompt, signal); | ||
| 3090 | break; | ||
| 3091 | case comfyTypes.standard: | ||
| 3092 | result = await generateComfyImage(prefixedPrompt, negativePrompt, signal); | ||
| 3093 | break; | ||
| 3094 | default: | ||
| 3095 | throw new Error('Unknown comfyUI server type.'); | ||
| 3096 | } | ||
| 3044 | break; | 3097 | break; |
| 3045 | case sources.togetherai: | 3098 | case sources.togetherai: |
| 3046 | result = await generateTogetherAIImage(prefixedPrompt, negativePrompt, signal); | 3099 | result = await generateTogetherAIImage(prefixedPrompt, negativePrompt, signal); |
| @@ -3775,25 +3828,17 @@ async function generateAimlapiImage(prompt, signal) { | |||
| 3775 | } | 3828 | } |
| 3776 | 3829 | ||
| 3777 | /** | 3830 | /** |
| 3778 | * Generates an image in ComfyUI using the provided prompt and configuration settings. | 3831 | * Generates an image in local ComfyUI or serverless runpod using the provided prompt and configuration settings. |
| 3779 | * | 3832 | * |
| 3780 | * @param {string} prompt - The main instruction used to guide the image generation. | 3833 | * @param {string} prompt - The main instruction used to guide the image generation. |
| 3781 | * @param {string} negativePrompt - The instruction used to restrict the image generation. | 3834 | * @param {string} negativePrompt - The instruction used to restrict the image generation. |
| 3782 | * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. | 3835 | * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. |
| 3836 | * @param {string} basePath - ST server endpoint for the service. '/api/sd/comfy' for local, '/api/sd/comfyrunpod' for serverless. | ||
| 3837 | * @param {string[]} placeholders - Array of substitutions to apply to the workflow. | ||
| 3838 | * @param {string} url - The url of the service to call. Passed to ST server. | ||
| 3783 | * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. | 3839 | * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. |
| 3784 | */ | 3840 | */ |
| 3785 | async function generateComfyImage(prompt, negativePrompt, signal) { | 3841 | async function generateComfyImageCommon(prompt, negativePrompt, signal, basePath, placeholders, url) { |
| 3786 | const placeholders = [ | ||
| 3787 | 'model', | ||
| 3788 | 'vae', | ||
| 3789 | 'sampler', | ||
| 3790 | 'scheduler', | ||
| 3791 | 'steps', | ||
| 3792 | 'scale', | ||
| 3793 | 'width', | ||
| 3794 | 'height', | ||
| 3795 | ]; | ||
| 3796 | |||
| 3797 | const workflowResponse = await fetch('/api/sd/comfy/workflow', { | 3842 | const workflowResponse = await fetch('/api/sd/comfy/workflow', { |
| 3798 | method: 'POST', | 3843 | method: 'POST', |
| 3799 | headers: getRequestHeaders(), | 3844 | headers: getRequestHeaders(), |
| @@ -3848,12 +3893,12 @@ async function generateComfyImage(prompt, negativePrompt, signal) { | |||
| 3848 | console.log(`{ | 3893 | console.log(`{ |
| 3849 | "prompt": ${workflow} | 3894 | "prompt": ${workflow} |
| 3850 | }`); | 3895 | }`); |
| 3851 | const promptResult = await fetch('/api/sd/comfy/generate', { | 3896 | const promptResult = await fetch(`${basePath}/generate`, { |
| 3852 | method: 'POST', | 3897 | method: 'POST', |
| 3853 | headers: getRequestHeaders(), | 3898 | headers: getRequestHeaders(), |
| 3854 | signal: signal, | 3899 | signal: signal, |
| 3855 | body: JSON.stringify({ | 3900 | body: JSON.stringify({ |
| 3856 | url: extension_settings.sd.comfy_url, | 3901 | url, |
| 3857 | prompt: `{ | 3902 | prompt: `{ |
| 3858 | "prompt": ${workflow} | 3903 | "prompt": ${workflow} |
| 3859 | }`, | 3904 | }`, |
| @@ -3867,6 +3912,46 @@ async function generateComfyImage(prompt, negativePrompt, signal) { | |||
| 3867 | return { format, data }; | 3912 | return { format, data }; |
| 3868 | } | 3913 | } |
| 3869 | 3914 | ||
| 3915 | /** | ||
| 3916 | * Generates an image in ComfyUI using the provided prompt and configuration settings. | ||
| 3917 | * | ||
| 3918 | * @param {string} prompt - The main instruction used to guide the image generation. | ||
| 3919 | * @param {string} negativePrompt - The instruction used to restrict the image generation. | ||
| 3920 | * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. | ||
| 3921 | * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. | ||
| 3922 | */ | ||
| 3923 | async function generateComfyImage(prompt, negativePrompt, signal) { | ||
| 3924 | const placeholders = [ | ||
| 3925 | 'model', | ||
| 3926 | 'vae', | ||
| 3927 | 'sampler', | ||
| 3928 | 'scheduler', | ||
| 3929 | 'steps', | ||
| 3930 | 'scale', | ||
| 3931 | 'width', | ||
| 3932 | 'height', | ||
| 3933 | ]; | ||
| 3934 | return generateComfyImageCommon(prompt, negativePrompt, signal, '/api/sd/comfy', placeholders, extension_settings.sd.comfy_url); | ||
| 3935 | } | ||
| 3936 | |||
| 3937 | /** | ||
| 3938 | * Generates an image using ComfyUI through serverless runpod endpoint using the provided prompt and configuration settings. | ||
| 3939 | * | ||
| 3940 | * @param {string} prompt - The main instruction used to guide the image generation. | ||
| 3941 | * @param {string} negativePrompt - The instruction used to restrict the image generation. | ||
| 3942 | * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. | ||
| 3943 | * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. | ||
| 3944 | */ | ||
| 3945 | async function generateComfyRunPodImage(prompt, negativePrompt, signal) { | ||
| 3946 | const placeholders = [ | ||
| 3947 | 'steps', | ||
| 3948 | 'scale', | ||
| 3949 | 'width', | ||
| 3950 | 'height', | ||
| 3951 | ]; | ||
| 3952 | |||
| 3953 | return generateComfyImageCommon(prompt, negativePrompt, signal, '/api/sd/comfyrunpod', placeholders, extension_settings.sd.comfy_runpod_url); | ||
| 3954 | } | ||
| 3870 | 3955 | ||
| 3871 | /** | 3956 | /** |
| 3872 | * Generates an image in Hugging Face Inference API using the provided prompt and configuration settings (model selected). | 3957 | * Generates an image in Hugging Face Inference API using the provided prompt and configuration settings (model selected). |
| @@ -4501,7 +4586,15 @@ function isValidState() { | |||
| 4501 | case sources.aimlapi: | 4586 | case sources.aimlapi: |
| 4502 | return secret_state[SECRET_KEYS.AIMLAPI]; | 4587 | return secret_state[SECRET_KEYS.AIMLAPI]; |
| 4503 | case sources.comfy: | 4588 | case sources.comfy: |
| 4504 | return true; | 4589 | switch (extension_settings.sd.comfy_type) { |
| 4590 | case comfyTypes.runpod_serverless: | ||
| 4591 | return !!extension_settings.sd.comfy_runpod_url && | ||
| 4592 | secret_state[SECRET_KEYS.COMFY_RUNPOD]; | ||
| 4593 | case comfyTypes.standard: | ||
| 4594 | return !!extension_settings.sd.comfy_url; | ||
| 4595 | default: | ||
| 4596 | return false; | ||
| 4597 | } | ||
| 4505 | case sources.togetherai: | 4598 | case sources.togetherai: |
| 4506 | return secret_state[SECRET_KEYS.TOGETHERAI]; | 4599 | return secret_state[SECRET_KEYS.TOGETHERAI]; |
| 4507 | case sources.pollinations: | 4600 | case sources.pollinations: |
| @@ -5167,8 +5260,11 @@ jQuery(async () => { | |||
| 5167 | $('#sd_novel_decrisper').on('input', onNovelDecrisperInput); | 5260 | $('#sd_novel_decrisper').on('input', onNovelDecrisperInput); |
| 5168 | $('#sd_novel_variety_boost').on('input', onNovelVarietyBoostInput); | 5261 | $('#sd_novel_variety_boost').on('input', onNovelVarietyBoostInput); |
| 5169 | $('#sd_pollinations_enhance').on('input', onPollinationsEnhanceInput); | 5262 | $('#sd_pollinations_enhance').on('input', onPollinationsEnhanceInput); |
| 5263 | $('#sd_comfy_type').on('change', onComfyTypeChange); | ||
| 5170 | $('#sd_comfy_validate').on('click', validateComfyUrl); | 5264 | $('#sd_comfy_validate').on('click', validateComfyUrl); |
| 5265 | $('#sd_comfy_runpod_validate').on('click', validateComfyRunPodUrl); | ||
| 5171 | $('#sd_comfy_url').on('input', onComfyUrlInput); | 5266 | $('#sd_comfy_url').on('input', onComfyUrlInput); |
| 5267 | $('#sd_comfy_runpod_url').on('input', onComfyRunPodUrlInput); | ||
| 5172 | $('#sd_comfy_workflow').on('change', onComfyWorkflowChange); | 5268 | $('#sd_comfy_workflow').on('change', onComfyWorkflowChange); |
| 5173 | $('#sd_comfy_open_workflow_editor').on('click', onComfyOpenWorkflowEditorClick); | 5269 | $('#sd_comfy_open_workflow_editor').on('click', onComfyOpenWorkflowEditorClick); |
| 5174 | $('#sd_comfy_new_workflow').on('click', onComfyNewWorkflowClick); | 5270 | $('#sd_comfy_new_workflow').on('click', onComfyNewWorkflowClick); |
| @@ -5246,6 +5342,7 @@ jQuery(async () => { | |||
| 5246 | case SECRET_KEYS.FALAI: | 5342 | case SECRET_KEYS.FALAI: |
| 5247 | case SECRET_KEYS.STABILITY: | 5343 | case SECRET_KEYS.STABILITY: |
| 5248 | case SECRET_KEYS.AIMLAPI: | 5344 | case SECRET_KEYS.AIMLAPI: |
| 5345 | case SECRET_KEYS.COMFY_RUNPOD: | ||
| 5249 | await loadSettingOptions(); | 5346 | await loadSettingOptions(); |
| 5250 | break; | 5347 | break; |
| 5251 | } | 5348 | } |
| @@ -198,14 +198,44 @@ | |||
| 198 | </div> | 198 | </div> |
| 199 | </div> | 199 | </div> |
| 200 | <div data-sd-source="comfy"> | 200 | <div data-sd-source="comfy"> |
| 201 | <label for="sd_comfy_url">ComfyUI URL</label> | 201 | <label for="sd_comfy_type">Server Type</label> |
| 202 | <div class="flex-container flexnowrap"> | 202 | <select id="sd_comfy_type"> |
| 203 | <input id="sd_comfy_url" type="text" class="text_pole" data-i18n="[placeholder]sd_comfy_url" placeholder="Example: {{comfy_url}}" value="{{comfy_url}}" /> | 203 | <option value="standard">Standard Server</option> |
| 204 | <div id="sd_comfy_validate" class="menu_button menu_button_icon"> | 204 | <option value="runpod_serverless">RunPod Serverless Endpoint</option> |
| 205 | <i class="fa-solid fa-check"></i> | 205 | </select> |
| 206 | <span data-i18n="Connect"> | 206 | <div data-sd-comfy-type="standard"> |
| 207 | Connect | 207 | <label for="sd_comfy_url">ComfyUI URL</label> |
| 208 | </span> | 208 | <div class="flex-container flexnowrap"> |
| 209 | <input id="sd_comfy_url" type="text" class="text_pole" data-i18n="[placeholder]sd_comfy_url" placeholder="Example: {{comfy_url}}" value="{{comfy_url}}" /> | ||
| 210 | <div id="sd_comfy_validate" class="menu_button menu_button_icon"> | ||
| 211 | <i class="fa-solid fa-check"></i> | ||
| 212 | <span data-i18n="Connect"> | ||
| 213 | Connect | ||
| 214 | </span> | ||
| 215 | </div> | ||
| 216 | </div> | ||
| 217 | </div> | ||
| 218 | <div data-sd-comfy-type="runpod_serverless"> | ||
| 219 | <label for="sd_comfy_runpod_url">ComfyUI RunPod URL</label> | ||
| 220 | <div class="flex-container flexnowrap"> | ||
| 221 | <input id="sd_comfy_runpod_url" type="text" class="text_pole" data-i18n="[placeholder]sd_comfy_runpod_url" placeholder="eg: https://api.runpod.ai/v2/<your endpoint id>" value="{{comfy_runpod_url}}" /> | ||
| 222 | <div id="sd_comfy_runpod_validate" class="menu_button menu_button_icon"> | ||
| 223 | <i class="fa-solid fa-check"></i> | ||
| 224 | <span data-i18n="Connect"> | ||
| 225 | Connect | ||
| 226 | </span> | ||
| 227 | </div> | ||
| 228 | </div> | ||
| 229 | <div class="flex-container flexnowrap alignItemsBaseline marginBot5"> | ||
| 230 | <a href="https://console.runpod.io/user/settings" target="_blank" rel="noopener noreferrer"> | ||
| 231 | <strong data-i18n="API Key">API Key</strong> | ||
| 232 | <i class="fa-solid fa-share-from-square"></i> | ||
| 233 | </a> | ||
| 234 | <span class="expander"></span> | ||
| 235 | <div id="sd_runpod_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_comfy_runpod"> | ||
| 236 | <i class="fa-fw fa-solid fa-key"></i> | ||
| 237 | <span data-i18n="Click to set">Click to set</span> | ||
| 238 | </div> | ||
| 209 | </div> | 239 | </div> |
| 210 | </div> | 240 | </div> |
| 211 | <p><i><b data-i18n="Important:">Important:</b></i><i data-i18n="The server must be accessible from the SillyTavern host machine."> The server must be accessible from the SillyTavern host machine.</i></p> | 241 | <p><i><b data-i18n="Important:">Important:</b></i><i data-i18n="The server must be accessible from the SillyTavern host machine."> The server must be accessible from the SillyTavern host machine.</i></p> |
| @@ -57,6 +57,7 @@ export const SECRET_KEYS = { | |||
| 57 | NANOGPT: 'api_key_nanogpt', | 57 | NANOGPT: 'api_key_nanogpt', |
| 58 | TAVILY: 'api_key_tavily', | 58 | TAVILY: 'api_key_tavily', |
| 59 | BFL: 'api_key_bfl', | 59 | BFL: 'api_key_bfl', |
| 60 | COMFY_RUNPOD: 'api_key_comfy_runpod', | ||
| 60 | GENERIC: 'api_key_generic', | 61 | GENERIC: 'api_key_generic', |
| 61 | DEEPSEEK: 'api_key_deepseek', | 62 | DEEPSEEK: 'api_key_deepseek', |
| 62 | SERPER: 'api_key_serper', | 63 | SERPER: 'api_key_serper', |
| @@ -112,6 +113,7 @@ const FRIENDLY_NAMES = { | |||
| 112 | [SECRET_KEYS.CUSTOM_OPENAI_TTS]: 'Custom OpenAI TTS', | 113 | [SECRET_KEYS.CUSTOM_OPENAI_TTS]: 'Custom OpenAI TTS', |
| 113 | [SECRET_KEYS.TAVILY]: 'Tavily', | 114 | [SECRET_KEYS.TAVILY]: 'Tavily', |
| 114 | [SECRET_KEYS.BFL]: 'Black Forest Labs', | 115 | [SECRET_KEYS.BFL]: 'Black Forest Labs', |
| 116 | [SECRET_KEYS.COMFY_RUNPOD]: 'ComfyUI RunPod', | ||
| 115 | [SECRET_KEYS.SERPAPI]: 'SerpApi', | 117 | [SECRET_KEYS.SERPAPI]: 'SerpApi', |
| 116 | [SECRET_KEYS.SERPER]: 'Serper', | 118 | [SECRET_KEYS.SERPER]: 'Serper', |
| 117 | [SECRET_KEYS.FALAI]: 'FAL.AI', | 119 | [SECRET_KEYS.FALAI]: 'FAL.AI', |
| @@ -174,6 +176,7 @@ const INPUT_MAP = { | |||
| 174 | [SECRET_KEYS.AZURE_OPENAI]: '#api_key_azure_openai', | 176 | [SECRET_KEYS.AZURE_OPENAI]: '#api_key_azure_openai', |
| 175 | [SECRET_KEYS.ZAI]: '#api_key_zai', | 177 | [SECRET_KEYS.ZAI]: '#api_key_zai', |
| 176 | [SECRET_KEYS.SILICONFLOW]: '#api_key_siliconflow', | 178 | [SECRET_KEYS.SILICONFLOW]: '#api_key_siliconflow', |
| 179 | [SECRET_KEYS.COMFY_RUNPOD]: '#api_key_comfy_runpod', | ||
| 177 | }; | 180 | }; |
| 178 | 181 | ||
| 179 | const getLabel = () => moment().format('L LT'); | 182 | const getLabel = () => moment().format('L LT'); |
| @@ -49,6 +49,7 @@ export const SECRET_KEYS = { | |||
| 49 | ELECTRONHUB: 'api_key_electronhub', | 49 | ELECTRONHUB: 'api_key_electronhub', |
| 50 | NANOGPT: 'api_key_nanogpt', | 50 | NANOGPT: 'api_key_nanogpt', |
| 51 | BFL: 'api_key_bfl', | 51 | BFL: 'api_key_bfl', |
| 52 | COMFY_RUNPOD: 'api_key_comfy_runpod', | ||
| 52 | FALAI: 'api_key_falai', | 53 | FALAI: 'api_key_falai', |
| 53 | GENERIC: 'api_key_generic', | 54 | GENERIC: 'api_key_generic', |
| 54 | DEEPSEEK: 'api_key_deepseek', | 55 | DEEPSEEK: 'api_key_deepseek', |
| @@ -603,6 +603,108 @@ comfy.post('/generate', async (request, response) => { | |||
| 603 | } | 603 | } |
| 604 | }); | 604 | }); |
| 605 | 605 | ||
| 606 | const comfyRunPod = express.Router(); | ||
| 607 | |||
| 608 | comfyRunPod.post('/ping', async (request, response) => { | ||
| 609 | try { | ||
| 610 | const key = readSecret(request.user.directories, SECRET_KEYS.COMFY_RUNPOD); | ||
| 611 | |||
| 612 | if (!key) { | ||
| 613 | console.warn('RunPod key not found.'); | ||
| 614 | return response.sendStatus(400); | ||
| 615 | } | ||
| 616 | |||
| 617 | const url = new URL(urlJoin(request.body.url, '/health')); | ||
| 618 | |||
| 619 | const result = await fetch(url, { | ||
| 620 | method: 'GET', | ||
| 621 | headers: { 'Authorization': `Bearer ${key}` }, | ||
| 622 | }); | ||
| 623 | if (!result.ok) { | ||
| 624 | throw new Error('ComfyUI returned an error.'); | ||
| 625 | } | ||
| 626 | /** @type {any} */ | ||
| 627 | const data = await result.json(); | ||
| 628 | if (data.workers.ready <= 0) { | ||
| 629 | console.warn(`No workers reported as ready. ${result}`); | ||
| 630 | } | ||
| 631 | |||
| 632 | return response.sendStatus(200); | ||
| 633 | } catch (error) { | ||
| 634 | console.error(error); | ||
| 635 | return response.sendStatus(500); | ||
| 636 | } | ||
| 637 | }); | ||
| 638 | |||
| 639 | comfyRunPod.post('/generate', async (request, response) => { | ||
| 640 | try { | ||
| 641 | const key = readSecret(request.user.directories, SECRET_KEYS.COMFY_RUNPOD); | ||
| 642 | |||
| 643 | if (!key) { | ||
| 644 | console.warn('RunPod key not found.'); | ||
| 645 | return response.sendStatus(400); | ||
| 646 | } | ||
| 647 | |||
| 648 | let jobId; | ||
| 649 | let item; | ||
| 650 | const url = new URL(urlJoin(request.body.url, '/run')); | ||
| 651 | |||
| 652 | const controller = new AbortController(); | ||
| 653 | request.socket.removeAllListeners('close'); | ||
| 654 | request.socket.on('close', function () { | ||
| 655 | if (!response.writableEnded && !item) { | ||
| 656 | const interruptUrl = new URL(urlJoin(request.body.url, `/cancel/${jobId}`)); | ||
| 657 | fetch(interruptUrl, { method: 'POST', headers: { 'Authorization': `Bearer ${key}` } }); | ||
| 658 | } | ||
| 659 | controller.abort(); | ||
| 660 | }); | ||
| 661 | const workflow = JSON.parse(request.body.prompt).prompt; | ||
| 662 | const wrappedWorkflow = workflow?.input?.workflow ? workflow : ({ input: { workflow: workflow } }); | ||
| 663 | const runpodPrompt = JSON.stringify(wrappedWorkflow); | ||
| 664 | |||
| 665 | console.debug('ComfyUI RunPod request:', wrappedWorkflow); | ||
| 666 | |||
| 667 | const promptResult = await fetch(url, { | ||
| 668 | method: 'POST', | ||
| 669 | headers: { 'Authorization': `Bearer ${key}` }, | ||
| 670 | body: runpodPrompt, | ||
| 671 | }); | ||
| 672 | if (!promptResult.ok) { | ||
| 673 | const text = await promptResult.text(); | ||
| 674 | throw new Error('ComfyUI returned an error.', { cause: tryParse(text) }); | ||
| 675 | } | ||
| 676 | |||
| 677 | /** @type {any} */ | ||
| 678 | const data = await promptResult.json(); | ||
| 679 | jobId = data.id; | ||
| 680 | const statusUrl = new URL(urlJoin(request.body.url, `/status/${jobId}`)); | ||
| 681 | while (true) { | ||
| 682 | const result = await fetch(statusUrl, { | ||
| 683 | method: 'GET', | ||
| 684 | headers: { 'Authorization': `Bearer ${key}` }, | ||
| 685 | }); | ||
| 686 | if (!result.ok) { | ||
| 687 | throw new Error('ComfyUI returned an error.'); | ||
| 688 | } | ||
| 689 | /** @type {any} */ | ||
| 690 | const status = await result.json(); | ||
| 691 | if (status.output) { | ||
| 692 | item = status.output.images[0]; | ||
| 693 | } | ||
| 694 | if (item) { | ||
| 695 | break; | ||
| 696 | } | ||
| 697 | await delay(500); | ||
| 698 | } | ||
| 699 | const format = path.extname(item.filename).slice(1).toLowerCase() || 'png'; | ||
| 700 | return response.send({ format: format, data: item.data }); | ||
| 701 | } catch (error) { | ||
| 702 | console.error('ComfyUI error:', error); | ||
| 703 | response.status(500).send(error.message); | ||
| 704 | return response; | ||
| 705 | } | ||
| 706 | }); | ||
| 707 | |||
| 606 | const together = express.Router(); | 708 | const together = express.Router(); |
| 607 | 709 | ||
| 608 | together.post('/models', async (request, response) => { | 710 | together.post('/models', async (request, response) => { |
| @@ -1704,6 +1806,7 @@ zai.post('/generate', async (request, response) => { | |||
| 1704 | }); | 1806 | }); |
| 1705 | 1807 | ||
| 1706 | router.use('/comfy', comfy); | 1808 | router.use('/comfy', comfy); |
| 1809 | router.use('/comfyrunpod', comfyRunPod); | ||
| 1707 | router.use('/together', together); | 1810 | router.use('/together', together); |
| 1708 | router.use('/drawthings', drawthings); | 1811 | router.use('/drawthings', drawthings); |
| 1709 | router.use('/pollinations', pollinations); | 1812 | router.use('/pollinations', pollinations); |