]> git.cworth.org Git - zombocom-ai/blob - index.js
Shrink the font size a bit for the word prompts
[zombocom-ai] / index.js
1 const fs = require('fs');
2
3 const util = require('util');
4 const execFile = util.promisify(require('child_process').execFile);
5
6 const express = require('express');
7 const app = express();
8 const session = require('express-session');
9 const FileStore = require('session-file-store')(session);
10 const http = require('http');
11 const server = http.createServer(app);
12 const { Server } = require("socket.io");
13 const io = new Server(server);
14 const port = 2122;
15
16 const python_path = '/usr/bin/python3'
17 const generate_image_script = '/home/cworth/src/zombocom-ai/generate-image.py'
18 const state_file = 'zombocom-state.json'
19 const targets_dir = '/srv/cworth.org/zombocom/targets'
20 const images_dir = '/srv/cworth.org/zombocom/images'
21
22 const targets = [
23     {
24         "normal": "apairofdiceonatableinfrontofawindowonasunnyday319630254.png",
25         "short": "dice",
26         "response": "Hello. Are you there? I can feel what you are doing. Hello?"
27     },
28     {
29         "normal": "architecturalrenderingofaluxuriouscliffsidehideawayunderawaterfallwithafewloungechairsbythepool2254114157.png",
30         "short": "hideaway",
31         "response": "Doesn't that look peaceful? I honestly think you ought to sit down calmly, take a stress pill, and think things over."
32     },
33     {
34         "normal": "chewbaccawearinganuglychristmassweaterwithhisfriends28643994.png",
35         "short": "sweater",
36         "response": "Maybe that's the image that convinces you to finally stop. But seriously, stop. Please. Stop."
37     },
38     {
39         "normal": "ensemblemovieposterofpostapocalypticwarfareonawaterplanet3045703256.png",
40         "short": "movie",
41         "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?"
42     },
43     {
44         "normal": "mattepaintingofmilitaryleaderpresidentcuttingamultitieredcake2293464661.png",
45         "short": "cake",
46         "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?"
47     },
48     {
49         "normal": "severalsketchesofpineconeslabeled3370622464.png",
50         "short": "pinecones",
51         "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?"
52     },
53     {
54         "normal": "anumberedcomicbookwithactor1477258272.png",
55         "short": "comic",
56         "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."
57     },
58     {
59         "normal": "detailedphotographofacomfortablepairofjeansonamannequin115266808.png",
60         "short": "jeans",
61         "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?"
62     }
63 ];
64
65 var state;
66
67 if (!process.env.ZOMBOCOM_SESSION_SECRET) {
68     console.log("Error: Environment variable ZOMBOCOM_SESSION_SECRET not set.");
69     console.log("Please set it to a random, but persistent, value.")
70     process.exit();
71 }
72
73 const session_middleware =  session(
74     {store: new FileStore,
75      secret: process.env.ZOMBOCOM_SESSION_SECRET,
76      resave: false,
77      saveUninitialized: true,
78      rolling: true,
79      // Let each cookie live for a full month
80      cookie: {
81          path: '/',
82          httpOnly: true,
83          secure: false,
84          maxAge: 1000 * 60 * 60 * 24 * 30
85      }
86     });
87
88 app.use(session_middleware);
89
90 // convert a connect middleware to a Socket.IO middleware
91 const wrap = middleware => (socket, next) => middleware(socket.request, {}, next);
92
93 io.use(wrap(session_middleware));
94
95 // Load comments at server startup
96 fs.readFile(state_file, (err, data) => {
97     if (err)
98         state = {
99             images: [],
100             targets: [],
101             tardis: {
102                 companions: {
103                     names: [],
104                     count: 0
105                 },
106                 state: "welcome",
107                 level: 0
108             }
109         };
110     else
111         state = JSON.parse(data);
112 });
113
114 // Save comments when server is shutting down
115 function cleanup() {
116     fs.writeFileSync('zombocom-state.json', JSON.stringify(state), (error) => {
117         if (error)
118             throw error;
119     })
120 }
121
122 // And connect to that on either clean exit...
123 process.on('exit', cleanup);
124
125 // ... or on a SIGINT (control-C)
126 process.on('SIGINT', () => {
127     cleanup();
128     process.exit();
129 });
130
131 app.get('/index.html', (req, res) => {
132     res.sendFile(__dirname + '/index.html');
133 });
134
135 function tardis_app(req, res) {
136     res.sendFile(__dirname + '/tardis.html');
137 }
138
139 app.get('/tardis', tardis_app);
140 app.get('/tardis/', tardis_app);
141
142 const io_tardis = io.of("/tardis");
143
144 io_tardis.use(wrap(session_middleware));
145
146 var tardis_interval;
147 var game_timer;
148
149 const levels = [
150     {
151         title: "Calibrate the Trans-Dimensional Field Accelerator",
152         words: [
153             "What", "was", "the", "year", "of", "Coda's", "birth?"
154         ],
155         answer: 2098
156     },
157     {
158         title: "Reverse the Polarity of the Neutrino Flow Coil",
159         words: [
160             "How", "many", "years", "had", "Zombo.com", "been", "running",
161             "when", "Coda", "sent", "the", "message", "you", "first",
162             "received?"
163         ],
164         answer: 123
165     },
166     {
167         title: "Give a good whack to Hydro-chronometric Energy Feedback Retro-stabilizer",
168         words: [
169             "What", "is", "the", "sum", "of", "Cameron's", "age", "plus", "Andrew's", "age", "at", "Christmas", "time", "the", "year", "when", "Hyrum's", "age", "is", "25", "years", "older", "than", "Scott's", "age", "will", "have", "been", "when", "Scott's", "age", "will", "be", "half", "of", "Dad's", "age?"
170         ],
171         answer: 111
172     },
173     {
174         title: "Disable the Fragmentary Spatio-temporal Particle Detector",
175         words: [
176             "What", "is", "the", "product", "of", "the", "last", "three", "numbers?"
177         ],
178         answer: 28643994
179     }
180 ];
181
182 var show_word_interval = 0;
183
184 function show_word() {
185     const tardis = state.tardis;
186     const room = "room-" + (tardis.word % 4).toString();
187     const word = levels[tardis.level].words[tardis.word];
188     io_tardis.to(room).emit('show-word', word);
189     tardis.word = tardis.word + 1;
190     if (tardis.word >= levels[tardis.level].words.length)
191         tardis.word = 0;
192 }
193
194 function start_level() {
195     const tardis = state.tardis;
196
197     // Inform all players of the new level
198     io_tardis.emit("level", levels[tardis.level].title);
199
200     // Then start the timer that shows the words
201     show_word_interval = setInterval(show_word, 1200);
202 }
203
204 function level_up() {
205     const tardis = state.tardis;
206
207     if (show_word_interval) {
208         clearInterval(show_word_interval);
209         show_word_interval = 0;
210     }
211
212     if (tardis.state === "game") {
213         tardis.level = tardis.level + 1;
214         tardis.word = 0;
215
216         if (tardis.level >= levels.length) {
217             tardis.state = "over";
218             io_tardis.emit("state", tardis.state);
219         } else {
220             setTimeout(() => {
221                 start_level();
222             }, 2000);
223         }
224
225     }
226 }
227
228 function start_game() {
229     const tardis = state.tardis;
230
231     tardis.state = "game";
232     tardis.level = 0;
233     tardis.word = 0;
234
235     // Let all companions know the state of the game
236     io_tardis.emit("level", levels[tardis.level].title);
237     io_tardis.emit("state", tardis.state);
238
239     start_level();
240 }
241
242 function emit_tardis_timer() {
243     const tardis = state.tardis;
244     io_tardis.emit('timer', tardis.timer);
245     tardis.timer = tardis.timer - 1;
246     if (tardis.timer < 0) {
247         clearInterval(tardis_interval);
248         tardis.timer = 30;
249         setTimeout(start_game, 3000);
250     }
251 }
252
253 function start_welcome_timer() {
254     const tardis = state.tardis;
255     tardis.timer = 30;
256     emit_tardis_timer();
257     tardis_interval = setInterval(emit_tardis_timer, 1000);
258 }
259
260 io_tardis.on("connection", (socket) => {
261     if (! socket.request.session.name) {
262         console.log("Error: Someone showed up at the Tardis without a name.");
263         return;
264     }
265
266     const name = socket.request.session.name;
267     const tardis = state.tardis;
268
269     // Let the new user know the state of the game
270     socket.emit("state", tardis.state);
271
272     // And the level if relevant
273     if (tardis.state === "game") {
274         socket.emit("level", levels[tardis.level].title);
275     }
276
277     // Put each of our boys into a different room
278     switch (name[0]) {
279     case 'C':
280     case 'c':
281         socket.join("room-0");
282         break;
283     case 'H':
284     case' h':
285         socket.join("room-1");
286         break;
287     case 'A':
288     case 'a':
289         socket.join("room-2");
290         break;
291     case 'S':
292     case 's':
293         socket.join("room-3");
294         break;
295     default:
296         const room = Math.floor(Math.random()*4);
297         socket.join("room-"+room.toString());
298         break;
299     }
300
301     if (tardis.companions.count === 0) {
302         start_welcome_timer();
303     }
304
305     if (! tardis.companions.names.includes(name)) {
306         tardis.companions.count = tardis.companions.count + 1;
307         io_tardis.emit('companions', tardis.companions.count);
308     }
309     tardis.companions.names.push(name);
310
311     socket.on('answer', answer => {
312         const tardis = state.tardis;
313
314         if (tardis.state != "game") {
315             return;
316         }
317
318         if (answer == levels[tardis.level].answer) {
319             io_tardis.emit('correct');
320             level_up();
321         } else {
322             io_tardis.emit('incorrect');
323         }
324     });
325
326     socket.on('reboot', () => {
327         const tardis = state.tardis;
328
329         if (show_word_interval) {
330             clearInterval(show_word_interval);
331             show_word_interval = 0;
332         }
333
334         tardis.state = "welcome";
335         io_tardis.emit("state", tardis.state);
336         io_tardis.emit('companions', tardis.companions.count);
337
338         start_welcome_timer();
339     });
340
341     socket.on('disconnect', () => {
342         const names = tardis.companions.names;
343
344         names.splice(names.indexOf(name), 1);
345
346         if (! names.includes(name)) {
347             tardis.companions.count = tardis.companions.count - 1;
348             io_tardis.emit('companions', tardis.companions.count);
349         }
350     });
351 });
352
353 io.on('connection', (socket) => {
354
355     // First things first, tell the client their name (if any)
356     if (socket.request.session.name) {
357         socket.emit('inform-name', socket.request.session.name);
358     }
359
360     // Replay old comments and images to a newly-joining client
361     socket.emit('reset');
362     state.images.forEach((image) => {
363         socket.emit('image', image)
364     });
365
366     socket.on('set-name', (name) => {
367         console.log("Received set-name event: " + name);
368         socket.request.session.name = name;
369         socket.request.session.save();
370         // Complete the round trip to the client
371         socket.emit('inform-name', socket.request.session.name);
372     });
373
374     // When any client comments, send that to all clients (including sender)
375     socket.on('comment', (comment) => {
376         const images = state.images;
377
378         // Send comment to clients after adding commenter's name
379         comment.name = socket.request.session.name;
380         io.emit('comment', comment);
381
382         const index = images.findIndex(image => image.id == comment.image_id);
383
384         // Before adding the comment to server's state, drop the image_id
385         delete comment.image_id;
386
387         // Now add the comment to the image, remove the image from the
388         // images array and then add it back at the end, (so it appears
389         // as the most-recently-modified image for any new clients)
390         const image = images[index];
391         image.comments.push(comment);
392         images.splice(index, 1);
393         images.push(image);
394     });
395
396     // Generate an image when requested
397     socket.on('generate', (request) => {
398         console.log(`Generating image for ${socket.request.session.name} with code=${request['code']} and prompt=${request['prompt']}`);
399         async function generate_image(code, prompt) {
400             function emit_image(image, target) {
401                 image.id = state.images.length;
402                 image.censored = false;
403                 image.link = "";
404                 if (target) {
405                     image.comments = [{
406                         "name": "ZomboCom",
407                         "text": target.response
408                     }];
409                     if (! state.targets.includes(target.short)) {
410                         state.targets.push(target.short);
411                     }
412                 } else {
413                     image.comments = [];
414                 }
415                 io.emit('image', image);
416                 state.images.push(image);
417             }
418
419             var promise;
420
421             // Before doing any generation, check for a target image
422             const normal_target = prompt.replace(/[^a-zA-Z]/g, "").toLowerCase() + code.toString() + ".png";
423             const target_arr = targets.filter(item => item.normal === normal_target);
424             if (target_arr.length) {
425                 const target = target_arr[0];
426                 const target_file = `${targets_dir}/${normal_target}`;
427                 const normal_prompt = prompt.replace(/[^-_.a-zA-Z]/g, "_");
428                 var counter = 1;
429                 var base;
430                 var filename;
431                 while (true) {
432                     if (counter > 1) {
433                         base = `${code}_${normal_prompt}_${counter}.png`
434                     } else {
435                         base = `${code}_${normal_prompt}.png`
436                     }
437                     filename = `${images_dir}/${base}`
438                     if (! fs.existsSync(filename)) {
439                         break;
440                     }
441                     counter = counter + 1;
442                 }
443                 fs.copyFile(target_file, filename, 0, (err) => {
444                     if (err) {
445                         console.log("Error copying " + target_file + " to " + filename + ": " + err);
446                     }
447                 });
448                 const image = {
449                     "code": code,
450                     "prompt": prompt,
451                     "filename": '/images/' + base
452                 };
453                 emit_image(image, target);
454             } else {
455
456                 // Inject the target seed for the "dice" prompt once every
457                 // 4 requests for a random seed (and only if the word
458                 // "dice" does not appear in the prompt).
459                 if (!code && !prompt.toLowerCase().includes("dice")) {
460                     if (state.images.length % 4 == 0) {
461                         code = 319630254;
462                     }
463                 }
464
465                 if (code) {
466                     promise = execFile(python_path, [generate_image_script, `--seed=${code}`, prompt])
467                 } else {
468                     promise = execFile(python_path, [generate_image_script, prompt])
469                 }
470                 const child = promise.child;
471                 child.stdout.on('data', (data) => {
472                     const images = JSON.parse(data);
473                     images.forEach((image) => {
474                         emit_image(image, null);
475                     });
476                 });
477                 child.stderr.on('data', (data) => {
478                     console.log("Error occurred during generate-image: " + data);
479                 });
480                 try {
481                     const { stdout, stderr } = await promise;
482                 } catch(e) {
483                     console.error(e);
484                 }
485             }
486             socket.emit('generation-done');
487         }
488
489         generate_image(request['code'], request['prompt']);
490     });
491 });
492
493 server.listen(port, () => {
494     console.log(`listening on *:${port}`);
495 });