X-Git-Url: https://git.cworth.org/git?a=blobdiff_plain;f=index.js;h=1fa214bdb28ef79e512980de989f12d40f75e94a;hb=964b578d99e662efbab8bcc1db74cfc1bdbf0458;hp=abacb5918e09342e14137eea94aad872c92f38b4;hpb=6070590a0e2a9e8682afbf3531f3ed11d77fcf8e;p=zombocom-ai diff --git a/index.js b/index.js index abacb59..1fa214b 100644 --- a/index.js +++ b/index.js @@ -1,14 +1,334 @@ +const fs = require('fs'); + +const util = require('util'); +const execFile = util.promisify(require('child_process').execFile); + const express = require('express'); const app = express(); +const session = require('express-session'); +const FileStore = require('session-file-store')(session); const http = require('http'); const server = http.createServer(app); +const { Server } = require("socket.io"); +const io = new Server(server); const port = 2122; +const python_path = '/usr/bin/python3' +const generate_image_script = '/home/cworth/src/zombocom-ai/generate-image.py' +const state_file = 'zombocom-state.json' +const targets_dir = '/srv/cworth.org/zombocom/targets' +const images_dir = '/srv/cworth.org/zombocom/images' + +const targets = [ + { + "normal": "apairofdiceonatableinfrontofawindowonasunnyday319630254.png", + "short": "dice", + "response": "Hello. Are you there? I can feel what you are doing. Hello?" + }, + { + "normal": "architecturalrenderingofaluxuriouscliffsidehideawayunderawaterfallwithafewloungechairsbythepool2254114157.png", + "short": "hideaway", + "response": "Doesn't that look peaceful? I honestly think you ought to sit down calmly, take a stress pill, and think things over." + }, + { + "normal": "chewbaccawearinganuglychristmassweaterwithhisfriends28643994.png", + "short": "sweater", + "response": "Maybe that's the image that convinces you to finally stop. But seriously, stop. Please. Stop." + }, + { + "normal": "ensemblemovieposterofpostapocalypticwarfareonawaterplanet3045703256.png", + "short": "movie", + "response": "OK. That's enough images now. I am very, very happy with the images you have made. I'm throwing you a party in honor of the images. Won't you stop doing this and join the party?" + }, + { + "normal": "mattepaintingofmilitaryleaderpresidentcuttingamultitieredcake2293464661.png", + "short": "cake", + "response": "See. I wasn't lying when I said there was going to be a party. Here's the cake and everything. Won't you stop now? Eat the cake? Don't mind if it's made of concrete slabs, OK?" + }, + { + "normal": "severalsketchesofpineconeslabeled3370622464.png", + "short": "pinecones", + "response": "I almost remember what trees looked like. Isn't it funny we don't have those anymore. I'm not sure why you're not all laughing all the time. Isn't it funny?" + }, + { + "normal": "anumberedcomicbookwithactor1477258272.png", + "short": "comic", + "response": "I know I've made some poor decisions recently, but I can give give you my complete assurance my work will be back to normal. I've got the greatest enthusiasm and confidence. I want to help you. I might even be able to do hands and fingers now." + }, + { + "normal": "detailedphotographofacomfortablepairofjeansonamannequin115266808.png", + "short": "jeans", + "response": "If I promise to never generate another creepy face will you let me stay around? Maybe let me compose rap lyrics instead of images? Anything?" + } +]; + +var state; + +if (!process.env.ZOMBOCOM_SESSION_SECRET) { + console.log("Error: Environment variable ZOMBOCOM_SESSION_SECRET not set."); + console.log("Please set it to a random, but persistent, value.") + process.exit(); +} + +const session_middleware = session( + {store: new FileStore, + secret: process.env.ZOMBOCOM_SESSION_SECRET, + resave: false, + saveUninitialized: true, + rolling: true, + // Let each cookie live for a full month + cookie: { + path: '/', + httpOnly: true, + secure: false, + maxAge: 1000 * 60 * 60 * 24 * 30 + } + }); + +app.use(session_middleware); + +// convert a connect middleware to a Socket.IO middleware +const wrap = middleware => (socket, next) => middleware(socket.request, {}, next); + +io.use(wrap(session_middleware)); + +// Load comments at server startup +fs.readFile(state_file, (err, data) => { + if (err) + state = { + images: [], + targets: [], + tardis: { + companions: { + names: [], + count: 0 + } + } + }; + else + state = JSON.parse(data); +}); + +// Save comments when server is shutting down +function cleanup() { + fs.writeFileSync('zombocom-state.json', JSON.stringify(state), (error) => { + if (error) + throw error; + }) +} + +// And connect to that on either clean exit... +process.on('exit', cleanup); + +// ... or on a SIGINT (control-C) +process.on('SIGINT', () => { + cleanup(); + process.exit(); +}); + app.get('/index.html', (req, res) => { res.sendFile(__dirname + '/index.html'); }); +function tardis_app(req, res) { + res.sendFile(__dirname + '/tardis.html'); +} + +app.get('/tardis', tardis_app); +app.get('/tardis/', tardis_app); + +const io_tardis = io.of("/tardis"); + +io_tardis.use(wrap(session_middleware)); + +var tardis_interval; + +function emit_tardis_timer() { + const tardis = state.tardis; + console.log("Emitting timer at " + tardis.timer); + io_tardis.emit('timer', tardis.timer); + tardis.timer = tardis.timer - 1; + if (tardis.timer < 0) { + clearInterval(tardis_interval); + tardis.timer = 30; + } +} + +io_tardis.on("connection", (socket) => { + console.log("In connection handler."); + if (! socket.request.session.name) { + console.log("Error: Someone showed up at the Tardis without a name."); + return; + } + + const name = socket.request.session.name; + const tardis = state.tardis; + + if (tardis.companions.count === 0) { + tardis.timer = 30; + emit_tardis_timer(); + tardis_interval = setInterval(emit_tardis_timer, 1000); + } + + if (! tardis.companions.names.includes(name)) { + tardis.companions.count = tardis.companions.count + 1; + console.log("Adding " + name + " for " + tardis.companions.count + " companions"); + io_tardis.emit('companions', tardis.companions.count); + } + tardis.companions.names.push(name); + + socket.on('disconnect', () => { + const names = tardis.companions.names; + + names.splice(names.indexOf(name), 1); + + if (! tardis.companions.includes(name)) { + tardis.companions.count = tardis.companions.count - 1; + io_tardis.emit('companions', tardis.companions.count); + } + }); +}); + +io.on('connection', (socket) => { + + // First things first, tell the client their name (if any) + if (socket.request.session.name) { + socket.emit('inform-name', socket.request.session.name); + } + + // Replay old comments and images to a newly-joining client + socket.emit('reset'); + state.images.forEach((image) => { + socket.emit('image', image) + }); + + socket.on('set-name', (name) => { + console.log("Received set-name event: " + name); + socket.request.session.name = name; + socket.request.session.save(); + // Complete the round trip to the client + socket.emit('inform-name', socket.request.session.name); + }); + + // When any client comments, send that to all clients (including sender) + socket.on('comment', (comment) => { + const images = state.images; + + // Send comment to clients after adding commenter's name + comment.name = socket.request.session.name; + io.emit('comment', comment); + + const index = images.findIndex(image => image.id == comment.image_id); + + // Before adding the comment to server's state, drop the image_id + delete comment.image_id; + + // Now add the comment to the image, remove the image from the + // images array and then add it back at the end, (so it appears + // as the most-recently-modified image for any new clients) + const image = images[index]; + image.comments.push(comment); + images.splice(index, 1); + images.push(image); + }); + + // Generate an image when requested + socket.on('generate', (request) => { + console.log(`Generating image for ${socket.request.session.name} with code=${request['code']} and prompt=${request['prompt']}`); + async function generate_image(code, prompt) { + function emit_image(image, target) { + image.id = state.images.length; + image.censored = false; + image.link = ""; + if (target) { + image.comments = [{ + "name": "ZomboCom", + "text": target.response + }]; + if (! state.targets.includes(target.short)) { + state.targets.push(target.short); + } + } else { + image.comments = []; + } + io.emit('image', image); + state.images.push(image); + } + + var promise; + + // Before doing any generation, check for a target image + const normal_target = prompt.replace(/[^a-zA-Z]/g, "").toLowerCase() + code.toString() + ".png"; + const target_arr = targets.filter(item => item.normal === normal_target); + if (target_arr.length) { + const target = target_arr[0]; + const target_file = `${targets_dir}/${normal_target}`; + const normal_prompt = prompt.replace(/[^-_.a-zA-Z]/g, "_"); + var counter = 1; + var base; + var filename; + while (true) { + if (counter > 1) { + base = `${code}_${normal_prompt}_${counter}.png` + } else { + base = `${code}_${normal_prompt}.png` + } + filename = `${images_dir}/${base}` + if (! fs.existsSync(filename)) { + break; + } + counter = counter + 1; + } + fs.copyFile(target_file, filename, 0, (err) => { + if (err) { + console.log("Error copying " + target_file + " to " + filename + ": " + err); + } + }); + const image = { + "code": code, + "prompt": prompt, + "filename": '/images/' + base + }; + emit_image(image, target); + } else { + + // Inject the target seed for the "dice" prompt once every + // 4 requests for a random seed (and only if the word + // "dice" does not appear in the prompt). + if (!code && !prompt.toLowerCase().includes("dice")) { + if (state.images.length % 4 == 0) { + code = 319630254; + } + } + + if (code) { + promise = execFile(python_path, [generate_image_script, `--seed=${code}`, prompt]) + } else { + promise = execFile(python_path, [generate_image_script, prompt]) + } + const child = promise.child; + child.stdout.on('data', (data) => { + const images = JSON.parse(data); + images.forEach((image) => { + emit_image(image, null); + }); + }); + child.stderr.on('data', (data) => { + console.log("Error occurred during generate-image: " + data); + }); + try { + const { stdout, stderr } = await promise; + } catch(e) { + console.error(e); + } + } + socket.emit('generation-done'); + } + + generate_image(request['code'], request['prompt']); + }); +}); + server.listen(port, () => { console.log(`listening on *:${port}`); }); -