/*
Copyright 2022 juenbug12851
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @file
* @brief Web UI entry point. Express 5 + Pug app on port 7861; serves the pages, the JSON API, and the output/ images, and spawns the CLI for any actual generation. Notes: notes/systems/server.md.
*/
console.log("Starting app...");
import _ from "lodash";
import fs from "node:fs";
import path from "node:path";
import { exec, execSync } from "node:child_process";
import { promisify } from "node:util";
import express from "express";
import open from "open";
import common from "./common.js";
import imageIndex from "./web/backend/indexImages.js";
import saveApng from "./helpers/saveApng.js";
import promptFiles from "./promptFilesAndSuggestions.js";
const { settings, defSettings, reloadSettings, saveSettings, replaceSettings, userSettings } =
common;
const execPromise = promisify(exec);
promptFiles.init(settings);
promptFiles.loadAll();
// Rebuild indexes
imageIndex.rebuildIndexes(settings());
console.log("Starting server...");
const app = express();
// Use pug as the template engine
app.set("view engine", "pug");
app.set("views", settings().serverSettings.webFolder + "/views");
// Use the body-parser middleware to parse incoming request bodies
app.use(express.json());
/**
* Open a folder in the OS file explorer (cross-platform).
* @param {string} dirPath The directory to open.
* @returns {(Buffer|undefined)} The execSync result, or undefined on (often benign) error.
*/
function dirOpen(dirPath) {
dirPath = path.resolve(dirPath);
let command = "";
switch (process.platform) {
case "darwin":
command = "open";
break;
case "win32":
command = "explorer";
break;
default:
command = "xdg-open";
break;
}
try {
return execSync(`${command} "${dirPath}"`);
} catch (err) {
// Weirdly, it often fails sauccesfully??? Disable error if it's successful
// console.error(err);
}
}
// Executes index.js with with currently set args
// await exec();
let args = {};
let execAppOngoing = false;
let execMagickOngoing = false;
/**
* Run an ImageMagick (`magick`) command, auto-quoting args that need it.
* @param {object} [args] Flag→value map (rendered as `-key value`).
* @param {string[]} [fileNames] Input filenames.
* @param {string} [output] Output filename.
* @param {boolean} [silent] Suppress logging.
* @returns {Promise<object>} The exec result (`{stdout, stderr}` on success, or `{error}`).
*/
async function execMagick(args, fileNames, output, silent) {
const nodeExecutable = `magick`;
const commandArgs = [nodeExecutable];
execMagickOngoing = true;
let ret = {};
// Add arguments "-key value"
if (args != undefined) {
for (const [key, value] of Object.entries(args)) {
commandArgs.push(`-${key}`);
// Seems it's a bit more complicated than just spaces, especially for powershell
// If it has any characters that warrant quotes auto-enclose in quotes
if (value !== undefined && value !== true) {
if (typeof value === "string" && /[^a-z0-9\-.,]/gi.test(value))
commandArgs.push(`"${value}"`);
else commandArgs.push(value);
}
}
}
// Add input filenames
if (fileNames != undefined) {
for (let i = 0; i < fileNames.length; i++) commandArgs.push(fileNames[i]);
}
// Add output filename
if (output != undefined) commandArgs.push(output);
// Log cmd used
let logCmd = commandArgs.join(" ");
if (!silent) console.log(logCmd);
try {
const { stdout, stderr } = await execPromise(commandArgs.join(" "));
ret = { stdout, stderr };
} catch (error) {
ret = { error };
if (!silent) console.error(`exec error: ${error}`);
}
execMagickOngoing = false;
return ret;
}
/**
* Spawn the CLI (`node . --flags`) with the current `args` to perform the actual
* generation, tracking `execAppOngoing`.
* @returns {Promise<object>} The exec result (`{stdout, stderr}` on success, or `{error}`).
*/
async function execApp() {
const command = ".";
const nodeExecutable = `"${process.argv[0]}"`;
const commandArgs = [nodeExecutable, command];
execAppOngoing = true;
let ret = {};
for (const [key, value] of Object.entries(args)) {
commandArgs.push(`--${key}`);
// Seems it's a bit more complicated than just spaces, especially for powershell
// If it has any characters that warrant quotes auto-enclose in quotes
if (value !== undefined && value !== true) {
if (typeof value === "string" && /[^a-z0-9\-.,]/gi.test(value))
commandArgs.push(`"${value}"`);
else commandArgs.push(value);
}
}
// Log cmd used
let logCmd = commandArgs.join(" ");
logCmd = logCmd.replace(nodeExecutable, "node");
console.log(logCmd);
try {
const { stdout, stderr } = await execPromise(commandArgs.join(" "));
ret = { stdout, stderr };
} catch (error) {
ret = { error };
console.error(`exec error: ${error}`);
}
execAppOngoing = false;
return ret;
}
/**
* Fetch generation progress from the CLI's progress server (port `portProgress`).
* @returns {Promise<(object|undefined)>} The progress payload, or undefined if unreachable.
*/
async function getProgressRequest() {
const url = `http://localhost:${settings().serverSettings.portProgress}/api/images/progress`;
// Send response
try {
const response = await fetch(
`http://localhost:${settings().serverSettings.portProgress}/api/images/progress`,
);
return await response.json();
} catch (err) {}
return undefined;
}
/**
* Get generation progress (from the CLI progress server, or a stopped-state default),
* with the server's own `execOngoing` flag merged in.
* @returns {Promise<object>} The progress payload.
*/
async function getProgress() {
let ret;
try {
ret = await getProgressRequest();
} catch (error) {}
if (ret == undefined) {
ret = {
// Progress on-going or not
progressOngoing: false,
// Image Progress
progressCurStep: 1,
progressTotalSteps: 1,
// Image Batch Progress
progressCurImg: 1,
progressTotalImg: 1,
// Image Total Progress
progressPercent: 1,
progressEta: "--s",
// Prompts Progress
progressCurPrompt: 1,
progressTotalPrompts: 1,
};
}
// Whether the actual request is done
ret.execOngoing = execAppOngoing;
return ret;
}
// Announce API is ready
app.listen(settings().serverSettings.port, () => {
console.log(`Done!`);
console.log(`Visit http://localhost:${settings().serverSettings.port}`);
// Auto-opens browser
open(`http://localhost:${settings().serverSettings.port}`);
});
// Linkup images to the output folder
app.use("/images", express.static(settings().imageSettings.saveTo));
// Add program icons to root for browser access
app.use("/", express.static("./assets/icons"));
// Make all assets accessible
app.use("/assets", express.static("./assets"));
// Linkup web content to the web folder
app.use(express.static(settings().serverSettings.webFolder + "/frontend"));
// API Requests
// These respond in JSON and sometimes require JSON input
app.get("/", (req, res) => {
res.render("feed");
});
app.get("/single", (req, res) => {
res.render("single");
});
app.get("/re-index", (req, res) => {
res.render("re-index");
});
app.get("/progress", (req, res) => {
res.render("progress");
});
app.get("/upscale-progress", (req, res) => {
res.render("upscale-progress");
});
app.get("/results", (req, res) => {
res.render("results");
});
app.get("/settings", (req, res) => {
res.render("settings");
});
app.get("/generate", (req, res) => {
res.render("generate");
});
app.get("/regen-anim", (req, res) => {
res.render("regen-anim");
});
app.get("/download/:file", (req, res) => {
res.download(`./${settings().imageSettings.saveTo}/${req.params.file}`);
});
app.get("/api/images/delete/:filename", (req, res) => {
// Delete file
try {
fs.unlinkSync(`./${settings().imageSettings.saveTo}/${req.params.filename}.png`);
fs.unlinkSync(`./${settings().imageSettings.saveTo}/${req.params.filename}.json`);
} catch (err) {
console.error(err);
}
// Mark as success
res.jsonp("success");
});
app.get("/api/animation/delete/:fileId", (req, res) => {
// Get image name
const imageName = req.params.fileId;
// Get image data
const imageData = _.cloneDeep(imageIndex.getFiles()[imageName]);
// Make sure image exists in index
if (imageData === undefined) {
res.jsonp({});
console.error("Error: API requested a non-indexed image");
return;
}
// Make sure it has at least 1 animation frame
if (imageData.animationFrames == undefined || imageData.animationFrames.length == 0) {
res.jsonp({});
console.error("Error: API requested to remove animation frames from an image with no frames");
return;
}
// Go through all frames
for (let i = 0; i < imageData.animationFrames.length; i++) {
// Get file name
const file = imageData.animationFrames[i].name;
// Delete file
try {
fs.unlinkSync(`./${settings().imageSettings.saveTo}/${file}.png`);
fs.unlinkSync(`./${settings().imageSettings.saveTo}/${file}.json`);
} catch (err) {
console.error(err);
}
}
// Mark as success
res.jsonp("success");
});
app.get("/api/animation/externalize/:fileId", (req, res) => {
// Get image name
const imageName = req.params.fileId;
// Get image data
const imageData = _.cloneDeep(imageIndex.getFiles()[imageName]);
// Make sure image exists in index
if (imageData === undefined) {
res.jsonp({});
console.error("Error: API requested a non-indexed image");
return;
}
// Make sure it has at least 1 animation frame
if (imageData.animationFrames == undefined || imageData.animationFrames.length == 0) {
res.jsonp({});
console.error(
"Error: API requested to prepare an animation for ai interpolation from an image with no frames",
);
return;
}
const baseFolderPath = `./${settings().imageSettings.saveTo}/external-${imageName}`;
// Make directory
try {
fs.mkdirSync(`${baseFolderPath}`);
} catch (err) {}
try {
fs.mkdirSync(`${baseFolderPath}/frames`);
} catch (err) {}
// Calculate info
const delayMs = +settings().imageSettings.animationDelay;
const frames = imageData.animationFrames.length;
const delayS = +(delayMs / 1000).toFixed(2);
const length = frames * delayS;
const fps = +(1 / delayS).toFixed(0);
let factor = +(60 / fps).toFixed(0);
// Write info to file
try {
fs.writeFileSync(
`${baseFolderPath}/info.txt`,
`Frame Delay: ${delayMs}ms (${delayS}s)
Total Frames: ${frames}
Animation Length: ${length}s
FPS: ${fps}
Target FPS: 60
Multiplier/Speed Factor to get near 60FPS: ${factor}`,
);
} catch (err) {
console.error(err);
}
// Go through all frames
for (let i = 0; i < imageData.animationFrames.length; i++) {
// Get from file name
const fromFilename = `${imageData.animationFrames[i].name}.png`;
// Get file name
// 00001.png, 00002.png, 00003.png, etc...
const toFilename = String(i + 1).padStart(5, "0") + ".png";
// Copy file
try {
fs.copyFileSync(
`./${settings().imageSettings.saveTo}/${fromFilename}`,
`${baseFolderPath}/frames/${toFilename}`,
);
} catch (err) {
console.error(err);
}
}
// Copy animation over as well
try {
fs.copyFileSync(
`./${settings().imageSettings.saveTo}/${imageName}.png`,
`${baseFolderPath}/animation.png`,
);
} catch (err) {
console.error(err);
}
// Open folder
dirOpen(baseFolderPath);
// Mark as success
res.jsonp("success");
});
app.get("/api/upscales/delete/:fileId", (req, res) => {
// Get image name
const imageName = req.params.fileId;
// Get image data
const imageData = _.cloneDeep(imageIndex.getFiles()[imageName]);
// Make sure image exists in index
if (imageData === undefined) {
res.jsonp({});
console.error("Error: API requested a non-indexed image");
return;
}
// Make sure it has at least 1 upscale
if (imageData.upscales == undefined || imageData.upscales.length == 0) {
res.jsonp({});
console.error("Error: API requested to remove upscales from an image with no upscales");
return;
}
// Go through all upscales
for (let i = 0; i < imageData.upscales.length; i++) {
// Get file name and remove fake path and extension
let file = imageData.upscales[i];
file = file.replaceAll("/images/", "");
file = file.replaceAll(".png", "");
// Delete file
try {
fs.unlinkSync(`./${settings().imageSettings.saveTo}/${file}.png`);
fs.unlinkSync(`./${settings().imageSettings.saveTo}/${file}.json`);
} catch (err) {
console.error(err);
}
}
// Mark as success
res.jsonp("success");
});
app.get("/api/images/query", async function (req, res) {
// Get query
const query = req.query;
if (query.query == undefined) {
res.jsonp([]);
console.error("Client sent a query request with no query attached");
return;
}
// Do a query
const results = imageIndex.query(query.query);
res.jsonp(results);
});
// Gets a random image name
app.get("/api/images/random-name", async function (req, res) {
const file = _.sample(imageIndex.getFiles());
if (file == undefined) {
res.jsonp("");
return;
}
res.jsonp(_.sample(imageIndex.getFiles()).name);
});
app.get("/api/images/single/:name", async function (req, res) {
// Get image name
const imageName = req.params.name;
// Get image data
const imageData = _.cloneDeep(imageIndex.getFiles()[imageName]);
// Make sure image exists in index
if (imageData === undefined) {
res.jsonp({});
console.error("Error: API requested a non-indexed image");
return;
}
// Get index stats
const stats = imageIndex.getIndexStats();
// Get max number to compare keyword popularity percent to
const keywordTotalCount = stats._total.highestKeywordCount;
// Get image keyword list and sort it alphabetically
const imageKeywords = _.sortBy(imageData.keywords);
// Begin working on the keyword cloud
const keywordCloud = [];
for (let i = 0; i < imageKeywords.length; i++) {
// Get keyword
const imageKeyword = imageKeywords[i];
// Get count
let keywordStats = stats[imageKeyword];
if (keywordStats == undefined) continue;
keywordStats = keywordStats.count;
// Calc percent
const keywordPercent = keywordStats / keywordTotalCount;
// Push to keyword cloud
keywordCloud.push({
keyword: imageKeyword,
count: keywordStats,
percent: keywordPercent,
});
}
// Save to object
imageData.keywordCloud = keywordCloud;
// Send object back
res.jsonp(imageData);
});
// Returns all images shuffled randomly
app.get("/api/images/feed", async function (req, res) {
res.jsonp(_.shuffle(_.uniqBy(_.values(imageIndex.getFiles()), "imgPath")));
});
// Rebuilds index
app.get("/api/images/re-index", async function (req, res) {
imageIndex.rebuildIndexes(settings());
res.jsonp("success");
});
// Returns all images unshuffled
app.get("/api/images/files", async function (req, res) {
res.jsonp(imageIndex.getFiles());
});
// Returns all keywords unshuffled
app.get("/api/images/index", async function (req, res) {
res.jsonp(imageIndex.getIndex());
});
app.get("/api/images/stats", async function (req, res) {
res.jsonp(imageIndex.getIndexStats());
});
// Returns random keyword suggestions with 1 to 3 keywords
app.get("/api/images/search-suggestion", async function (req, res) {
// Whether to pull from a file 1-3 keywords or 1 random keyword
const fromFile = _.random(0.0, 1.0, true) < 0.5;
// How many to pull (if from file)
const count = _.random(1, 3, false);
if (fromFile) {
const file = _.sample(_.values(imageIndex.getFiles()));
if (file == undefined) {
res.jsonp("Please make some images by clicking new...");
return;
}
const keywords = _.sampleSize(file.keywords, count);
res.jsonp(keywords.join(" "));
} else {
const keyword = _.sample(_.keys(imageIndex.getIndex()));
if (keyword == undefined) {
res.jsonp("Please make some images by clicking new...");
return;
}
res.jsonp(keyword);
}
});
app.get("/api/images/random-keywords", async function (req, res) {
// Get up to 25 random keywords
const keywords = _.sampleSize(_.keys(imageIndex.getIndex()), 50);
res.jsonp(keywords);
});
app.get("/api/images/progress", async function (req, res) {
const progress = await getProgress();
res.jsonp(progress);
});
app.get("/api/images/reindex-progress", async function (req, res) {
res.jsonp(imageIndex.getProgress());
});
app.get("/api/progress-results", async function (req, res) {
// get results
let results = {};
try {
results = JSON.parse(fs.readFileSync("./results.json").toString());
} catch (err) {
console.error(err);
}
// Breakdown results into images and prompts
const images = results.images;
const prompts = results.prompts;
// Array to send to client
let ret = {
images: [],
prompts: prompts ? prompts : [],
};
// Convert image name array to an image url array for the web ui
if (images != undefined) {
for (let i = 0; i < images.length; i++) {
// get path to png file
ret.images.push(`/images/${images[i]}.png`);
}
}
res.jsonp(ret);
});
app.get("/api/results", async function (req, res) {
try {
// get results
const results = JSON.parse(fs.readFileSync("./results.json").toString());
// Breakdown results into images and prompts
const images = results.images;
const prompts = results.prompts;
// Array to send to client
let ret = {
images: [],
prompts: prompts ? prompts : [],
};
if (images != undefined) {
for (let i = 0; i < images.length; i++) {
// Prepare object to be pushed into array
const obj = {};
// get path to png file
obj.image = `/images/${images[i]}.png`;
// Save reference to index data and index link if it exists
const data = imageIndex.getFiles()[images[i]];
if (data != undefined) {
obj.data = data;
obj.link = `/single?name=${images[i]}`;
}
// Otherwise attempt to assume its an upscale and get link that way
else {
try {
const json = JSON.parse(
fs.readFileSync(`${settings().imageSettings.saveTo}/${images[i]}.json`, "utf8"),
);
if (json.upscaleOf) {
obj.data = imageIndex.getFiles()[json.upscaleOf];
obj.link = `/single?name=${json.upscaleOf}`;
}
} catch (err) {}
}
// Save it
ret.images.push(obj);
}
}
res.jsonp(ret);
return;
} catch (err) {}
res.jsonp({
images: [],
prompts: [],
});
});
app.get("/api/ping", (req, res) => {
res.jsonp("pong");
});
// Single setting
// app.get('/api/setting/:path', (req, res) => {
// const setting = _.at(settings(), req.params.path);
// if(setting == null || setting == undefined || setting.length == 0) {
// res.jsonp(null);
// return;
// }
// res.jsonp(setting[0]);
// });
app.post("/api/setting", (req, res) => {
// Verify the body contains an object with a kery of value and a value of anything but undefined
if (
req.body == null ||
req.body == undefined ||
req.body.value == undefined ||
req.body.path == undefined
) {
console.error("Posting a setting needs to be done in {value: ..., path: ...}");
res.jsonp(null);
return;
}
_.set(settings(), req.body.path, req.body.value);
saveSettings();
res.jsonp("success");
});
// Get settings
app.get("/api/settings", (req, res) => {
res.jsonp(settings());
});
app.get("/api/default-settings", (req, res) => {
res.jsonp(defSettings());
});
app.get("/api/user-settings", (req, res) => {
res.jsonp(userSettings());
});
// Reload settings
app.get("/api/reload-settings", (req, res) => {
reloadSettings();
res.jsonp("success");
});
// Save settings
app.get("/api/save-settings", (req, res) => {
saveSettings();
res.jsonp("success");
});
app.post("/api/expansion/save", (req, res) => {
// Verify the body contains an object
if (req.body == null || req.body == undefined) {
console.error("Can't save expansion without json body");
res.jsonp(null);
return;
}
fs.writeFileSync(
`${settings().settings.expansionFiles}/${req.body.fileName}.txt`,
req.body.prompt,
);
res.jsonp("Success!");
});
app.post("/api/preset/save", (req, res) => {
// Verify the body contains an object
if (req.body == null || req.body == undefined) {
console.error("Can't save preset without json body");
res.jsonp(null);
return;
}
fs.writeFileSync(
`${settings().settings.presetFiles}/${req.body.fileName}.json`,
JSON.stringify(req.body.presetObj, null, 4),
);
res.jsonp("Success!");
});
// Put settings
app.post("/api/replace-settings", (req, res) => {
// Change out settings
replaceSettings(req.body);
// Save settings
saveSettings();
res.jsonp("success");
});
// Put settings
app.post("/api/merge-settings", (req, res) => {
// Merge settings
_.merge(settings(), req.body);
// Save settings
saveSettings();
res.jsonp("success");
});
// Make file variations
app.get("/api/file-variation/:fileId", async (req, res) => {
args = {
"file-variations": req.params.fileId,
};
// Run file variatons
await execApp();
res.jsonp("success");
});
// Upscale existing
app.get("/api/upscale-file/:fileId", async (req, res) => {
args = {
"upscale-file": req.params.fileId,
};
// Run file variatons
await execApp();
res.jsonp("success");
});
// Reroll existing
app.get("/api/reroll-file/:fileId/:field", async (req, res) => {
args = {
"reroll-file": req.params.fileId,
"reroll-field": req.params.field,
};
// Run file variatons
await execApp();
res.jsonp("success");
});
// Make file variations
app.get("/api/file-update-animation/:fileId", async (req, res) => {
// Get image name
const imageName = req.params.fileId;
// Get image data
const imageData = _.cloneDeep(imageIndex.getFiles()[imageName]);
// Make sure image exists in index
if (imageData === undefined) {
res.jsonp({});
console.error("Error: API requested a non-indexed image");
return;
}
// Make sure it has at least 1 animation frame
if (imageData.animationFrames == undefined || imageData.animationFrames.length == 0) {
res.jsonp({});
console.error("Error: API requested to update an image with no frames");
return;
}
// Convert to filename list
const files = [];
for (let i = 0; i < imageData.animationFrames.length; i++) {
files.push(imageData.animationFrames[i].name);
}
// Set animation of
settings().imageSettings.animationOf = imageName;
// Re-update animated image and skip writing json file
saveApng(files, settings().imageSettings, true);
// Clear animation of
delete settings().imageSettings.animationOf;
// Return
res.jsonp("success");
});
// Do normal generation
app.get("/api/generate", async (req, res) => {
// Run file variatons
await execApp();
res.jsonp("success");
});
app.post("/api/generate", async (req, res) => {
if (
req.body != null &&
req.body != undefined &&
req.body.value != undefined &&
req.body.value != ""
)
args.prompt = req.body.value;
// Run file variatons
await execApp();
res.jsonp("success");
});
app.post("/api/generate-full", async (req, res) => {
if (req.body != null && req.body != undefined) args = req.body;
// Run file variatons
await execApp();
res.jsonp("success");
});
app.get("/api/magick-installed", async (req, res) => {
// Run file variatons
const ret = await execMagick(
{
version: undefined,
},
undefined,
undefined,
true,
);
const isInstalled = ret.stdout != undefined && ret.stdout.length > 0;
res.jsonp(isInstalled);
});
// Use Image Magick to convert animation to another animation file
app.get("/api/magick-animation-convert/:fileId/:ext", async (req, res) => {
// Get image name
const imageName = req.params.fileId;
// Get new extension
const newExt = req.params.ext;
// Get image data
const imageData = _.cloneDeep(imageIndex.getFiles()[imageName]);
// Make sure image exists in index
if (imageData === undefined) {
res.jsonp({});
console.error("Error: API requested a non-indexed image");
return;
}
// Make sure it has at least 1 animation frame
if (imageData.animationFrames == undefined || imageData.animationFrames.length == 0) {
res.jsonp({});
console.error("Error: API requested to convert an animation with no frames");
return;
}
// Convert to filename list
const files = [];
for (let i = 0; i < imageData.animationFrames.length; i++) {
files.push(`./${settings().imageSettings.saveTo}/${imageData.animationFrames[i].name}.png`);
}
// Run file variatons
await execMagick(
{
delay: +(settings().imageSettings.animationDelay / 10).toFixed(0),
},
files,
`./${settings().imageSettings.saveTo}/${imageName}.${newExt}`,
);
// Return
res.download(`./${settings().imageSettings.saveTo}/${imageName}.${newExt}`);
// Auto-remove after some time
setTimeout(function () {
fs.unlinkSync(`./${settings().imageSettings.saveTo}/${imageName}.${newExt}`);
}, 5 * 1000);
});
// Use Image Magick to convert image to another image file
app.get("/api/magick-image-convert/:fileId/:ext", async (req, res) => {
// Get image name
const imageName = req.params.fileId;
// Get new extension
const newExt = req.params.ext;
// Get image data
const imageData = _.cloneDeep(imageIndex.getFiles()[imageName]);
// Make sure image exists in index
if (imageData === undefined) {
res.jsonp({});
console.error("Error: API requested a non-indexed image");
return;
}
// Convert to filename list
const files = [`./${settings().imageSettings.saveTo}/${imageName}.png`];
// Run file variatons
await execMagick(undefined, files, `./${settings().imageSettings.saveTo}/${imageName}.${newExt}`);
// Return
res.download(`./${settings().imageSettings.saveTo}/${imageName}.${newExt}`);
// Auto-remove after some time
setTimeout(function () {
fs.unlinkSync(`./${settings().imageSettings.saveTo}/${imageName}.${newExt}`);
}, 5 * 1000);
});
app.get("/api/prompt-suggestion", (req, res) => {
res.jsonp(promptFiles.promptSuggestion());
});
app.get("/api/files/dynamic-prompts", (req, res) => {
res.jsonp(promptFiles.loadDynPromptList());
});
app.get("/api/files/expansions", (req, res) => {
res.jsonp(promptFiles.loadExpansionFileList());
});
app.get("/api/files/lists", (req, res) => {
// Refresh the catalog, then return the adult-aware picker view (NSFW names hidden
// when adult is off; explicit -sfw/-nsfw variants exposed when on).
promptFiles.loadListFileList();
res.jsonp(promptFiles.pickerListNames());
});
app.get("/api/files/presets", (req, res) => {
const files = fs.readdirSync(settings().settings.presetFiles);
const userFiles = [];
for (let i = 0; i < files.length; i++) {
// Get filename without suffix
const file = files[i].substr(0, files[i].lastIndexOf("."));
userFiles.push(file);
}
res.jsonp(userFiles);
});