Add support for FAL.AI as image gen provider

6e0ed8552ff01f1ce445d5d62c2b261fababd0f8

Kristan Schlikow <kristian@schlikow.de>

Signed
5 files changed, +202 -0Ignore whitespace
public/scripts/extensions/stable-diffusion/index.js+65 -0
@@ -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};
8586
86const initiators = {87const 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}
11711172
1173async function onFalaiKeyClick() {
1174 return onApiKeyClick('FALAI API Key:', SECRET_KEYS.FALAI);
1175}
1176
1172function onBflUpsamplingInput() {1177function 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 }
17111719
1712 for (const model of models) {1720 for (const model of models) {
@@ -1744,6 +1752,21 @@ async function loadBflModels() {
1744 ];1752 ];
1745}1753}
17461754
1755async 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
1747async function loadPollinationsModels() {1770async 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 }
20852111
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 }
27392768
2740 if (!result.data) {2769 if (!result.data) {
@@ -3496,6 +3525,39 @@ async function generateBflImage(prompt, signal) {
3496 }3525 }
3497}3526}
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 */
3535async 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
3499async function onComfyOpenWorkflowEditorClick() {3561async 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}
37873851
@@ -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);
44464511
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 () {
public/scripts/extensions/stable-diffusion/settings.html+15 -0
@@ -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>
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
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>
public/scripts/secrets.js+1 -0
@@ -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};
4546
46const INPUT_MAP = {47const INPUT_MAP = {
src/endpoints/secrets.js+1 -0
@@ -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',
src/endpoints/stable-diffusion.js+120 -0
@@ -1228,6 +1228,125 @@ bfl.post('/generate', jsonParser, async (request, response) => {
1228 }1228 }
1229});1229});
12301230
1231const falai = express.Router();
1232
1233falai.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
1262falai.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
1231router.use('/comfy', comfy);1350router.use('/comfy', comfy);
1232router.use('/together', together);1351router.use('/together', together);
1233router.use('/drawthings', drawthings);1352router.use('/drawthings', drawthings);
@@ -1237,3 +1356,4 @@ router.use('/blockentropy', blockentropy);
1237router.use('/huggingface', huggingface);1356router.use('/huggingface', huggingface);
1238router.use('/nanogpt', nanogpt);1357router.use('/nanogpt', nanogpt);
1239router.use('/bfl', bfl);1358router.use('/bfl', bfl);
1359router.use('/falai', falai);