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 -0Showing whitespace changes
public/scripts/extensions/stable-diffusion/index.js+67 -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();
@@ -1299,6 +1304,7 @@ async function onModelChange() {
1299 sources.huggingface,1304 sources.huggingface,
1300 sources.nanogpt,1305 sources.nanogpt,
1301 sources.bfl,1306 sources.bfl,
1307 sources.falai,
1302 ];1308 ];
13031309
1304 if (cloudSources.includes(extension_settings.sd.source)) {1310 if (cloudSources.includes(extension_settings.sd.source)) {
@@ -1707,6 +1713,9 @@ async function loadModels() {
1707 case sources.bfl:1713 case sources.bfl:
1708 models = await loadBflModels();1714 models = await loadBflModels();
1709 break;1715 break;
1716 case sources.falai:
1717 models = await loadFalaiModels();
1718 break;
1710 }1719 }
17111720
1712 for (const model of models) {1721 for (const model of models) {
@@ -1744,6 +1753,21 @@ async function loadBflModels() {
1744 ];1753 ];
1745}1754}
17461755
1756async 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
1747async function loadPollinationsModels() {1771async function loadPollinationsModels() {
1748 const result = await fetch('/api/sd/pollinations/models', {1772 const result = await fetch('/api/sd/pollinations/models', {
1749 method: 'POST',1773 method: 'POST',
@@ -2081,6 +2105,9 @@ async function loadSchedulers() {
2081 case sources.bfl:2105 case sources.bfl:
2082 schedulers = ['N/A'];2106 schedulers = ['N/A'];
2083 break;2107 break;
2108 case sources.falai:
2109 schedulers = ['N/A'];
2110 break;
2084 }2111 }
20852112
2086 for (const scheduler of schedulers) {2113 for (const scheduler of schedulers) {
@@ -2735,6 +2762,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
2735 case sources.bfl:2762 case sources.bfl:
2736 result = await generateBflImage(prefixedPrompt, signal);2763 result = await generateBflImage(prefixedPrompt, signal);
2737 break;2764 break;
2765 case sources.falai:
2766 result = await generateFalaiImage(prefixedPrompt, negativePrompt, signal);
2767 break;
2738 }2768 }
27392769
2740 if (!result.data) {2770 if (!result.data) {
@@ -3496,6 +3526,40 @@ async function generateBflImage(prompt, signal) {
3496 }3526 }
3497}3527}
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 */
3536async 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
3499async function onComfyOpenWorkflowEditorClick() {3563async function onComfyOpenWorkflowEditorClick() {
3500 let workflow = await (await fetch('/api/sd/comfy/workflow', {3564 let workflow = await (await fetch('/api/sd/comfy/workflow', {
3501 method: 'POST',3565 method: 'POST',
@@ -3782,6 +3846,8 @@ function isValidState() {
3782 return secret_state[SECRET_KEYS.NANOGPT];3846 return secret_state[SECRET_KEYS.NANOGPT];
3783 case sources.bfl:3847 case sources.bfl:
3784 return secret_state[SECRET_KEYS.BFL];3848 return secret_state[SECRET_KEYS.BFL];
3849 case sources.falai:
3850 return secret_state[SECRET_KEYS.FALAI];
3785 }3851 }
3786}3852}
37873853
@@ -4443,6 +4509,7 @@ jQuery(async () => {
4443 $('#sd_function_tool').on('input', onFunctionToolInput);4509 $('#sd_function_tool').on('input', onFunctionToolInput);
4444 $('#sd_bfl_key').on('click', onBflKeyClick);4510 $('#sd_bfl_key').on('click', onBflKeyClick);
4445 $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput);4511 $('#sd_bfl_upsampling').on('input', onBflUpsamplingInput);
4512 $('#sd_falai_key').on('click', onFalaiKeyClick);
44464513
4447 if (!CSS.supports('field-sizing', 'content')) {4514 if (!CSS.supports('field-sizing', 'content')) {
4448 $('.sd_settings .inline-drawer-toggle').on('click', function () {4515 $('.sd_settings .inline-drawer-toggle').on('click', function () {
public/scripts/extensions/stable-diffusion/settings.html+15 -0
@@ -42,6 +42,7 @@
42 <option value="comfy">ComfyUI</option>42 <option value="comfy">ComfyUI</option>
43 <option value="drawthings">DrawThings HTTP API</option>43 <option value="drawthings">DrawThings HTTP API</option>
44 <option value="extras">Extras API (deprecated)</option>44 <option value="extras">Extras API (deprecated)</option>
45 <option value="falai">FAL.AI</option>
45 <option value="huggingface">HuggingFace Inference API (serverless)</option>46 <option value="huggingface">HuggingFace Inference API (serverless)</option>
46 <option value="nanogpt">NanoGPT</option>47 <option value="nanogpt">NanoGPT</option>
47 <option value="novel">NovelAI Diffusion</option>48 <option value="novel">NovelAI Diffusion</option>
@@ -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+126 -0
@@ -1228,6 +1228,131 @@ 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 .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
1263falai.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
1231router.use('/comfy', comfy);1356router.use('/comfy', comfy);
1232router.use('/together', together);1357router.use('/together', together);
1233router.use('/drawthings', drawthings);1358router.use('/drawthings', drawthings);
@@ -1237,3 +1362,4 @@ router.use('/blockentropy', blockentropy);
1237router.use('/huggingface', huggingface);1362router.use('/huggingface', huggingface);
1238router.use('/nanogpt', nanogpt);1363router.use('/nanogpt', nanogpt);
1239router.use('/bfl', bfl);1364router.use('/bfl', bfl);
1365router.use('/falai', falai);