ImageGen: add BFL API for image generation

67869364a531b206ae48a27c32585fdf8e57b968

Cohee <18619528+Cohee1207@users.noreply.github.com>

5 files changed, +226 -5Showing whitespace changes
public/scripts/extensions/stable-diffusion/index.js+86 -4
@@ -39,6 +39,7 @@ const UPDATE_INTERVAL = 1000;
39// This is a 1x1 transparent PNG39// This is a 1x1 transparent PNG
40const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';40const PNG_PIXEL = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
41const CUSTOM_STOP_EVENT = 'sd_stop_generation';41const CUSTOM_STOP_EVENT = 'sd_stop_generation';
42const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
4243
43const sources = {44const sources = {
44 extras: 'extras',45 extras: 'extras',
@@ -55,6 +56,7 @@ const sources = {
55 blockentropy: 'blockentropy',56 blockentropy: 'blockentropy',
56 huggingface: 'huggingface',57 huggingface: 'huggingface',
57 nanogpt: 'nanogpt',58 nanogpt: 'nanogpt',
59 bfl: 'bfl',
58};60};
5961
60const initiators = {62const initiators = {
@@ -296,6 +298,9 @@ const defaultSettings = {
296298
297 // Stability AI settings299 // Stability AI settings
298 stability_style_preset: 'anime',300 stability_style_preset: 'anime',
301
302 // BFL API settings
303 bfl_upsampling: false,
299};304};
300305
301const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);306const writePromptFieldsDebounced = debounce(writePromptFields, debounce_timeout.relaxed);
@@ -463,6 +468,7 @@ async function loadSettings() {
463 $('#sd_stability_style_preset').val(extension_settings.sd.stability_style_preset);468 $('#sd_stability_style_preset').val(extension_settings.sd.stability_style_preset);
464 $('#sd_huggingface_model_id').val(extension_settings.sd.huggingface_model_id);469 $('#sd_huggingface_model_id').val(extension_settings.sd.huggingface_model_id);
465 $('#sd_function_tool').prop('checked', extension_settings.sd.function_tool);470 $('#sd_function_tool').prop('checked', extension_settings.sd.function_tool);
471 $('#sd_bfl_upsampling').prop('checked', extension_settings.sd.bfl_upsampling);
466472
467 for (const style of extension_settings.sd.styles) {473 for (const style of extension_settings.sd.styles) {
468 const option = document.createElement('option');474 const option = document.createElement('option');
@@ -1089,15 +1095,14 @@ function onComfyWorkflowChange() {
1089 saveSettingsDebounced();1095 saveSettingsDebounced();
1090}1096}
10911097
1092async function onStabilityKeyClick() {1098async function onApiKeyClick(popupText, secretKey) {
1093 const popupText = 'Stability AI API Key:';
1094 const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, '', {1099 const key = await callGenericPopup(popupText, POPUP_TYPE.INPUT, '', {
1095 customButtons: [{1100 customButtons: [{
1096 text: 'Remove Key',1101 text: 'Remove Key',
1097 appendAtEnd: true,1102 appendAtEnd: true,
1098 result: POPUP_RESULT.NEGATIVE,1103 result: POPUP_RESULT.NEGATIVE,
1099 action: async () => {1104 action: async () => {
1100 await writeSecret(SECRET_KEYS.STABILITY, '');1105 await writeSecret(secretKey, '');
1101 toastr.success('API Key removed');1106 toastr.success('API Key removed');
1102 await loadSettingOptions();1107 await loadSettingOptions();
1103 },1108 },
@@ -1108,12 +1113,25 @@ async function onStabilityKeyClick() {
1108 return;1113 return;
1109 }1114 }
11101115
1111 await writeSecret(SECRET_KEYS.STABILITY, String(key));1116 await writeSecret(secretKey, String(key));
11121117
1113 toastr.success('API Key saved');1118 toastr.success('API Key saved');
1114 await loadSettingOptions();1119 await loadSettingOptions();
1115}1120}
11161121
1122async function onStabilityKeyClick() {
1123 return onApiKeyClick('Stability AI API Key:', SECRET_KEYS.STABILITY);
1124}
1125
1126async function onBflKeyClick() {
1127 return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL);
1128}
1129
1130function onBflUpsamplingInput() {
1131 extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked');
1132 saveSettingsDebounced();
1133}
1134
1117function onStabilityStylePresetChange() {1135function onStabilityStylePresetChange() {
1118 extension_settings.sd.stability_style_preset = String($('#sd_stability_style_preset').val());1136 extension_settings.sd.stability_style_preset = String($('#sd_stability_style_preset').val());
1119 saveSettingsDebounced();1137 saveSettingsDebounced();
@@ -1238,6 +1256,7 @@ async function onModelChange() {
1238 sources.blockentropy,1256 sources.blockentropy,
1239 sources.huggingface,1257 sources.huggingface,
1240 sources.nanogpt,1258 sources.nanogpt,
1259 sources.bfl,
1241 ];1260 ];
12421261
1243 if (cloudSources.includes(extension_settings.sd.source)) {1262 if (cloudSources.includes(extension_settings.sd.source)) {
@@ -1459,6 +1478,9 @@ async function loadSamplers() {
1459 case sources.nanogpt:1478 case sources.nanogpt:
1460 samplers = ['N/A'];1479 samplers = ['N/A'];
1461 break;1480 break;
1481 case sources.bfl:
1482 samplers = ['N/A'];
1483 break;
1462 }1484 }
14631485
1464 for (const sampler of samplers) {1486 for (const sampler of samplers) {
@@ -1654,6 +1676,9 @@ async function loadModels() {
1654 case sources.nanogpt:1676 case sources.nanogpt:
1655 models = await loadNanoGPTModels();1677 models = await loadNanoGPTModels();
1656 break;1678 break;
1679 case sources.bfl:
1680 models = await loadBflModels();
1681 break;
1657 }1682 }
16581683
1659 for (const model of models) {1684 for (const model of models) {
@@ -1680,6 +1705,17 @@ async function loadStabilityModels() {
1680 ];1705 ];
1681}1706}
16821707
1708async function loadBflModels() {
1709 $('#sd_bfl_key').toggleClass('success', !!secret_state[SECRET_KEYS.BFL]);
1710
1711 return [
1712 { value: 'flux-pro-1.1-ultra', text: 'flux-pro-1.1-ultra' },
1713 { value: 'flux-pro-1.1', text: 'flux-pro-1.1' },
1714 { value: 'flux-pro', text: 'flux-pro' },
1715 { value: 'flux-dev', text: 'flux-dev' },
1716 ];
1717}
1718
1683async function loadPollinationsModels() {1719async function loadPollinationsModels() {
1684 const result = await fetch('/api/sd/pollinations/models', {1720 const result = await fetch('/api/sd/pollinations/models', {
1685 method: 'POST',1721 method: 'POST',
@@ -2027,6 +2063,9 @@ async function loadSchedulers() {
2027 case sources.nanogpt:2063 case sources.nanogpt:
2028 schedulers = ['N/A'];2064 schedulers = ['N/A'];
2029 break;2065 break;
2066 case sources.bfl:
2067 schedulers = ['N/A'];
2068 break;
2030 }2069 }
20312070
2032 for (const scheduler of schedulers) {2071 for (const scheduler of schedulers) {
@@ -2112,6 +2151,9 @@ async function loadVaes() {
2112 case sources.nanogpt:2151 case sources.nanogpt:
2113 vaes = ['N/A'];2152 vaes = ['N/A'];
2114 break;2153 break;
2154 case sources.bfl:
2155 vaes = ['N/A'];
2156 break;
2115 }2157 }
21162158
2117 for (const vae of vaes) {2159 for (const vae of vaes) {
@@ -2666,6 +2708,10 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
2666 break;2708 break;
2667 case sources.nanogpt:2709 case sources.nanogpt:
2668 result = await generateNanoGPTImage(prefixedPrompt, negativePrompt, signal);2710 result = await generateNanoGPTImage(prefixedPrompt, negativePrompt, signal);
2711 break;
2712 case sources.bfl:
2713 result = await generateBflImage(prefixedPrompt, signal);
2714 break;
2669 }2715 }
26702716
2671 if (!result.data) {2717 if (!result.data) {
@@ -3384,6 +3430,38 @@ async function generateNanoGPTImage(prompt, negativePrompt, signal) {
3384 }3430 }
3385}3431}
33863432
3433/**
3434 * Generates an image using the BFL API.
3435 * @param {string} prompt - The main instruction used to guide the image generation.
3436 * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
3437 * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
3438 */
3439async function generateBflImage(prompt, signal) {
3440 const result = await fetch('/api/sd/bfl/generate', {
3441 method: 'POST',
3442 headers: getRequestHeaders(),
3443 signal: signal,
3444 body: JSON.stringify({
3445 prompt: prompt,
3446 model: extension_settings.sd.model,
3447 steps: clamp(extension_settings.sd.steps, 1, 50),
3448 guidance: clamp(extension_settings.sd.scale, 1.5, 5),
3449 width: clamp(extension_settings.sd.width, 256, 1440),
3450 height: clamp(extension_settings.sd.height, 256, 1440),
3451 prompt_upsampling: !!extension_settings.sd.bfl_upsampling,
3452 seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
3453 }),
3454 });
3455
3456 if (result.ok) {
3457 const data = await result.json();
3458 return { format: 'jpg', data: data.image };
3459 } else {
3460 const text = await result.text();
3461 throw new Error(text);
3462 }
3463}
3464
3387async function onComfyOpenWorkflowEditorClick() {3465async function onComfyOpenWorkflowEditorClick() {
3388 let workflow = await (await fetch('/api/sd/comfy/workflow', {3466 let workflow = await (await fetch('/api/sd/comfy/workflow', {
3389 method: 'POST',3467 method: 'POST',
@@ -3668,6 +3746,8 @@ function isValidState() {
3668 return secret_state[SECRET_KEYS.HUGGINGFACE];3746 return secret_state[SECRET_KEYS.HUGGINGFACE];
3669 case sources.nanogpt:3747 case sources.nanogpt:
3670 return secret_state[SECRET_KEYS.NANOGPT];3748 return secret_state[SECRET_KEYS.NANOGPT];
3749 case sources.bfl:
3750 return secret_state[SECRET_KEYS.BFL];
3671 }3751 }
3672}3752}
36733753
@@ -4338,6 +4418,8 @@ jQuery(async () => {
4338 $('#sd_stability_style_preset').on('change', onStabilityStylePresetChange);4418 $('#sd_stability_style_preset').on('change', onStabilityStylePresetChange);
4339 $('#sd_huggingface_model_id').on('input', onHFModelInput);4419 $('#sd_huggingface_model_id').on('input', onHFModelInput);
4340 $('#sd_function_tool').on('input', onFunctionToolInput);4420 $('#sd_function_tool').on('input', onFunctionToolInput);
4421 $('#sd_bfl_key').on('click', onBflKeyClick);
4422 $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput);
43414423
4342 if (!CSS.supports('field-sizing', 'content')) {4424 if (!CSS.supports('field-sizing', 'content')) {
4343 $('.sd_settings .inline-drawer-toggle').on('click', function () {4425 $('.sd_settings .inline-drawer-toggle').on('click', function () {
public/scripts/extensions/stable-diffusion/settings.html+23 -1
@@ -37,6 +37,7 @@
37 </label>37 </label>
38 <label for="sd_source" data-i18n="Source">Source</label>38 <label for="sd_source" data-i18n="Source">Source</label>
39 <select id="sd_source">39 <select id="sd_source">
40 <option value="bfl">BFL (Black Forest Labs)</option>
40 <option value="blockentropy">Block Entropy</option>41 <option value="blockentropy">Block Entropy</option>
41 <option value="comfy">ComfyUI</option>42 <option value="comfy">ComfyUI</option>
42 <option value="drawthings">DrawThings HTTP API</option>43 <option value="drawthings">DrawThings HTTP API</option>
@@ -234,6 +235,27 @@
234 </div>235 </div>
235 </div>236 </div>
236 </div>237 </div>
238
239 <div data-sd-source="bfl">
240 <div class="flex-container flexnowrap alignItemsBaseline marginBot5">
241 <a href="https://api.bfl.ml/" target="_blank" rel="noopener noreferrer">
242 <strong data-i18n="API Key">API Key</strong>
243 <i class="fa-solid fa-share-from-square"></i>
244 </a>
245 <span class="expander"></span>
246 <div id="sd_bfl_key" class="menu_button menu_button_icon">
247 <i class="fa-fw fa-solid fa-key"></i>
248 <span data-i18n="Click to set">Click to set</span>
249 </div>
250 </div>
251 <label class="checkbox_label marginBot5" for="sd_bfl_upsampling" title="Whether to perform upsampling on the prompt. If active, automatically modifies the prompt for more creative generation.">
252 <input id="sd_bfl_upsampling" type="checkbox" />
253 <span data-i18n="Prompt Upsampling">
254 Prompt Upsampling
255 </span>
256 </label>
257 </div>
258
237 <div class="flex-container">259 <div class="flex-container">
238 <div class="flex1">260 <div class="flex1">
239 <label for="sd_model" data-i18n="Model">Model</label>261 <label for="sd_model" data-i18n="Model">Model</label>
@@ -385,7 +407,7 @@
385 </label>407 </label>
386 </div>408 </div>
387409
388 <div data-sd-source="novel,togetherai,pollinations,comfy,drawthings,vlad,auto,horde,extras,stability,blockentropy" class="marginTop5">410 <div data-sd-source="novel,togetherai,pollinations,comfy,drawthings,vlad,auto,horde,extras,stability,blockentropy,bfl" class="marginTop5">
389 <label for="sd_seed">411 <label for="sd_seed">
390 <span data-i18n="Seed">Seed</span>412 <span data-i18n="Seed">Seed</span>
391 <small data-i18n="(-1 for random)">(-1 for random)</small>413 <small data-i18n="(-1 for random)">(-1 for random)</small>
public/scripts/secrets.js+1 -0
@@ -37,6 +37,7 @@ export const SECRET_KEYS = {
37 CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts',37 CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts',
38 NANOGPT: 'api_key_nanogpt',38 NANOGPT: 'api_key_nanogpt',
39 TAVILY: 'api_key_tavily',39 TAVILY: 'api_key_tavily',
40 BFL: 'api_key_bfl',
40};41};
4142
42const INPUT_MAP = {43const INPUT_MAP = {
src/endpoints/secrets.js+1 -0
@@ -49,6 +49,7 @@ export const SECRET_KEYS = {
49 CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts',49 CUSTOM_OPENAI_TTS: 'api_key_custom_openai_tts',
50 TAVILY: 'api_key_tavily',50 TAVILY: 'api_key_tavily',
51 NANOGPT: 'api_key_nanogpt',51 NANOGPT: 'api_key_nanogpt',
52 BFL: 'api_key_bfl',
52};53};
5354
54// These are the keys that are safe to expose, even if allowKeysExposure is false55// These are the keys that are safe to expose, even if allowKeysExposure is false
src/endpoints/stable-diffusion.js+115 -0
@@ -1101,6 +1101,120 @@ nanogpt.post('/generate', jsonParser, async (request, response) => {
1101 }1101 }
1102});1102});
11031103
1104const bfl = express.Router();
1105
1106bfl.post('/generate', jsonParser, async (request, response) => {
1107 try {
1108 const key = readSecret(request.user.directories, SECRET_KEYS.BFL);
1109
1110 if (!key) {
1111 console.log('BFL key not found.');
1112 return response.sendStatus(400);
1113 }
1114
1115 const requestBody = {
1116 prompt: request.body.prompt,
1117 steps: request.body.steps,
1118 guidance: request.body.guidance,
1119 width: request.body.width,
1120 height: request.body.height,
1121 prompt_upsampling: request.body.prompt_upsampling,
1122 seed: request.body.seed ?? null,
1123 safety_tolerance: 6, // being least strict
1124 output_format: 'jpeg',
1125 };
1126
1127 function getClosestAspectRatio(width, height) {
1128 const minAspect = 9 / 21;
1129 const maxAspect = 21 / 9;
1130 const currentAspect = width / height;
1131
1132 const gcd = (a, b) => b === 0 ? a : gcd(b, a % b);
1133 const simplifyRatio = (w, h) => {
1134 const divisor = gcd(w, h);
1135 return `${w / divisor}:${h / divisor}`;
1136 };
1137
1138 if (currentAspect < minAspect) {
1139 const adjustedHeight = Math.round(width / minAspect);
1140 return simplifyRatio(width, adjustedHeight);
1141 } else if (currentAspect > maxAspect) {
1142 const adjustedWidth = Math.round(height * maxAspect);
1143 return simplifyRatio(adjustedWidth, height);
1144 } else {
1145 return simplifyRatio(width, height);
1146 }
1147 }
1148
1149 if (String(request.body.model).endsWith('-ultra')) {
1150 requestBody.aspect_ratio = getClosestAspectRatio(request.body.width, request.body.height);
1151 delete requestBody.steps;
1152 delete requestBody.guidance;
1153 delete requestBody.width;
1154 delete requestBody.height;
1155 delete requestBody.prompt_upsampling;
1156 }
1157
1158 if (String(request.body.model).endsWith('-pro-1.1')) {
1159 delete requestBody.steps;
1160 delete requestBody.guidance;
1161 }
1162
1163 console.log('BFL request:', requestBody);
1164
1165 const result = await fetch(`https://api.bfl.ml/v1/${request.body.model}`, {
1166 method: 'POST',
1167 body: JSON.stringify(requestBody),
1168 headers: {
1169 'Content-Type': 'application/json',
1170 'x-key': key,
1171 },
1172 });
1173
1174 if (!result.ok) {
1175 console.log('BFL returned an error.');
1176 return response.sendStatus(500);
1177 }
1178
1179 /** @type {any} */
1180 const taskData = await result.json();
1181 const { id } = taskData;
1182
1183 const MAX_ATTEMPTS = 100;
1184 for (let i = 0; i < MAX_ATTEMPTS; i++) {
1185 await delay(2500);
1186
1187 const statusResult = await fetch(`https://api.bfl.ml/v1/get_result?id=${id}`);
1188
1189 if (!statusResult.ok) {
1190 const text = await statusResult.text();
1191 console.log('BFL returned an error.', text);
1192 return response.sendStatus(500);
1193 }
1194
1195 /** @type {any} */
1196 const statusData = await statusResult.json();
1197
1198 if (statusData?.status === 'Pending') {
1199 continue;
1200 }
1201
1202 if (statusData?.status === 'Ready') {
1203 const { sample } = statusData.result;
1204 const fetchResult = await fetch(sample);
1205 const fetchData = await fetchResult.arrayBuffer();
1206 const image = Buffer.from(fetchData).toString('base64');
1207 return response.send({ image: image });
1208 }
1209
1210 throw new Error('BFL failed to generate image.', { cause: statusData });
1211 }
1212 } catch (error) {
1213 console.log(error);
1214 return response.sendStatus(500);
1215 }
1216});
1217
1104router.use('/comfy', comfy);1218router.use('/comfy', comfy);
1105router.use('/together', together);1219router.use('/together', together);
1106router.use('/drawthings', drawthings);1220router.use('/drawthings', drawthings);
@@ -1109,3 +1223,4 @@ router.use('/stability', stability);
1109router.use('/blockentropy', blockentropy);1223router.use('/blockentropy', blockentropy);
1110router.use('/huggingface', huggingface);1224router.use('/huggingface', huggingface);
1111router.use('/nanogpt', nanogpt);1225router.use('/nanogpt', nanogpt);
1226router.use('/bfl', bfl);