Add support for FAL.AI as image gen provider
Signed| @@ -81,6 +81,7 @@ const sources = { | |||
| 81 | huggingface: 'huggingface', | 81 | huggingface: 'huggingface', |
| 82 | nanogpt: 'nanogpt', | 82 | nanogpt: 'nanogpt', |
| 83 | bfl: 'bfl', | 83 | bfl: 'bfl', |
| 84 | falai: 'falai', | ||
| 84 | }; | 85 | }; |
| 85 | 86 | ||
| 86 | const initiators = { | 87 | const initiators = { |
| @@ -1169,6 +1170,10 @@ async function onBflKeyClick() { | |||
| 1169 | return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL); | 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 | function onBflUpsamplingInput() { | 1177 | function onBflUpsamplingInput() { |
| 1173 | extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked'); | 1178 | extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked'); |
| 1174 | saveSettingsDebounced(); | 1179 | saveSettingsDebounced(); |
| @@ -1707,6 +1712,9 @@ async function loadModels() { | |||
| 1707 | case sources.bfl: | 1712 | case sources.bfl: |
| 1708 | models = await loadBflModels(); | 1713 | models = await loadBflModels(); |
| 1709 | break; | 1714 | break; |
| 1715 | case sources.falai: | ||
| 1716 | models = await loadFalaiModels(); | ||
| 1717 | break; | ||
| 1710 | } | 1718 | } |
| 1711 | 1719 | ||
| 1712 | for (const model of models) { | 1720 | for (const model of models) { |
| @@ -1744,6 +1752,21 @@ async function loadBflModels() { | |||
| 1744 | ]; | 1752 | ]; |
| 1745 | } | 1753 | } |
| 1746 | 1754 | ||
| 1755 | async function loadFalaiModels() { | ||
| 1756 | $('#sd_falai_key').toggleClass('success', !!secret_state[SECRET_KEYS.FALAI]); | ||
| 1757 | |||
| 1758 | const result = await fetch('/api/sd/falai/models', { | ||
| 1759 | method: 'POST', | ||
| 1760 | headers: getRequestHeaders(), | ||
| 1761 | }); | ||
| 1762 | |||
| 1763 | if (result.ok) { | ||
| 1764 | return await result.json(); | ||
| 1765 | } | ||
| 1766 | |||
| 1767 | return []; | ||
| 1768 | } | ||
| 1769 | |||
| 1747 | async function loadPollinationsModels() { | 1770 | async function loadPollinationsModels() { |
| 1748 | const result = await fetch('/api/sd/pollinations/models', { | 1771 | const result = await fetch('/api/sd/pollinations/models', { |
| 1749 | method: 'POST', | 1772 | method: 'POST', |
| @@ -2081,6 +2104,9 @@ async function loadSchedulers() { | |||
| 2081 | case sources.bfl: | 2104 | case sources.bfl: |
| 2082 | schedulers = ['N/A']; | 2105 | schedulers = ['N/A']; |
| 2083 | break; | 2106 | break; |
| 2107 | case sources.falai: | ||
| 2108 | schedulers = ['N/A']; | ||
| 2109 | break; | ||
| 2084 | } | 2110 | } |
| 2085 | 2111 | ||
| 2086 | for (const scheduler of schedulers) { | 2112 | for (const scheduler of schedulers) { |
| @@ -2735,6 +2761,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP | |||
| 2735 | case sources.bfl: | 2761 | case sources.bfl: |
| 2736 | result = await generateBflImage(prefixedPrompt, signal); | 2762 | result = await generateBflImage(prefixedPrompt, signal); |
| 2737 | break; | 2763 | break; |
| 2764 | case sources.falai: | ||
| 2765 | result = await generateFalaiImage(prefixedPrompt, negativePrompt, signal); | ||
| 2766 | break; | ||
| 2738 | } | 2767 | } |
| 2739 | 2768 | ||
| 2740 | if (!result.data) { | 2769 | if (!result.data) { |
| @@ -3496,6 +3525,39 @@ async function generateBflImage(prompt, signal) { | |||
| 3496 | } | 3525 | } |
| 3497 | } | 3526 | } |
| 3498 | 3527 | ||
| 3528 | /** | ||
| 3529 | * Generates an image using the FAL.AI API. | ||
| 3530 | * @param {string} prompt - The main instruction used to guide the image generation. | ||
| 3531 | * @param {string} negativePrompt - The negative prompt used to guide the image generation. | ||
| 3532 | * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request. | ||
| 3533 | * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete. | ||
| 3534 | */ | ||
| 3535 | async function generateFalaiImage(prompt, negativePrompt, signal) { | ||
| 3536 | const result = await fetch('/api/sd/falai/generate', { | ||
| 3537 | method: 'POST', | ||
| 3538 | headers: getRequestHeaders(), | ||
| 3539 | signal: signal, | ||
| 3540 | body: JSON.stringify({ | ||
| 3541 | prompt: prompt, | ||
| 3542 | negative_prompt: negativePrompt, | ||
| 3543 | model: extension_settings.sd.model, | ||
| 3544 | steps: clamp(extension_settings.sd.steps, 1, 50), | ||
| 3545 | guidance: clamp(extension_settings.sd.scale, 1.5, 5), | ||
| 3546 | width: clamp(extension_settings.sd.width, 256, 1440), | ||
| 3547 | height: clamp(extension_settings.sd.height, 256, 1440), | ||
| 3548 | seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined, | ||
| 3549 | }), | ||
| 3550 | }); | ||
| 3551 | |||
| 3552 | if (result.ok) { | ||
| 3553 | const data = await result.json(); | ||
| 3554 | return { format: 'jpg', data: data.image }; | ||
| 3555 | } else { | ||
| 3556 | const text = await result.text(); | ||
| 3557 | throw new Error(text); | ||
| 3558 | } | ||
| 3559 | } | ||
| 3560 | |||
| 3499 | async function onComfyOpenWorkflowEditorClick() { | 3561 | async function onComfyOpenWorkflowEditorClick() { |
| 3500 | let workflow = await (await fetch('/api/sd/comfy/workflow', { | 3562 | let workflow = await (await fetch('/api/sd/comfy/workflow', { |
| 3501 | method: 'POST', | 3563 | method: 'POST', |
| @@ -3782,6 +3844,8 @@ function isValidState() { | |||
| 3782 | return secret_state[SECRET_KEYS.NANOGPT]; | 3844 | return secret_state[SECRET_KEYS.NANOGPT]; |
| 3783 | case sources.bfl: | 3845 | case sources.bfl: |
| 3784 | return secret_state[SECRET_KEYS.BFL]; | 3846 | return secret_state[SECRET_KEYS.BFL]; |
| 3847 | case sources.falai: | ||
| 3848 | return secret_state[SECRET_KEYS.FALAI]; | ||
| 3785 | } | 3849 | } |
| 3786 | } | 3850 | } |
| 3787 | 3851 | ||
| @@ -4443,6 +4507,7 @@ jQuery(async () => { | |||
| 4443 | $('#sd_function_tool').on('input', onFunctionToolInput); | 4507 | $('#sd_function_tool').on('input', onFunctionToolInput); |
| 4444 | $('#sd_bfl_key').on('click', onBflKeyClick); | 4508 | $('#sd_bfl_key').on('click', onBflKeyClick); |
| 4445 | $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput); | 4509 | $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput); |
| 4510 | $('#sd_falai_key').on('click', onFalaiKeyClick); | ||
| 4446 | 4511 | ||
| 4447 | if (!CSS.supports('field-sizing', 'content')) { | 4512 | if (!CSS.supports('field-sizing', 'content')) { |
| 4448 | $('.sd_settings .inline-drawer-toggle').on('click', function () { | 4513 | $('.sd_settings .inline-drawer-toggle').on('click', function () { |
| @@ -52,6 +52,7 @@ | |||
| 52 | <option value="auto">Stable Diffusion Web UI (AUTOMATIC1111)</option> | 52 | <option value="auto">Stable Diffusion Web UI (AUTOMATIC1111)</option> |
| 53 | <option value="horde">Stable Horde</option> | 53 | <option value="horde">Stable Horde</option> |
| 54 | <option value="togetherai">TogetherAI</option> | 54 | <option value="togetherai">TogetherAI</option> |
| 55 | <option value="falai">FAL.AI</option> | ||
| 55 | </select> | 56 | </select> |
| 56 | <div data-sd-source="auto"> | 57 | <div data-sd-source="auto"> |
| 57 | <label for="sd_auto_url">SD Web UI URL</label> | 58 | <label for="sd_auto_url">SD Web UI URL</label> |
| @@ -256,6 +257,20 @@ | |||
| 256 | </label> | 257 | </label> |
| 257 | </div> | 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 | <div class="flex-container"> | 274 | <div class="flex-container"> |
| 260 | <div class="flex1"> | 275 | <div class="flex1"> |
| 261 | <label for="sd_model" data-i18n="Model">Model</label> | 276 | <label for="sd_model" data-i18n="Model">Model</label> |
| @@ -41,6 +41,7 @@ export const SECRET_KEYS = { | |||
| 41 | GENERIC: 'api_key_generic', | 41 | GENERIC: 'api_key_generic', |
| 42 | DEEPSEEK: 'api_key_deepseek', | 42 | DEEPSEEK: 'api_key_deepseek', |
| 43 | SERPER: 'api_key_serper', | 43 | SERPER: 'api_key_serper', |
| 44 | FALAI: 'api_key_falai', | ||
| 44 | }; | 45 | }; |
| 45 | 46 | ||
| 46 | const INPUT_MAP = { | 47 | const INPUT_MAP = { |
| @@ -50,6 +50,7 @@ export const SECRET_KEYS = { | |||
| 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 | BFL: 'api_key_bfl', |
| 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', |
| 55 | SERPER: 'api_key_serper', | 56 | SERPER: 'api_key_serper', |
| @@ -1228,6 +1228,125 @@ 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 | .map(x => ({ value: x.modelUrl.split('fal-ai/')[1], text: x.title })); | ||
| 1255 | return response.send(models); | ||
| 1256 | } catch (error) { | ||
| 1257 | console.error(error); | ||
| 1258 | return response.sendStatus(500); | ||
| 1259 | } | ||
| 1260 | }); | ||
| 1261 | |||
| 1262 | falai.post('/generate', jsonParser, async (request, response) => { | ||
| 1263 | try { | ||
| 1264 | const key = readSecret(request.user.directories, SECRET_KEYS.FALAI); | ||
| 1265 | |||
| 1266 | if (!key) { | ||
| 1267 | console.warn('FAL.AI key not found.'); | ||
| 1268 | return response.sendStatus(400); | ||
| 1269 | } | ||
| 1270 | |||
| 1271 | const requestBody = { | ||
| 1272 | prompt: request.body.prompt, | ||
| 1273 | image_size: { 'width': request.body.width, 'height': request.body.height }, | ||
| 1274 | num_inference_steps: request.body.steps, | ||
| 1275 | seed: request.body.seed ?? null, | ||
| 1276 | guidance_scale: request.body.guidance, | ||
| 1277 | enable_safety_checker: false, | ||
| 1278 | }; | ||
| 1279 | |||
| 1280 | console.debug('FAL.AI request:', requestBody); | ||
| 1281 | |||
| 1282 | const result = await fetch(`https://queue.fal.run/fal-ai/${request.body.model}`, { | ||
| 1283 | method: 'POST', | ||
| 1284 | body: JSON.stringify(requestBody), | ||
| 1285 | headers: { | ||
| 1286 | 'Content-Type': 'application/json', | ||
| 1287 | 'Authorization': `Key ${key}`, | ||
| 1288 | }, | ||
| 1289 | }); | ||
| 1290 | |||
| 1291 | if (!result.ok) { | ||
| 1292 | console.warn('FAL.AI returned an error.'); | ||
| 1293 | return response.sendStatus(500); | ||
| 1294 | } | ||
| 1295 | |||
| 1296 | /** @type {any} */ | ||
| 1297 | const taskData = await result.json(); | ||
| 1298 | const { status_url } = taskData; | ||
| 1299 | |||
| 1300 | const MAX_ATTEMPTS = 100; | ||
| 1301 | for (let i = 0; i < MAX_ATTEMPTS; i++) { | ||
| 1302 | await delay(2500); | ||
| 1303 | |||
| 1304 | const statusResult = await fetch(status_url, { | ||
| 1305 | headers: { | ||
| 1306 | 'Authorization': `Key ${key}`, | ||
| 1307 | }, | ||
| 1308 | }); | ||
| 1309 | |||
| 1310 | if (!statusResult.ok) { | ||
| 1311 | const text = await statusResult.text(); | ||
| 1312 | console.warn('FAL.AI returned an error.', text); | ||
| 1313 | return response.sendStatus(500); | ||
| 1314 | } | ||
| 1315 | |||
| 1316 | /** @type {any} */ | ||
| 1317 | const statusData = await statusResult.json(); | ||
| 1318 | |||
| 1319 | if (statusData?.status === 'IN_QUEUE' || statusData?.status === 'IN_PROGRESS') { | ||
| 1320 | continue; | ||
| 1321 | } | ||
| 1322 | |||
| 1323 | if (statusData?.status === 'COMPLETED') { | ||
| 1324 | const resultFetch = await fetch(statusData?.response_url, { | ||
| 1325 | method: 'GET', | ||
| 1326 | headers: { | ||
| 1327 | 'Authorization': `Key ${key}`, | ||
| 1328 | }, | ||
| 1329 | }); | ||
| 1330 | const resultData = await resultFetch.json(); | ||
| 1331 | const imageFetch = await fetch(resultData?.images[0].url, { | ||
| 1332 | headers: { | ||
| 1333 | 'Authorization': `Key ${key}`, | ||
| 1334 | }, | ||
| 1335 | }); | ||
| 1336 | |||
| 1337 | const fetchData = await imageFetch.arrayBuffer(); | ||
| 1338 | const image = Buffer.from(fetchData).toString('base64'); | ||
| 1339 | return response.send({ image: image }); | ||
| 1340 | } | ||
| 1341 | |||
| 1342 | throw new Error('FAL.AI failed to generate image.', { cause: statusData }); | ||
| 1343 | } | ||
| 1344 | } catch (error) { | ||
| 1345 | console.error(error); | ||
| 1346 | return response.sendStatus(500); | ||
| 1347 | } | ||
| 1348 | }); | ||
| 1349 | |||
| 1231 | router.use('/comfy', comfy); | 1350 | router.use('/comfy', comfy); |
| 1232 | router.use('/together', together); | 1351 | router.use('/together', together); |
| 1233 | router.use('/drawthings', drawthings); | 1352 | router.use('/drawthings', drawthings); |
| @@ -1237,3 +1356,4 @@ router.use('/blockentropy', blockentropy); | |||
| 1237 | router.use('/huggingface', huggingface); | 1356 | router.use('/huggingface', huggingface); |
| 1238 | router.use('/nanogpt', nanogpt); | 1357 | router.use('/nanogpt', nanogpt); |
| 1239 | router.use('/bfl', bfl); | 1358 | router.use('/bfl', bfl); |
| 1359 | router.use('/falai', falai); | ||