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 | 93 | zai: 'zai', |
| 94 | 94 | openrouter: 'openrouter', |
| 95 | 95 | }; |
| 96 | +const comfyTypes = { | |
| 97 | + standard: 'standard', | |
| 98 | + runpod_serverless: 'runpod_serverless', | |
| 99 | +}; | |
| 96 | 100 | |
| 97 | 101 | const initiators = { |
| 98 | 102 | command: 'command', |
| @@ -322,9 +326,13 @@ const defaultSettings = { | ||
| 322 | 326 | styles: defaultStyles, |
| 323 | 327 | |
| 324 | 328 | // ComyUI settings |
| 329 | + comfy_type: 'standard', | |
| 330 | + | |
| 325 | 331 | comfy_url: 'http://127.0.0.1:8188', |
| 326 | 332 | comfy_workflow: 'Default_Comfy_Workflow.json', |
| 327 | 333 | |
| 334 | + comfy_runpod_url: '', | |
| 335 | + | |
| 328 | 336 | // Pollinations settings |
| 329 | 337 | pollinations_enhance: false, |
| 330 | 338 | |
| @@ -437,6 +445,10 @@ function toggleSourceControls() { | ||
| 437 | 445 | const source = $(this).data('sd-source').split(','); |
| 438 | 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 | 454 | async function loadSettings() { |
| @@ -517,8 +529,10 @@ async function loadSettings() { | ||
| 517 | 529 | $('#sd_openai_style').val(extension_settings.sd.openai_style); |
| 518 | 530 | $('#sd_openai_quality').val(extension_settings.sd.openai_quality); |
| 519 | 531 | $('#sd_openai_duration').val(extension_settings.sd.openai_duration); |
| 532 | + $('#sd_comfy_type').val(extension_settings.sd.comfy_type); | |
| 520 | 533 | $('#sd_comfy_url').val(extension_settings.sd.comfy_url); |
| 521 | 534 | $('#sd_comfy_prompt').val(extension_settings.sd.comfy_prompt); |
| 535 | + $('#sd_comfy_runpod_url').val(extension_settings.sd.comfy_runpod_url); | |
| 522 | 536 | $('#sd_snap').prop('checked', extension_settings.sd.snap); |
| 523 | 537 | $('#sd_clip_skip').val(extension_settings.sd.clip_skip); |
| 524 | 538 | $('#sd_clip_skip_value').val(extension_settings.sd.clip_skip); |
| @@ -1016,6 +1030,11 @@ async function onSourceChange() { | ||
| 1016 | 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 | 1038 | function onFunctionToolInput() { |
| 1020 | 1039 | extension_settings.sd.function_tool = !!$(this).prop('checked'); |
| 1021 | 1040 | saveSettingsDebounced(); |
| @@ -1166,11 +1185,15 @@ function onHrSecondPassStepsInput() { | ||
| 1166 | 1185 | } |
| 1167 | 1186 | |
| 1168 | 1187 | function onComfyUrlInput() { |
| 1169 | - // Remove trailing slashes | |
| 1170 | 1188 | extension_settings.sd.comfy_url = String($('#sd_comfy_url').val()); |
| 1171 | 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 | 1197 | function onHFModelInput() { |
| 1175 | 1198 | extension_settings.sd.huggingface_model_id = $('#sd_huggingface_model_id').val(); |
| 1176 | 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 | 1346 | async function onModelChange() { |
| 1300 | 1347 | const selectedModel = $('#sd_model').find(':selected'); |
| 1301 | 1348 | extension_settings.sd.model = selectedModel.val(); |
| @@ -1309,27 +1356,13 @@ async function onModelChange() { | ||
| 1309 | 1356 | |
| 1310 | 1357 | switchModelSpecificControls(extension_settings.sd.model); |
| 1311 | 1358 | |
| 1312 | 1359 | const cloudSourcesupdateRemoteModelSources = [ |
| 1313 | 1360 | sources.hordeauto, |
| 1314 | 1361 | sources.novelvlad, |
| 1315 | 1362 | sources.openaiextras, |
| 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 | 1365 | if (cloudSources!updateRemoteModelSources.includes(extension_settings.sd.source)) { |
| 1333 | 1366 | return; |
| 1334 | 1367 | } |
| 1335 | 1368 | |
| @@ -1685,6 +1718,9 @@ async function loadNovelSamplers() { | ||
| 1685 | 1718 | } |
| 1686 | 1719 | |
| 1687 | 1720 | async function loadComfySamplers() { |
| 1721 | + if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) { | |
| 1722 | + return ['N/A']; | |
| 1723 | + } | |
| 1688 | 1724 | if (!extension_settings.sd.comfy_url) { |
| 1689 | 1725 | return []; |
| 1690 | 1726 | } |
| @@ -2279,6 +2315,12 @@ function loadNovelSchedulers() { | ||
| 2279 | 2315 | } |
| 2280 | 2316 | |
| 2281 | 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 | 2324 | if (!extension_settings.sd.comfy_url) { |
| 2283 | 2325 | return []; |
| 2284 | 2326 | } |
| @@ -2388,6 +2430,9 @@ async function loadSchedulers() { | ||
| 2388 | 2430 | } |
| 2389 | 2431 | |
| 2390 | 2432 | async function loadComfySchedulers() { |
| 2433 | + if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) { | |
| 2434 | + return ['N/A']; | |
| 2435 | + } | |
| 2391 | 2436 | if (!extension_settings.sd.comfy_url) { |
| 2392 | 2437 | return []; |
| 2393 | 2438 | } |
| @@ -2521,6 +2566,9 @@ async function loadAutoVaes() { | ||
| 2521 | 2566 | } |
| 2522 | 2567 | |
| 2523 | 2568 | async function loadComfyVaes() { |
| 2569 | + if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) { | |
| 2570 | + return ['N/A']; | |
| 2571 | + } | |
| 2524 | 2572 | if (!extension_settings.sd.comfy_url) { |
| 2525 | 2573 | return []; |
| 2526 | 2574 | } |
| @@ -2543,10 +2591,6 @@ async function loadComfyVaes() { | ||
| 2543 | 2591 | } |
| 2544 | 2592 | |
| 2545 | 2593 | async function loadComfyWorkflows() { |
| 2546 | - if (!extension_settings.sd.comfy_url) { | |
| 2547 | - return; | |
| 2548 | - } | |
| 2549 | - | |
| 2550 | 2594 | try { |
| 2551 | 2595 | $('#sd_comfy_workflow').empty(); |
| 2552 | 2596 | const result = await fetch('/api/sd/comfy/workflows', { |
| @@ -3040,7 +3084,16 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP | ||
| 3040 | 3084 | result = await generateAimlapiImage(prefixedPrompt, signal); |
| 3041 | 3085 | break; |
| 3042 | 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 | 3097 | break; |
| 3045 | 3098 | case sources.togetherai: |
| 3046 | 3099 | result = await generateTogetherAIImage(prefixedPrompt, negativePrompt, signal); |
| @@ -3775,25 +3828,17 @@ async function generateAimlapiImage(prompt, signal) { | ||
| 3775 | 3828 | } |
| 3776 | 3829 | |
| 3777 | 3830 | /** |
| 3778 | 3831 | * Generates an image in local ComfyUI or serverless runpod using the provided prompt and configuration settings. |
| 3779 | 3832 | * |
| 3780 | 3833 | * @param {string} prompt - The main instruction used to guide the image generation. |
| 3781 | 3834 | * @param {string} negativePrompt - The instruction used to restrict the image generation. |
| 3782 | 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 | 3839 | * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. |
| 3784 | 3840 | */ |
| 3785 | 3841 | async function generateComfyImagegenerateComfyImageCommon(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 | 3842 | const workflowResponse = await fetch('/api/sd/comfy/workflow', { |
| 3798 | 3843 | method: 'POST', |
| 3799 | 3844 | headers: getRequestHeaders(), |
| @@ -3848,12 +3893,12 @@ async function generateComfyImage(prompt, negativePrompt, signal) { | ||
| 3848 | 3893 | console.log(`{ |
| 3849 | 3894 | "prompt": ${workflow} |
| 3850 | 3895 | }`); |
| 3851 | 3896 | const promptResult = await fetch('/api/sd/comfy`${basePath}/generate'`, { |
| 3852 | 3897 | method: 'POST', |
| 3853 | 3898 | headers: getRequestHeaders(), |
| 3854 | 3899 | signal: signal, |
| 3855 | 3900 | body: JSON.stringify({ |
| 3856 | - url: extension_settings.sd.comfy_url, | |
| 3901 | + url, | |
| 3857 | 3902 | prompt: `{ |
| 3858 | 3903 | "prompt": ${workflow} |
| 3859 | 3904 | }`, |
| @@ -3867,6 +3912,46 @@ async function generateComfyImage(prompt, negativePrompt, signal) { | ||
| 3867 | 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 | 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 | 4586 | case sources.aimlapi: |
| 4502 | 4587 | return secret_state[SECRET_KEYS.AIMLAPI]; |
| 4503 | 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 | 4598 | case sources.togetherai: |
| 4506 | 4599 | return secret_state[SECRET_KEYS.TOGETHERAI]; |
| 4507 | 4600 | case sources.pollinations: |
| @@ -5167,8 +5260,11 @@ jQuery(async () => { | ||
| 5167 | 5260 | $('#sd_novel_decrisper').on('input', onNovelDecrisperInput); |
| 5168 | 5261 | $('#sd_novel_variety_boost').on('input', onNovelVarietyBoostInput); |
| 5169 | 5262 | $('#sd_pollinations_enhance').on('input', onPollinationsEnhanceInput); |
| 5263 | + $('#sd_comfy_type').on('change', onComfyTypeChange); | |
| 5170 | 5264 | $('#sd_comfy_validate').on('click', validateComfyUrl); |
| 5265 | + $('#sd_comfy_runpod_validate').on('click', validateComfyRunPodUrl); | |
| 5171 | 5266 | $('#sd_comfy_url').on('input', onComfyUrlInput); |
| 5267 | + $('#sd_comfy_runpod_url').on('input', onComfyRunPodUrlInput); | |
| 5172 | 5268 | $('#sd_comfy_workflow').on('change', onComfyWorkflowChange); |
| 5173 | 5269 | $('#sd_comfy_open_workflow_editor').on('click', onComfyOpenWorkflowEditorClick); |
| 5174 | 5270 | $('#sd_comfy_new_workflow').on('click', onComfyNewWorkflowClick); |
| @@ -5246,6 +5342,7 @@ jQuery(async () => { | ||
| 5246 | 5342 | case SECRET_KEYS.FALAI: |
| 5247 | 5343 | case SECRET_KEYS.STABILITY: |
| 5248 | 5344 | case SECRET_KEYS.AIMLAPI: |
| 5345 | + case SECRET_KEYS.COMFY_RUNPOD: | |
| 5249 | 5346 | await loadSettingOptions(); |
| 5250 | 5347 | break; |
| 5251 | 5348 | } |
| @@ -198,14 +198,44 @@ | ||
| 198 | 198 | </div> |
| 199 | 199 | </div> |
| 200 | 200 | <div data-sd-source="comfy"> |
| 201 | 201 | <label for="sd_comfy_urlsd_comfy_type">ComfyUIServer URLType</label> |
| 202 | 202 | <divselect classid="flex-container flexnowrapsd_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 | 206 | <spandiv data-i18nsd-comfy-type="Connectstandard"> |
| 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 | 239 | </div> |
| 210 | 240 | </div> |
| 211 | 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 | 57 | NANOGPT: 'api_key_nanogpt', |
| 58 | 58 | TAVILY: 'api_key_tavily', |
| 59 | 59 | BFL: 'api_key_bfl', |
| 60 | + COMFY_RUNPOD: 'api_key_comfy_runpod', | |
| 60 | 61 | GENERIC: 'api_key_generic', |
| 61 | 62 | DEEPSEEK: 'api_key_deepseek', |
| 62 | 63 | SERPER: 'api_key_serper', |
| @@ -112,6 +113,7 @@ const FRIENDLY_NAMES = { | ||
| 112 | 113 | [SECRET_KEYS.CUSTOM_OPENAI_TTS]: 'Custom OpenAI TTS', |
| 113 | 114 | [SECRET_KEYS.TAVILY]: 'Tavily', |
| 114 | 115 | [SECRET_KEYS.BFL]: 'Black Forest Labs', |
| 116 | + [SECRET_KEYS.COMFY_RUNPOD]: 'ComfyUI RunPod', | |
| 115 | 117 | [SECRET_KEYS.SERPAPI]: 'SerpApi', |
| 116 | 118 | [SECRET_KEYS.SERPER]: 'Serper', |
| 117 | 119 | [SECRET_KEYS.FALAI]: 'FAL.AI', |
| @@ -174,6 +176,7 @@ const INPUT_MAP = { | ||
| 174 | 176 | [SECRET_KEYS.AZURE_OPENAI]: '#api_key_azure_openai', |
| 175 | 177 | [SECRET_KEYS.ZAI]: '#api_key_zai', |
| 176 | 178 | [SECRET_KEYS.SILICONFLOW]: '#api_key_siliconflow', |
| 179 | + [SECRET_KEYS.COMFY_RUNPOD]: '#api_key_comfy_runpod', | |
| 177 | 180 | }; |
| 178 | 181 | |
| 179 | 182 | const getLabel = () => moment().format('L LT'); |
| @@ -49,6 +49,7 @@ export const SECRET_KEYS = { | ||
| 49 | 49 | ELECTRONHUB: 'api_key_electronhub', |
| 50 | 50 | NANOGPT: 'api_key_nanogpt', |
| 51 | 51 | BFL: 'api_key_bfl', |
| 52 | + COMFY_RUNPOD: 'api_key_comfy_runpod', | |
| 52 | 53 | FALAI: 'api_key_falai', |
| 53 | 54 | GENERIC: 'api_key_generic', |
| 54 | 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 | 708 | const together = express.Router(); |
| 607 | 709 | |
| 608 | 710 | together.post('/models', async (request, response) => { |
| @@ -1704,6 +1806,7 @@ zai.post('/generate', async (request, response) => { | ||
| 1704 | 1806 | }); |
| 1705 | 1807 | |
| 1706 | 1808 | router.use('/comfy', comfy); |
| 1809 | +router.use('/comfyrunpod', comfyRunPod); | |
| 1707 | 1810 | router.use('/together', together); |
| 1708 | 1811 | router.use('/drawthings', drawthings); |
| 1709 | 1812 | router.use('/pollinations', pollinations); |