Merge pull request #3476 from Dakraid/feature/fal-integration Feature: FAL.AI Integration

5477586ce46a99039bf5bda329ffff596e9b0a9a

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

Signed
5 files changed, +210 -0Ignore whitespace
public/scripts/extensions/stable-diffusion/index.js+67 -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();
@@ -1299,6 +1304,7 @@ async function onModelChange() {
12991304 sources.huggingface,
13001305 sources.nanogpt,
13011306 sources.bfl,
1307+ sources.falai,
13021308 ];
13031309
13041310 if (cloudSources.includes(extension_settings.sd.source)) {
@@ -1707,6 +1713,9 @@ async function loadModels() {
17071713 case sources.bfl:
17081714 models = await loadBflModels();
17091715 break;
1716+ case sources.falai:
1717+ models = await loadFalaiModels();
1718+ break;
17101719 }
17111720
17121721 for (const model of models) {
@@ -1744,6 +1753,21 @@ async function loadBflModels() {
17441753 ];
17451754}
17461755
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+
17471771async function loadPollinationsModels() {
17481772 const result = await fetch('/api/sd/pollinations/models', {
17491773 method: 'POST',
@@ -2081,6 +2105,9 @@ async function loadSchedulers() {
20812105 case sources.bfl:
20822106 schedulers = ['N/A'];
20832107 break;
2108+ case sources.falai:
2109+ schedulers = ['N/A'];
2110+ break;
20842111 }
20852112
20862113 for (const scheduler of schedulers) {
@@ -2735,6 +2762,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
27352762 case sources.bfl:
27362763 result = await generateBflImage(prefixedPrompt, signal);
27372764 break;
2765+ case sources.falai:
2766+ result = await generateFalaiImage(prefixedPrompt, negativePrompt, signal);
2767+ break;
27382768 }
27392769
27402770 if (!result.data) {
@@ -3496,6 +3526,40 @@ async function generateBflImage(prompt, signal) {
34963526 }
34973527}
34983528
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+
34993563async function onComfyOpenWorkflowEditorClick() {
35003564 let workflow = await (await fetch('/api/sd/comfy/workflow', {
35013565 method: 'POST',
@@ -3782,6 +3846,8 @@ function isValidState() {
37823846 return secret_state[SECRET_KEYS.NANOGPT];
37833847 case sources.bfl:
37843848 return secret_state[SECRET_KEYS.BFL];
3849+ case sources.falai:
3850+ return secret_state[SECRET_KEYS.FALAI];
37853851 }
37863852}
37873853
@@ -4443,6 +4509,7 @@ jQuery(async () => {
44434509 $('#sd_function_tool').on('input', onFunctionToolInput);
44444510 $('#sd_bfl_key').on('click', onBflKeyClick);
44454511 $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput);
4512+ $('#sd_falai_key').on('click', onFalaiKeyClick);
44464513
44474514 if (!CSS.supports('field-sizing', 'content')) {
44484515 $('.sd_settings .inline-drawer-toggle').on('click', function () {
public/scripts/extensions/stable-diffusion/settings.html+15 -0
@@ -42,6 +42,7 @@
4242 <option value="comfy">ComfyUI</option>
4343 <option value="drawthings">DrawThings HTTP API</option>
4444 <option value="extras">Extras API (deprecated)</option>
45+ <option value="falai">FAL.AI</option>
4546 <option value="huggingface">HuggingFace Inference API (serverless)</option>
4647 <option value="nanogpt">NanoGPT</option>
4748 <option value="novel">NovelAI Diffusion</option>
@@ -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+126 -0
@@ -1228,6 +1228,131 @@ 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+ .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+
12311356router.use('/comfy', comfy);
12321357router.use('/together', together);
12331358router.use('/drawthings', drawthings);
@@ -1237,3 +1362,4 @@ router.use('/blockentropy', blockentropy);
12371362router.use('/huggingface', huggingface);
12381363router.use('/nanogpt', nanogpt);
12391364router.use('/bfl', bfl);
1365+router.use('/falai', falai);