Integrate Cloudflare Workers AI text-to-image into SD extension (#5434) * feat: integrate Cloudflare Workers AI for text-to-image generation in SD extension Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/efc79e4d-2119-4cdb-8afb-f26e318a38ef Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * fix: address review - use oai_settings for account ID, sort dropdown alphabetically, remove Account ID input, move debug log Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/bf0dda38-df40-44f4-8a63-0c952b48905d Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * Clean-up diffs * feat: add refresh models button to Workers AI section Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/ab6b5e7a-84d2-44d1-9f6e-3d330de04ef1 Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * fix: revert unrelated package-lock.json changes Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/ab6b5e7a-84d2-44d1-9f6e-3d330de04ef1 Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * Fix models loading * refactor: update model refresh button ID and add class to select elements * Send formData to BFL models * fix: adjust use FormData condition * fix: validate Workers AI account ID before proceeding with image model loading --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -99,6 +99,7 @@ const sources = { | |||
| 99 | google: 'google', | 99 | google: 'google', |
| 100 | zai: 'zai', | 100 | zai: 'zai', |
| 101 | openrouter: 'openrouter', | 101 | openrouter: 'openrouter', |
| 102 | workersai: 'workersai', | ||
| 102 | }; | 103 | }; |
| 103 | const comfyTypes = { | 104 | const comfyTypes = { |
| 104 | standard: 'standard', | 105 | standard: 'standard', |
| @@ -1747,6 +1748,9 @@ async function loadSamplers() { | |||
| 1747 | case sources.openrouter: | 1748 | case sources.openrouter: |
| 1748 | samplers = ['N/A']; | 1749 | samplers = ['N/A']; |
| 1749 | break; | 1750 | break; |
| 1751 | case sources.workersai: | ||
| 1752 | samplers = ['N/A']; | ||
| 1753 | break; | ||
| 1750 | } | 1754 | } |
| 1751 | 1755 | ||
| 1752 | for (const sampler of samplers) { | 1756 | for (const sampler of samplers) { |
| @@ -1997,6 +2001,9 @@ async function loadModels() { | |||
| 1997 | case sources.openrouter: | 2001 | case sources.openrouter: |
| 1998 | models = await loadOpenRouterModels(); | 2002 | models = await loadOpenRouterModels(); |
| 1999 | break; | 2003 | break; |
| 2004 | case sources.workersai: | ||
| 2005 | models = await loadWorkersAIImageModels(); | ||
| 2006 | break; | ||
| 2000 | } | 2007 | } |
| 2001 | 2008 | ||
| 2002 | if (extension_settings.sd.source === sources.electronhub) { | 2009 | if (extension_settings.sd.source === sources.electronhub) { |
| @@ -2124,6 +2131,33 @@ async function loadXAIModels() { | |||
| 2124 | ]; | 2131 | ]; |
| 2125 | } | 2132 | } |
| 2126 | 2133 | ||
| 2134 | async function loadWorkersAIImageModels() { | ||
| 2135 | $('#sd_cf_workers_key').toggleClass('success', !!secret_state[SECRET_KEYS.WORKERS_AI]); | ||
| 2136 | |||
| 2137 | if (!secret_state[SECRET_KEYS.WORKERS_AI]) { | ||
| 2138 | return []; | ||
| 2139 | } | ||
| 2140 | |||
| 2141 | if (!oai_settings.workers_ai_account_id) { | ||
| 2142 | toastr.warning('Workers AI account ID is required. Save it in the "API Connections" panel.', 'Image Generation'); | ||
| 2143 | return []; | ||
| 2144 | } | ||
| 2145 | |||
| 2146 | const result = await fetch('/api/sd/workersai/models', { | ||
| 2147 | method: 'POST', | ||
| 2148 | headers: getRequestHeaders(), | ||
| 2149 | body: JSON.stringify({ | ||
| 2150 | account_id: oai_settings.workers_ai_account_id, | ||
| 2151 | }), | ||
| 2152 | }); | ||
| 2153 | |||
| 2154 | if (result.ok) { | ||
| 2155 | return await result.json(); | ||
| 2156 | } | ||
| 2157 | |||
| 2158 | return []; | ||
| 2159 | } | ||
| 2160 | |||
| 2127 | async function loadPollinationsModels() { | 2161 | async function loadPollinationsModels() { |
| 2128 | $('#sd_pollinations_key').toggleClass('success', !!secret_state[SECRET_KEYS.POLLINATIONS]); | 2162 | $('#sd_pollinations_key').toggleClass('success', !!secret_state[SECRET_KEYS.POLLINATIONS]); |
| 2129 | 2163 | ||
| @@ -2609,6 +2643,9 @@ async function loadSchedulers() { | |||
| 2609 | case sources.openrouter: | 2643 | case sources.openrouter: |
| 2610 | schedulers = ['N/A']; | 2644 | schedulers = ['N/A']; |
| 2611 | break; | 2645 | break; |
| 2646 | case sources.workersai: | ||
| 2647 | schedulers = ['N/A']; | ||
| 2648 | break; | ||
| 2612 | } | 2649 | } |
| 2613 | 2650 | ||
| 2614 | for (const scheduler of schedulers) { | 2651 | for (const scheduler of schedulers) { |
| @@ -2729,6 +2766,9 @@ async function loadVaes() { | |||
| 2729 | case sources.openrouter: | 2766 | case sources.openrouter: |
| 2730 | vaes = ['N/A']; | 2767 | vaes = ['N/A']; |
| 2731 | break; | 2768 | break; |
| 2769 | case sources.workersai: | ||
| 2770 | vaes = ['N/A']; | ||
| 2771 | break; | ||
| 2732 | } | 2772 | } |
| 2733 | 2773 | ||
| 2734 | for (const vae of vaes) { | 2774 | for (const vae of vaes) { |
| @@ -3432,6 +3472,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP | |||
| 3432 | case sources.openrouter: | 3472 | case sources.openrouter: |
| 3433 | result = await generateOpenRouterImage(prefixedPrompt, signal); | 3473 | result = await generateOpenRouterImage(prefixedPrompt, signal); |
| 3434 | break; | 3474 | break; |
| 3475 | case sources.workersai: | ||
| 3476 | result = await generateWorkersAIImage(prefixedPrompt, negativePrompt, signal); | ||
| 3477 | break; | ||
| 3435 | } | 3478 | } |
| 3436 | 3479 | ||
| 3437 | if (!result.data) { | 3480 | if (!result.data) { |
| @@ -4748,6 +4791,33 @@ async function generateOpenRouterImage(prompt, signal) { | |||
| 4748 | throw new Error(text); | 4791 | throw new Error(text); |
| 4749 | } | 4792 | } |
| 4750 | 4793 | ||
| 4794 | async function generateWorkersAIImage(prompt, negativePrompt, signal) { | ||
| 4795 | const result = await fetch('/api/sd/workersai/generate', { | ||
| 4796 | method: 'POST', | ||
| 4797 | headers: getRequestHeaders(), | ||
| 4798 | signal: signal, | ||
| 4799 | body: JSON.stringify({ | ||
| 4800 | prompt: prompt, | ||
| 4801 | negative_prompt: negativePrompt, | ||
| 4802 | model: extension_settings.sd.model, | ||
| 4803 | width: extension_settings.sd.width, | ||
| 4804 | height: extension_settings.sd.height, | ||
| 4805 | steps: extension_settings.sd.steps, | ||
| 4806 | scale: extension_settings.sd.scale, | ||
| 4807 | seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, | ||
| 4808 | account_id: oai_settings.workers_ai_account_id, | ||
| 4809 | }), | ||
| 4810 | }); | ||
| 4811 | |||
| 4812 | if (result.ok) { | ||
| 4813 | const data = await result.json(); | ||
| 4814 | return { format: data?.format, data: data?.image }; | ||
| 4815 | } else { | ||
| 4816 | const text = await result.text(); | ||
| 4817 | throw new Error(text); | ||
| 4818 | } | ||
| 4819 | } | ||
| 4820 | |||
| 4751 | async function onComfyOpenWorkflowEditorClick() { | 4821 | async function onComfyOpenWorkflowEditorClick() { |
| 4752 | let workflow = await (await fetch('/api/sd/comfy/workflow', { | 4822 | let workflow = await (await fetch('/api/sd/comfy/workflow', { |
| 4753 | method: 'POST', | 4823 | method: 'POST', |
| @@ -5120,6 +5190,8 @@ function isValidState() { | |||
| 5120 | return secret_state[SECRET_KEYS.ZAI]; | 5190 | return secret_state[SECRET_KEYS.ZAI]; |
| 5121 | case sources.openrouter: | 5191 | case sources.openrouter: |
| 5122 | return secret_state[SECRET_KEYS.OPENROUTER]; | 5192 | return secret_state[SECRET_KEYS.OPENROUTER]; |
| 5193 | case sources.workersai: | ||
| 5194 | return !!oai_settings.workers_ai_account_id && secret_state[SECRET_KEYS.WORKERS_AI]; | ||
| 5123 | default: | 5195 | default: |
| 5124 | return false; | 5196 | return false; |
| 5125 | } | 5197 | } |
| @@ -5879,6 +5951,9 @@ export async function init() { | |||
| 5879 | extension_settings.sd.google_duration = Number($(this).val()); | 5951 | extension_settings.sd.google_duration = Number($(this).val()); |
| 5880 | saveSettingsDebounced(); | 5952 | saveSettingsDebounced(); |
| 5881 | }); | 5953 | }); |
| 5954 | $('#sd_models_refresh').on('click', async () => { | ||
| 5955 | await loadModels(); | ||
| 5956 | }); | ||
| 5882 | $('#sd_electronhub_quality').on('change', function () { | 5957 | $('#sd_electronhub_quality').on('change', function () { |
| 5883 | extension_settings.sd.electronhub_quality = String($(this).val()); | 5958 | extension_settings.sd.electronhub_quality = String($(this).val()); |
| 5884 | saveSettingsDebounced(); | 5959 | saveSettingsDebounced(); |
| @@ -5922,6 +5997,7 @@ export async function init() { | |||
| 5922 | [sources.aimlapi]: SECRET_KEYS.AIMLAPI, | 5997 | [sources.aimlapi]: SECRET_KEYS.AIMLAPI, |
| 5923 | [sources.comfy]: SECRET_KEYS.COMFY_RUNPOD, | 5998 | [sources.comfy]: SECRET_KEYS.COMFY_RUNPOD, |
| 5924 | [sources.pollinations]: SECRET_KEYS.POLLINATIONS, | 5999 | [sources.pollinations]: SECRET_KEYS.POLLINATIONS, |
| 6000 | [sources.workersai]: SECRET_KEYS.WORKERS_AI, | ||
| 5925 | }; | 6001 | }; |
| 5926 | const shouldReloadOptions = Object.entries(keySourceMap).some(([k, v]) => k === extension_settings.sd.source && v === key); | 6002 | const shouldReloadOptions = Object.entries(keySourceMap).some(([k, v]) => k === extension_settings.sd.source && v === key); |
| 5927 | if (!shouldReloadOptions) { | 6003 | if (!shouldReloadOptions) { |
| @@ -40,10 +40,11 @@ | |||
| 40 | <span data-i18n="sd_minimal_prompt_processing_txt">Minimal response prompt processing</span> | 40 | <span data-i18n="sd_minimal_prompt_processing_txt">Minimal response prompt processing</span> |
| 41 | </label> | 41 | </label> |
| 42 | <label for="sd_source" data-i18n="Source">Source</label> | 42 | <label for="sd_source" data-i18n="Source">Source</label> |
| 43 | <select id="sd_source"> | 43 | <select id="sd_source" class="text_pole"> |
| 44 | <option value="aimlapi">AI/ML API</option> | 44 | <option value="aimlapi">AI/ML API</option> |
| 45 | <option value="bfl">BFL (Black Forest Labs)</option> | 45 | <option value="bfl">BFL (Black Forest Labs)</option> |
| 46 | <option value="chutes">Chutes</option> | 46 | <option value="chutes">Chutes</option> |
| 47 | <option value="workersai">Cloudflare Workers AI</option> | ||
| 47 | <option value="comfy">ComfyUI</option> | 48 | <option value="comfy">ComfyUI</option> |
| 48 | <option value="drawthings">DrawThings HTTP API</option> | 49 | <option value="drawthings">DrawThings HTTP API</option> |
| 49 | <option value="electronhub">Electron Hub</option> | 50 | <option value="electronhub">Electron Hub</option> |
| @@ -123,7 +124,7 @@ | |||
| 123 | <div class="flex-container" id="sd_electronhub_quality_row"> | 124 | <div class="flex-container" id="sd_electronhub_quality_row"> |
| 124 | <div class="flex1"> | 125 | <div class="flex1"> |
| 125 | <label for="sd_electronhub_quality" data-i18n="Image Quality">Image Quality</label> | 126 | <label for="sd_electronhub_quality" data-i18n="Image Quality">Image Quality</label> |
| 126 | <select id="sd_electronhub_quality"></select> | 127 | <select id="sd_electronhub_quality" class="text_pole"></select> |
| 127 | </div> | 128 | </div> |
| 128 | </div> | 129 | </div> |
| 129 | </div> | 130 | </div> |
| @@ -191,14 +192,14 @@ | |||
| 191 | <div class="flex-container"> | 192 | <div class="flex-container"> |
| 192 | <div data-sd-model="dall-e-3" class="flex1"> | 193 | <div data-sd-model="dall-e-3" class="flex1"> |
| 193 | <label for="sd_openai_style" data-i18n="Image Style">Image Style</label> | 194 | <label for="sd_openai_style" data-i18n="Image Style">Image Style</label> |
| 194 | <select id="sd_openai_style"> | 195 | <select id="sd_openai_style" class="text_pole"> |
| 195 | <option value="vivid">Vivid</option> | 196 | <option value="vivid">Vivid</option> |
| 196 | <option value="natural">Natural</option> | 197 | <option value="natural">Natural</option> |
| 197 | </select> | 198 | </select> |
| 198 | </div> | 199 | </div> |
| 199 | <div data-sd-model="gpt-image" class="flex1"> | 200 | <div data-sd-model="gpt-image" class="flex1"> |
| 200 | <label for="sd_openai_quality_gpt" data-i18n="Image Quality">Image Quality</label> | 201 | <label for="sd_openai_quality_gpt" data-i18n="Image Quality">Image Quality</label> |
| 201 | <select id="sd_openai_quality_gpt"> | 202 | <select id="sd_openai_quality_gpt" class="text_pole"> |
| 202 | <option value="auto" data-i18n="Auto">Auto</option> | 203 | <option value="auto" data-i18n="Auto">Auto</option> |
| 203 | <option value="low" data-i18n="Low">Low</option> | 204 | <option value="low" data-i18n="Low">Low</option> |
| 204 | <option value="medium" data-i18n="Medium">Medium</option> | 205 | <option value="medium" data-i18n="Medium">Medium</option> |
| @@ -207,7 +208,7 @@ | |||
| 207 | </div> | 208 | </div> |
| 208 | <div data-sd-model="dall-e-3,cogview-4,glm-image,cogvideox" class="flex1"> | 209 | <div data-sd-model="dall-e-3,cogview-4,glm-image,cogvideox" class="flex1"> |
| 209 | <label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label> | 210 | <label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label> |
| 210 | <select id="sd_openai_quality"> | 211 | <select id="sd_openai_quality" class="text_pole"> |
| 211 | <option value="standard" data-i18n="Standard">Standard</option> | 212 | <option value="standard" data-i18n="Standard">Standard</option> |
| 212 | <option value="hd" data-i18n="HD">HD</option> | 213 | <option value="hd" data-i18n="HD">HD</option> |
| 213 | </select> | 214 | </select> |
| @@ -216,7 +217,7 @@ | |||
| 216 | <div data-sd-model="sora-2,sora-2-pro" class="flex-container"> | 217 | <div data-sd-model="sora-2,sora-2-pro" class="flex-container"> |
| 217 | <div class="flex1"> | 218 | <div class="flex1"> |
| 218 | <label for="sd_openai_duration" data-i18n="Duration">Duration</label> | 219 | <label for="sd_openai_duration" data-i18n="Duration">Duration</label> |
| 219 | <select id="sd_openai_duration"> | 220 | <select id="sd_openai_duration" class="text_pole"> |
| 220 | <option value="4" data-i18n="Short (4 seconds)">Short (4 seconds)</option> | 221 | <option value="4" data-i18n="Short (4 seconds)">Short (4 seconds)</option> |
| 221 | <option value="8" data-i18n="Medium (8 seconds)">Medium (8 seconds)</option> | 222 | <option value="8" data-i18n="Medium (8 seconds)">Medium (8 seconds)</option> |
| 222 | <option value="12" data-i18n="Long (16 seconds)">Long (12 seconds)</option> | 223 | <option value="12" data-i18n="Long (16 seconds)">Long (12 seconds)</option> |
| @@ -226,7 +227,7 @@ | |||
| 226 | </div> | 227 | </div> |
| 227 | <div data-sd-source="comfy"> | 228 | <div data-sd-source="comfy"> |
| 228 | <label for="sd_comfy_type">Server Type</label> | 229 | <label for="sd_comfy_type">Server Type</label> |
| 229 | <select id="sd_comfy_type"> | 230 | <select id="sd_comfy_type" class="text_pole"> |
| 230 | <option value="standard">Standard Server</option> | 231 | <option value="standard">Standard Server</option> |
| 231 | <option value="runpod_serverless">RunPod Serverless Endpoint</option> | 232 | <option value="runpod_serverless">RunPod Serverless Endpoint</option> |
| 232 | </select> | 233 | </select> |
| @@ -318,7 +319,7 @@ | |||
| 318 | <div class="flex-container"> | 319 | <div class="flex-container"> |
| 319 | <div class="flex1"> | 320 | <div class="flex1"> |
| 320 | <label for="sd_stability_style_preset" data-i18n="Style Preset">Style Preset</label> | 321 | <label for="sd_stability_style_preset" data-i18n="Style Preset">Style Preset</label> |
| 321 | <select id="sd_stability_style_preset"> | 322 | <select id="sd_stability_style_preset" class="text_pole"> |
| 322 | <option value="anime">Anime</option> | 323 | <option value="anime">Anime</option> |
| 323 | <option value="3d-model">3D Model</option> | 324 | <option value="3d-model">3D Model</option> |
| 324 | <option value="analog-film">Analog Film</option> | 325 | <option value="analog-film">Analog Film</option> |
| @@ -375,6 +376,20 @@ | |||
| 375 | </div> | 376 | </div> |
| 376 | </div> | 377 | </div> |
| 377 | 378 | ||
| 379 | <div data-sd-source="workersai"> | ||
| 380 | <a href="https://dash.cloudflare.com" target="_blank" rel="noopener noreferrer">Cloudflare Workers AI</a> | ||
| 381 | <div class="flex-container flexnowrap alignItemsBaseline marginBot5"> | ||
| 382 | <strong class="flex1" data-i18n="API Key">API Key</strong> | ||
| 383 | <div id="sd_cf_workers_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_workers_ai"> | ||
| 384 | <i class="fa-fw fa-solid fa-key"></i> | ||
| 385 | <span data-i18n="Click to set">Click to set</span> | ||
| 386 | </div> | ||
| 387 | </div> | ||
| 388 | <div class="flex-container flexnowrap alignItemsBaseline"> | ||
| 389 | <small class="flex1" data-i18n="Hint: Account ID and API key are pulled from API connections.">Hint: Account ID and API key are pulled from API connections.</small> | ||
| 390 | </div> | ||
| 391 | </div> | ||
| 392 | |||
| 378 | <div data-sd-source="google"> | 393 | <div data-sd-source="google"> |
| 379 | <div class="flex-container"> | 394 | <div class="flex-container"> |
| 380 | <div class="flex1"> | 395 | <div class="flex1"> |
| @@ -394,7 +409,7 @@ | |||
| 394 | </label> | 409 | </label> |
| 395 | <div class="flex1"> | 410 | <div class="flex1"> |
| 396 | <label for="sd_google_duration" data-i18n="Duration (Veo)">Duration (Veo)</label> | 411 | <label for="sd_google_duration" data-i18n="Duration (Veo)">Duration (Veo)</label> |
| 397 | <select id="sd_google_duration"> | 412 | <select id="sd_google_duration" class="text_pole"> |
| 398 | <option value="4">Short (4 seconds)</option> | 413 | <option value="4">Short (4 seconds)</option> |
| 399 | <option value="6">Medium (6 seconds)</option> | 414 | <option value="6">Medium (6 seconds)</option> |
| 400 | <option value="8">Long (8 seconds)</option> | 415 | <option value="8">Long (8 seconds)</option> |
| @@ -405,37 +420,42 @@ | |||
| 405 | 420 | ||
| 406 | <div class="flex-container"> | 421 | <div class="flex-container"> |
| 407 | <div class="flex1"> | 422 | <div class="flex1"> |
| 408 | <label for="sd_model" data-i18n="Model">Model</label> | 423 | <label for="sd_model" class="flex-container justifySpaceBetween"> |
| 409 | <select id="sd_model"></select> | 424 | <span data-i18n="Model">Model</span> |
| 425 | <div id="sd_models_refresh" class="right_menu_button margin0 padding0" title="Refresh model list" data-i18n="[title]Refresh model list"> | ||
| 426 | <i class="fa-solid fa-sync"></i> | ||
| 427 | </div> | ||
| 428 | </label> | ||
| 429 | <select id="sd_model" class="text_pole"></select> | ||
| 410 | </div> | 430 | </div> |
| 411 | 431 | ||
| 412 | <div class="flex1" data-sd-source="comfy,auto"> | 432 | <div class="flex1" data-sd-source="comfy,auto"> |
| 413 | <label for="sd_vae">VAE</label> | 433 | <label for="sd_vae">VAE</label> |
| 414 | <select id="sd_vae"></select> | 434 | <select id="sd_vae" class="text_pole"></select> |
| 415 | </div> | 435 | </div> |
| 416 | </div> | 436 | </div> |
| 417 | 437 | ||
| 418 | <div class="flex-container"> | 438 | <div class="flex-container"> |
| 419 | <div class="flex1" data-sd-source="extras,horde,auto,drawthings,novel,vlad,comfy,sdcpp"> | 439 | <div class="flex1" data-sd-source="extras,horde,auto,drawthings,novel,vlad,comfy,sdcpp"> |
| 420 | <label for="sd_sampler" data-i18n="Sampling method">Sampling method</label> | 440 | <label for="sd_sampler" data-i18n="Sampling method">Sampling method</label> |
| 421 | <select id="sd_sampler"></select> | 441 | <select id="sd_sampler" class="text_pole"></select> |
| 422 | </div> | 442 | </div> |
| 423 | 443 | ||
| 424 | <div class="flex1" data-sd-source="comfy,auto,novel,sdcpp"> | 444 | <div class="flex1" data-sd-source="comfy,auto,novel,sdcpp"> |
| 425 | <label for="sd_scheduler" data-i18n="Scheduler">Scheduler</label> | 445 | <label for="sd_scheduler" data-i18n="Scheduler">Scheduler</label> |
| 426 | <select id="sd_scheduler"></select> | 446 | <select id="sd_scheduler" class="text_pole"></select> |
| 427 | </div> | 447 | </div> |
| 428 | </div> | 448 | </div> |
| 429 | 449 | ||
| 430 | <div class="flex-container"> | 450 | <div class="flex-container"> |
| 431 | <div class="flex1"> | 451 | <div class="flex1"> |
| 432 | <label for="sd_resolution" data-i18n="Resolution">Resolution</label> | 452 | <label for="sd_resolution" data-i18n="Resolution">Resolution</label> |
| 433 | <select id="sd_resolution"><!-- Populated in JS --></select> | 453 | <select id="sd_resolution" class="text_pole"><!-- Populated in JS --></select> |
| 434 | </div> | 454 | </div> |
| 435 | 455 | ||
| 436 | <div class="flex1" data-sd-source="auto,vlad,drawthings"> | 456 | <div class="flex1" data-sd-source="auto,vlad,drawthings"> |
| 437 | <label for="sd_hr_upscaler" data-i18n="Upscaler">Upscaler</label> | 457 | <label for="sd_hr_upscaler" data-i18n="Upscaler">Upscaler</label> |
| 438 | <select id="sd_hr_upscaler"></select> | 458 | <select id="sd_hr_upscaler" class="text_pole"></select> |
| 439 | </div> | 459 | </div> |
| 440 | </div> | 460 | </div> |
| 441 | 461 | ||
| @@ -1,7 +1,3 @@ | |||
| 1 | .sd_settings label:not(.checkbox_label) { | ||
| 2 | display: block; | ||
| 3 | } | ||
| 4 | |||
| 5 | #sd_dropdown { | 1 | #sd_dropdown { |
| 6 | z-index: 30000; | 2 | z-index: 30000; |
| 7 | backdrop-filter: blur(var(--SmartThemeBlurStrength)); | 3 | backdrop-filter: blur(var(--SmartThemeBlurStrength)); |
| @@ -5,7 +5,6 @@ import express from 'express'; | |||
| 5 | import fetch from 'node-fetch'; | 5 | import fetch from 'node-fetch'; |
| 6 | import sanitize from 'sanitize-filename'; | 6 | import sanitize from 'sanitize-filename'; |
| 7 | import { sync as writeFileAtomicSync } from 'write-file-atomic'; | 7 | import { sync as writeFileAtomicSync } from 'write-file-atomic'; |
| 8 | import FormData from 'form-data'; | ||
| 9 | import urlJoin from 'url-join'; | 8 | import urlJoin from 'url-join'; |
| 10 | import _ from 'lodash'; | 9 | import _ from 'lodash'; |
| 11 | import mime from 'mime-types'; | 10 | import mime from 'mime-types'; |
| @@ -2031,6 +2030,145 @@ zai.post('/generate-video', async (request, response) => { | |||
| 2031 | } | 2030 | } |
| 2032 | }); | 2031 | }); |
| 2033 | 2032 | ||
| 2033 | const workersai = express.Router(); | ||
| 2034 | |||
| 2035 | workersai.post('/models', async (request, response) => { | ||
| 2036 | try { | ||
| 2037 | const key = readSecret(request.user.directories, SECRET_KEYS.WORKERS_AI); | ||
| 2038 | |||
| 2039 | if (!key) { | ||
| 2040 | console.warn('Cloudflare Workers AI API key not found.'); | ||
| 2041 | return response.sendStatus(400); | ||
| 2042 | } | ||
| 2043 | |||
| 2044 | const accountId = String(request.body.account_id || '').trim(); | ||
| 2045 | if (!accountId) { | ||
| 2046 | console.warn('Cloudflare Workers AI Account ID not found.'); | ||
| 2047 | return response.sendStatus(400); | ||
| 2048 | } | ||
| 2049 | |||
| 2050 | const apiUrl = new URL(`https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/models/search`); | ||
| 2051 | apiUrl.searchParams.set('task', 'Text-to-Image'); | ||
| 2052 | apiUrl.searchParams.set('per_page', '1000'); | ||
| 2053 | const result = await fetch(apiUrl, { | ||
| 2054 | method: 'GET', | ||
| 2055 | headers: { | ||
| 2056 | 'Authorization': `Bearer ${key}`, | ||
| 2057 | }, | ||
| 2058 | }); | ||
| 2059 | |||
| 2060 | if (!result.ok) { | ||
| 2061 | console.warn('Cloudflare Workers AI returned an error.', result.statusText); | ||
| 2062 | return response.sendStatus(500); | ||
| 2063 | } | ||
| 2064 | |||
| 2065 | /** @type {any} */ | ||
| 2066 | const data = await result.json(); | ||
| 2067 | |||
| 2068 | if (!data.success || !Array.isArray(data.result)) { | ||
| 2069 | console.warn('Cloudflare Workers AI returned invalid data.'); | ||
| 2070 | return response.sendStatus(500); | ||
| 2071 | } | ||
| 2072 | |||
| 2073 | const models = data.result.map(x => ({ value: x.name, text: x.name })); | ||
| 2074 | return response.send(models); | ||
| 2075 | } catch (error) { | ||
| 2076 | console.error(error); | ||
| 2077 | return response.sendStatus(500); | ||
| 2078 | } | ||
| 2079 | }); | ||
| 2080 | |||
| 2081 | workersai.post('/generate', async (request, response) => { | ||
| 2082 | try { | ||
| 2083 | const key = readSecret(request.user.directories, SECRET_KEYS.WORKERS_AI); | ||
| 2084 | |||
| 2085 | if (!key) { | ||
| 2086 | console.warn('Cloudflare Workers AI API key not found.'); | ||
| 2087 | return response.sendStatus(400); | ||
| 2088 | } | ||
| 2089 | |||
| 2090 | const accountId = String(request.body.account_id || '').trim(); | ||
| 2091 | if (!accountId) { | ||
| 2092 | console.warn('Cloudflare Workers AI Account ID not found.'); | ||
| 2093 | return response.sendStatus(400); | ||
| 2094 | } | ||
| 2095 | |||
| 2096 | const model = String(request.body.model || '').trim(); | ||
| 2097 | if (!model) { | ||
| 2098 | console.warn('Cloudflare Workers AI model not specified.'); | ||
| 2099 | return response.sendStatus(400); | ||
| 2100 | } | ||
| 2101 | |||
| 2102 | const apiUrl = `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/run/${model}`; | ||
| 2103 | |||
| 2104 | const body = { | ||
| 2105 | prompt: request.body.prompt, | ||
| 2106 | negative_prompt: request.body.negative_prompt || undefined, | ||
| 2107 | width: request.body.width ? Number(request.body.width) : undefined, | ||
| 2108 | height: request.body.height ? Number(request.body.height) : undefined, | ||
| 2109 | num_steps: request.body.steps ? Number(request.body.steps) : undefined, | ||
| 2110 | guidance: request.body.scale ? Number(request.body.scale) : undefined, | ||
| 2111 | seed: request.body.seed >= 0 ? Number(request.body.seed) : undefined, | ||
| 2112 | }; | ||
| 2113 | |||
| 2114 | // Remove undefined values | ||
| 2115 | for (const prop of Object.keys(body)) { | ||
| 2116 | if (body[prop] === undefined) { | ||
| 2117 | delete body[prop]; | ||
| 2118 | } | ||
| 2119 | } | ||
| 2120 | |||
| 2121 | console.debug('Cloudflare Workers AI request:', model, body); | ||
| 2122 | |||
| 2123 | /** @type {import('node-fetch').RequestInit} */ | ||
| 2124 | const apiRequest = { | ||
| 2125 | method: 'POST', | ||
| 2126 | headers: { | ||
| 2127 | 'Authorization': `Bearer ${key}`, | ||
| 2128 | }, | ||
| 2129 | }; | ||
| 2130 | |||
| 2131 | if (/flux-2/.test(model)) { | ||
| 2132 | const formData = new FormData(); | ||
| 2133 | for (const [key, value] of Object.entries(body)) { | ||
| 2134 | formData.append(key, String(value)); | ||
| 2135 | } | ||
| 2136 | apiRequest.body = formData; | ||
| 2137 | } else { | ||
| 2138 | apiRequest.headers = { ...apiRequest.headers, 'Content-Type': 'application/json' }; | ||
| 2139 | apiRequest.body = JSON.stringify(body); | ||
| 2140 | } | ||
| 2141 | |||
| 2142 | const result = await fetch(apiUrl, apiRequest); | ||
| 2143 | if (!result.ok) { | ||
| 2144 | const text = await result.text(); | ||
| 2145 | console.warn('Cloudflare Workers AI returned an error.', result.status, result.statusText, text); | ||
| 2146 | return response.status(500).send(text); | ||
| 2147 | } | ||
| 2148 | |||
| 2149 | const contentType = result.headers.get('content-type') || ''; | ||
| 2150 | |||
| 2151 | // Partner models return JSON with base64 image | ||
| 2152 | if (contentType.includes('application/json')) { | ||
| 2153 | /** @type {any} */ | ||
| 2154 | const data = await result.json(); | ||
| 2155 | const image = data?.result?.image || data?.image; | ||
| 2156 | if (!image) { | ||
| 2157 | console.warn('Cloudflare Workers AI returned JSON without image data.'); | ||
| 2158 | return response.sendStatus(500); | ||
| 2159 | } | ||
| 2160 | return response.send({ format: 'png', image: image }); | ||
| 2161 | } | ||
| 2162 | |||
| 2163 | // Non-partner models return raw binary image data | ||
| 2164 | const buffer = await result.arrayBuffer(); | ||
| 2165 | return response.send({ format: 'png', image: Buffer.from(buffer).toString('base64') }); | ||
| 2166 | } catch (error) { | ||
| 2167 | console.error(error); | ||
| 2168 | return response.sendStatus(500); | ||
| 2169 | } | ||
| 2170 | }); | ||
| 2171 | |||
| 2034 | router.use('/comfy', comfy); | 2172 | router.use('/comfy', comfy); |
| 2035 | router.use('/comfyrunpod', comfyRunPod); | 2173 | router.use('/comfyrunpod', comfyRunPod); |
| 2036 | router.use('/together', together); | 2174 | router.use('/together', together); |
| @@ -2047,3 +2185,4 @@ router.use('/falai', falai); | |||
| 2047 | router.use('/xai', xai); | 2185 | router.use('/xai', xai); |
| 2048 | router.use('/aimlapi', aimlapi); | 2186 | router.use('/aimlapi', aimlapi); |
| 2049 | router.use('/zai', zai); | 2187 | router.use('/zai', zai); |
| 2188 | router.use('/workersai', workersai); | ||