Add support for FAL.AI as image gen provider

6e0ed8552ff01f1ce445d5d62c2b261fababd0f8

Kristan Schlikow <kristian@schlikow.de>

Signed
5 files changed, +202 -0Showing whitespace changes
public/scripts/extensions/stable-diffusion/index.js+65 -0
@@ -81,6 +81,7 @@ const sources = {
8181 huggingface: 'huggingface',
8282 nanogpt: 'nanogpt',
8383 bfl: 'bfl',
84+ falai: 'falai',
8485};
8586
8687const initiators = {
@@ -1169,6 +1170,10 @@ async function onBflKeyClick() {
11691170 return onApiKeyClick('BFL API Key:', SECRET_KEYS.BFL);
11701171}
11711172
1173+async function onFalaiKeyClick() {
1174+ return onApiKeyClick('FALAI API Key:', SECRET_KEYS.FALAI);
1175+}
1176+
11721177function onBflUpsamplingInput() {
11731178 extension_settings.sd.bfl_upsampling = !!$('#sd_bfl_upsampling').prop('checked');
11741179 saveSettingsDebounced();
@@ -1707,6 +1712,9 @@ async function loadModels() {
17071712 case sources.bfl:
17081713 models = await loadBflModels();
17091714 break;
1715+ case sources.falai:
1716+ models = await loadFalaiModels();
1717+ break;
17101718 }
17111719
17121720 for (const model of models) {
@@ -1744,6 +1752,21 @@ async function loadBflModels() {
17441752 ];
17451753}
17461754
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+
17471770async function loadPollinationsModels() {
17481771 const result = await fetch('/api/sd/pollinations/models', {
17491772 method: 'POST',
@@ -2081,6 +2104,9 @@ async function loadSchedulers() {
20812104 case sources.bfl:
20822105 schedulers = ['N/A'];
20832106 break;
2107+ case sources.falai:
2108+ schedulers = ['N/A'];
2109+ break;
20842110 }
20852111
20862112 for (const scheduler of schedulers) {
@@ -2735,6 +2761,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
27352761 case sources.bfl:
27362762 result = await generateBflImage(prefixedPrompt, signal);
27372763 break;
2764+ case sources.falai:
2765+ result = await generateFalaiImage(prefixedPrompt, negativePrompt, signal);
2766+ break;
27382767 }
27392768
27402769 if (!result.data) {
@@ -3496,6 +3525,39 @@ async function generateBflImage(prompt, signal) {
34963525 }
34973526}
34983527
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+
34993561async function onComfyOpenWorkflowEditorClick() {
35003562 let workflow = await (await fetch('/api/sd/comfy/workflow', {
35013563 method: 'POST',
@@ -3782,6 +3844,8 @@ function isValidState() {
37823844 return secret_state[SECRET_KEYS.NANOGPT];
37833845 case sources.bfl:
37843846 return secret_state[SECRET_KEYS.BFL];
3847+ case sources.falai:
3848+ return secret_state[SECRET_KEYS.FALAI];
37853849 }
37863850}
37873851
@@ -4443,6 +4507,7 @@ jQuery(async () => {
44434507 $('#sd_function_tool').on('input', onFunctionToolInput);
44444508 $('#sd_bfl_key').on('click', onBflKeyClick);
44454509 $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput);
4510+ $('#sd_falai_key').on('click', onFalaiKeyClick);
44464511
44474512 if (!CSS.supports('field-sizing', 'content')) {
44484513 $('.sd_settings .inline-drawer-toggle').on('click', function () {
public/scripts/extensions/stable-diffusion/settings.html+15 -0
@@ -52,6 +52,7 @@
5252 <option value="auto">Stable Diffusion Web UI (AUTOMATIC1111)</option>
5353 <option value="horde">Stable Horde</option>
5454 <option value="togetherai">TogetherAI</option>
55+ <option value="falai">FAL.AI</option>
5556 </select>
5657 <div data-sd-source="auto">
5758 <label for="sd_auto_url">SD Web UI URL</label>
@@ -256,6 +257,20 @@
256257 </label>
257258 </div>
258259
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+
259274 <div class="flex-container">
260275 <div class="flex1">
261276 <label for="sd_model" data-i18n="Model">Model</label>
public/scripts/secrets.js+1 -0
@@ -41,6 +41,7 @@ export const SECRET_KEYS = {
4141 GENERIC: 'api_key_generic',
4242 DEEPSEEK: 'api_key_deepseek',
4343 SERPER: 'api_key_serper',
44+ FALAI: 'api_key_falai',
4445};
4546
4647const INPUT_MAP = {
src/endpoints/secrets.js+1 -0
@@ -50,6 +50,7 @@ export const SECRET_KEYS = {
5050 TAVILY: 'api_key_tavily',
5151 NANOGPT: 'api_key_nanogpt',
5252 BFL: 'api_key_bfl',
53+ FALAI: 'api_key_falai',
5354 GENERIC: 'api_key_generic',
5455 DEEPSEEK: 'api_key_deepseek',
5556 SERPER: 'api_key_serper',
src/endpoints/stable-diffusion.js+120 -0
@@ -1228,6 +1228,125 @@ bfl.post('/generate', jsonParser, async (request, response) => {
12281228 }
12291229});
12301230
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+
12311350router.use('/comfy', comfy);
12321351router.use('/together', together);
12331352router.use('/drawthings', drawthings);
@@ -1237,3 +1356,4 @@ router.use('/blockentropy', blockentropy);
12371356router.use('/huggingface', huggingface);
12381357router.use('/nanogpt', nanogpt);
12391358router.use('/bfl', bfl);
1359+router.use('/falai', falai);