Merge pull request #3476 from Dakraid/feature/fal-integration Feature: FAL.AI Integration
Signed| @@ -81,6 +81,7 @@ const sources = { | ||
| 81 | 81 | huggingface: 'huggingface', |
| 82 | 82 | nanogpt: 'nanogpt', |
| 83 | 83 | bfl: 'bfl', |
| 84 | + falai: 'falai', | |
| 84 | 85 | }; |
| 85 | 86 | |
| 86 | 87 | const initiators = { |
| @@ -1169,6 +1170,10 @@ async function onBflKeyClick() { | ||
| 1169 | 1170 | return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL); |
| 1170 | 1171 | } |
| 1171 | 1172 | |
| 1173 | +async function onFalaiKeyClick() { | |
| 1174 | + return onApiKeyClick('FALAI API Key:', SECRET_KEYS.FALAI); | |
| 1175 | +} | |
| 1176 | + | |
| 1172 | 1177 | function onBflUpsamplingInput() { |
| 1173 | 1178 | extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked'); |
| 1174 | 1179 | saveSettingsDebounced(); |
| @@ -1299,6 +1304,7 @@ async function onModelChange() { | ||
| 1299 | 1304 | sources.huggingface, |
| 1300 | 1305 | sources.nanogpt, |
| 1301 | 1306 | sources.bfl, |
| 1307 | + sources.falai, | |
| 1302 | 1308 | ]; |
| 1303 | 1309 | |
| 1304 | 1310 | if (cloudSources.includes(extension_settings.sd.source)) { |
| @@ -1707,6 +1713,9 @@ async function loadModels() { | ||
| 1707 | 1713 | case sources.bfl: |
| 1708 | 1714 | models = await loadBflModels(); |
| 1709 | 1715 | break; |
| 1716 | + case sources.falai: | |
| 1717 | + models = await loadFalaiModels(); | |
| 1718 | + break; | |
| 1710 | 1719 | } |
| 1711 | 1720 | |
| 1712 | 1721 | for (const model of models) { |
| @@ -1744,6 +1753,21 @@ async function loadBflModels() { | ||
| 1744 | 1753 | ]; |
| 1745 | 1754 | } |
| 1746 | 1755 | |
| 1756 | +async function loadFalaiModels() { | |
| 1757 | + $('#sd_falai_key').toggleClass('success', !!secret_state[SECRET_KEYS.FALAI]); | |
| 1758 | + | |
| 1759 | + const result = await fetch('/api/sd/falai/models', { | |
| 1760 | + method: 'POST', | |
| 1761 | + headers: getRequestHeaders(), | |
| 1762 | + }); | |
| 1763 | + | |
| 1764 | + if (result.ok) { | |
| 1765 | + return await result.json(); | |
| 1766 | + } | |
| 1767 | + | |
| 1768 | + return []; | |
| 1769 | +} | |
| 1770 | + | |
| 1747 | 1771 | async function loadPollinationsModels() { |
| 1748 | 1772 | const result = await fetch('/api/sd/pollinations/models', { |
| 1749 | 1773 | method: 'POST', |
| @@ -2081,6 +2105,9 @@ async function loadSchedulers() { | ||
| 2081 | 2105 | case sources.bfl: |
| 2082 | 2106 | schedulers = ['N/A']; |
| 2083 | 2107 | break; |
| 2108 | + case sources.falai: | |
| 2109 | + schedulers = ['N/A']; | |
| 2110 | + break; | |
| 2084 | 2111 | } |
| 2085 | 2112 | |
| 2086 | 2113 | for (const scheduler of schedulers) { |
| @@ -2735,6 +2762,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP | ||
| 2735 | 2762 | case sources.bfl: |
| 2736 | 2763 | result = await generateBflImage(prefixedPrompt, signal); |
| 2737 | 2764 | break; |
| 2765 | + case sources.falai: | |
| 2766 | + result = await generateFalaiImage(prefixedPrompt, negativePrompt, signal); | |
| 2767 | + break; | |
| 2738 | 2768 | } |
| 2739 | 2769 | |
| 2740 | 2770 | if (!result.data) { |
| @@ -3496,6 +3526,40 @@ async function generateBflImage(prompt, signal) { | ||
| 3496 | 3526 | } |
| 3497 | 3527 | } |
| 3498 | 3528 | |
| 3529 | +/** | |
| 3530 | + * Generates an image using the FAL.AI API. | |
| 3531 | + * @param {string} prompt - The main instruction used to guide the image generation. | |
| 3532 | + * @param {string} negativePrompt - The negative prompt used to guide the image generation. | |
| 3533 | + * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. | |
| 3534 | + * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. | |
| 3535 | + */ | |
| 3536 | +async function generateFalaiImage(prompt, negativePrompt, signal) { | |
| 3537 | + const result = await fetch('/api/sd/falai/generate', { | |
| 3538 | + method: 'POST', | |
| 3539 | + headers: getRequestHeaders(), | |
| 3540 | + signal: signal, | |
| 3541 | + body: JSON.stringify({ | |
| 3542 | + prompt: prompt, | |
| 3543 | + negative_prompt: negativePrompt, | |
| 3544 | + model: extension_settings.sd.model, | |
| 3545 | + steps: clamp(extension_settings.sd.steps, 1, 50), | |
| 3546 | + guidance: clamp(extension_settings.sd.scale, 1.5, 5), | |
| 3547 | + width: clamp(extension_settings.sd.width, 256, 1440), | |
| 3548 | + height: clamp(extension_settings.sd.height, 256, 1440), | |
| 3549 | + seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, | |
| 3550 | + }), | |
| 3551 | + }); | |
| 3552 | + | |
| 3553 | + if (result.ok) { | |
| 3554 | + const data = await result.json(); | |
| 3555 | + return { format: 'jpg', data: data.image }; | |
| 3556 | + } else { | |
| 3557 | + const text = await result.text(); | |
| 3558 | + console.log(text); | |
| 3559 | + throw new Error(text); | |
| 3560 | + } | |
| 3561 | +} | |
| 3562 | + | |
| 3499 | 3563 | async function onComfyOpenWorkflowEditorClick() { |
| 3500 | 3564 | let workflow = await (await fetch('/api/sd/comfy/workflow', { |
| 3501 | 3565 | method: 'POST', |
| @@ -3782,6 +3846,8 @@ function isValidState() { | ||
| 3782 | 3846 | return secret_state[SECRET_KEYS.NANOGPT]; |
| 3783 | 3847 | case sources.bfl: |
| 3784 | 3848 | return secret_state[SECRET_KEYS.BFL]; |
| 3849 | + case sources.falai: | |
| 3850 | + return secret_state[SECRET_KEYS.FALAI]; | |
| 3785 | 3851 | } |
| 3786 | 3852 | } |
| 3787 | 3853 | |
| @@ -4443,6 +4509,7 @@ jQuery(async () => { | ||
| 4443 | 4509 | $('#sd_function_tool').on('input', onFunctionToolInput); |
| 4444 | 4510 | $('#sd_bfl_key').on('click', onBflKeyClick); |
| 4445 | 4511 | $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput); |
| 4512 | + $('#sd_falai_key').on('click', onFalaiKeyClick); | |
| 4446 | 4513 | |
| 4447 | 4514 | if (!CSS.supports('field-sizing', 'content')) { |
| 4448 | 4515 | $('.sd_settings .inline-drawer-toggle').on('click', function () { |
| @@ -42,6 +42,7 @@ | ||
| 42 | 42 | <option value="comfy">ComfyUI</option> |
| 43 | 43 | <option value="drawthings">DrawThings HTTP API</option> |
| 44 | 44 | <option value="extras">Extras API (deprecated)</option> |
| 45 | + <option value="falai">FAL.AI</option> | |
| 45 | 46 | <option value="huggingface">HuggingFace Inference API (serverless)</option> |
| 46 | 47 | <option value="nanogpt">NanoGPT</option> |
| 47 | 48 | <option value="novel">NovelAI Diffusion</option> |
| @@ -256,6 +257,20 @@ | ||
| 256 | 257 | </label> |
| 257 | 258 | </div> |
| 258 | 259 | |
| 260 | + <div data-sd-source="falai"> | |
| 261 | + <div class="flex-container flexnowrap alignItemsBaseline marginBot5"> | |
| 262 | + <a href="https://fal.ai/dashboard" target="_blank" rel="noopener noreferrer"> | |
| 263 | + <strong data-i18n="API Key">API Key</strong> | |
| 264 | + <i class="fa-solid fa-share-from-square"></i> | |
| 265 | + </a> | |
| 266 | + <span class="expander"></span> | |
| 267 | + <div id="sd_falai_key" class="menu_button menu_button_icon"> | |
| 268 | + <i class="fa-fw fa-solid fa-key"></i> | |
| 269 | + <span data-i18n="Click to set">Click to set</span> | |
| 270 | + </div> | |
| 271 | + </div> | |
| 272 | + </div> | |
| 273 | + | |
| 259 | 274 | <div class="flex-container"> |
| 260 | 275 | <div class="flex1"> |
| 261 | 276 | <label for="sd_model" data-i18n="Model">Model</label> |
| @@ -41,6 +41,7 @@ export const SECRET_KEYS = { | ||
| 41 | 41 | GENERIC: 'api_key_generic', |
| 42 | 42 | DEEPSEEK: 'api_key_deepseek', |
| 43 | 43 | SERPER: 'api_key_serper', |
| 44 | + FALAI: 'api_key_falai', | |
| 44 | 45 | }; |
| 45 | 46 | |
| 46 | 47 | const INPUT_MAP = { |
| @@ -50,6 +50,7 @@ export const SECRET_KEYS = { | ||
| 50 | 50 | TAVILY: 'api_key_tavily', |
| 51 | 51 | NANOGPT: 'api_key_nanogpt', |
| 52 | 52 | BFL: 'api_key_bfl', |
| 53 | + FALAI: 'api_key_falai', | |
| 53 | 54 | GENERIC: 'api_key_generic', |
| 54 | 55 | DEEPSEEK: 'api_key_deepseek', |
| 55 | 56 | SERPER: 'api_key_serper', |
| @@ -1228,6 +1228,131 @@ bfl.post('/generate', jsonParser, async (request, response) => { | ||
| 1228 | 1228 | } |
| 1229 | 1229 | }); |
| 1230 | 1230 | |
| 1231 | +const falai = express.Router(); | |
| 1232 | + | |
| 1233 | +falai.post('/models', jsonParser, async (_request, response) => { | |
| 1234 | + try { | |
| 1235 | + const modelsUrl = new URL('https://fal.ai/api/models?categories=text-to-image'); | |
| 1236 | + const result = await fetch(modelsUrl); | |
| 1237 | + | |
| 1238 | + if (!result.ok) { | |
| 1239 | + console.warn('FAL.AI returned an error.', result.status, result.statusText); | |
| 1240 | + throw new Error('FAL.AI request failed.'); | |
| 1241 | + } | |
| 1242 | + | |
| 1243 | + const data = await result.json(); | |
| 1244 | + | |
| 1245 | + if (!Array.isArray(data)) { | |
| 1246 | + console.warn('FAL.AI returned invalid data.'); | |
| 1247 | + throw new Error('FAL.AI request failed.'); | |
| 1248 | + } | |
| 1249 | + | |
| 1250 | + const models = data | |
| 1251 | + .filter(x => !x.title.toLowerCase().includes('inpainting') && | |
| 1252 | + !x.title.toLowerCase().includes('control') && | |
| 1253 | + !x.title.toLowerCase().includes('upscale')) | |
| 1254 | + .sort((a, b) => a.title.localeCompare(b.title)) | |
| 1255 | + .map(x => ({ value: x.modelUrl.split('fal-ai/')[1], text: x.title })); | |
| 1256 | + return response.send(models); | |
| 1257 | + } catch (error) { | |
| 1258 | + console.error(error); | |
| 1259 | + return response.sendStatus(500); | |
| 1260 | + } | |
| 1261 | +}); | |
| 1262 | + | |
| 1263 | +falai.post('/generate', jsonParser, async (request, response) => { | |
| 1264 | + try { | |
| 1265 | + const key = readSecret(request.user.directories, SECRET_KEYS.FALAI); | |
| 1266 | + | |
| 1267 | + if (!key) { | |
| 1268 | + console.warn('FAL.AI key not found.'); | |
| 1269 | + return response.sendStatus(400); | |
| 1270 | + } | |
| 1271 | + | |
| 1272 | + const requestBody = { | |
| 1273 | + prompt: request.body.prompt, | |
| 1274 | + image_size: { 'width': request.body.width, 'height': request.body.height }, | |
| 1275 | + num_inference_steps: request.body.steps, | |
| 1276 | + seed: request.body.seed ?? null, | |
| 1277 | + guidance_scale: request.body.guidance, | |
| 1278 | + enable_safety_checker: false, | |
| 1279 | + }; | |
| 1280 | + | |
| 1281 | + console.debug('FAL.AI request:', requestBody); | |
| 1282 | + | |
| 1283 | + const result = await fetch(`https://queue.fal.run/fal-ai/${request.body.model}`, { | |
| 1284 | + method: 'POST', | |
| 1285 | + body: JSON.stringify(requestBody), | |
| 1286 | + headers: { | |
| 1287 | + 'Content-Type': 'application/json', | |
| 1288 | + 'Authorization': `Key ${key}`, | |
| 1289 | + }, | |
| 1290 | + }); | |
| 1291 | + | |
| 1292 | + if (!result.ok) { | |
| 1293 | + console.warn('FAL.AI returned an error.'); | |
| 1294 | + return response.sendStatus(500); | |
| 1295 | + } | |
| 1296 | + | |
| 1297 | + /** @type {any} */ | |
| 1298 | + const taskData = await result.json(); | |
| 1299 | + const { status_url } = taskData; | |
| 1300 | + | |
| 1301 | + const MAX_ATTEMPTS = 100; | |
| 1302 | + for (let i = 0; i < MAX_ATTEMPTS; i++) { | |
| 1303 | + await delay(2500); | |
| 1304 | + | |
| 1305 | + const statusResult = await fetch(status_url, { | |
| 1306 | + headers: { | |
| 1307 | + 'Authorization': `Key ${key}`, | |
| 1308 | + }, | |
| 1309 | + }); | |
| 1310 | + | |
| 1311 | + if (!statusResult.ok) { | |
| 1312 | + const text = await statusResult.text(); | |
| 1313 | + console.warn('FAL.AI returned an error.', text); | |
| 1314 | + return response.sendStatus(500); | |
| 1315 | + } | |
| 1316 | + | |
| 1317 | + /** @type {any} */ | |
| 1318 | + const statusData = await statusResult.json(); | |
| 1319 | + | |
| 1320 | + if (statusData?.status === 'IN_QUEUE' || statusData?.status === 'IN_PROGRESS') { | |
| 1321 | + continue; | |
| 1322 | + } | |
| 1323 | + | |
| 1324 | + if (statusData?.status === 'COMPLETED') { | |
| 1325 | + const resultFetch = await fetch(statusData?.response_url, { | |
| 1326 | + method: 'GET', | |
| 1327 | + headers: { | |
| 1328 | + 'Authorization': `Key ${key}`, | |
| 1329 | + }, | |
| 1330 | + }); | |
| 1331 | + const resultData = await resultFetch.json(); | |
| 1332 | + | |
| 1333 | + if (resultData.detail !== null && resultData.detail !== undefined) { | |
| 1334 | + throw new Error('FAL.AI failed to generate image.', { cause: `${resultData.detail[0].loc[1]}: ${resultData.detail[0].msg}` }); | |
| 1335 | + } | |
| 1336 | + | |
| 1337 | + const imageFetch = await fetch(resultData?.images[0].url, { | |
| 1338 | + headers: { | |
| 1339 | + 'Authorization': `Key ${key}`, | |
| 1340 | + }, | |
| 1341 | + }); | |
| 1342 | + | |
| 1343 | + const fetchData = await imageFetch.arrayBuffer(); | |
| 1344 | + const image = Buffer.from(fetchData).toString('base64'); | |
| 1345 | + return response.send({ image: image }); | |
| 1346 | + } | |
| 1347 | + | |
| 1348 | + throw new Error('FAL.AI failed to generate image.', { cause: statusData }); | |
| 1349 | + } | |
| 1350 | + } catch (error) { | |
| 1351 | + console.error(error); | |
| 1352 | + return response.status(500).send(error.cause || error.message); | |
| 1353 | + } | |
| 1354 | +}); | |
| 1355 | + | |
| 1231 | 1356 | router.use('/comfy', comfy); |
| 1232 | 1357 | router.use('/together', together); |
| 1233 | 1358 | router.use('/drawthings', drawthings); |
| @@ -1237,3 +1362,4 @@ router.use('/blockentropy', blockentropy); | ||
| 1237 | 1362 | router.use('/huggingface', huggingface); |
| 1238 | 1363 | router.use('/nanogpt', nanogpt); |
| 1239 | 1364 | router.use('/bfl', bfl); |
| 1365 | +router.use('/falai', falai); | |