123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317 |
- import { isValidImage } from "../util";
- import Jimp from "jimp";
- import { client } from "../client";
- import * as cv from "opencv4nodejs";
- import * as path from "path";
- import request from "request-promise-native";
- import { Message, MessageAttachment } from "discord.js";
- import { getRepository } from "typeorm";
- import { FaceCaptionMessage, FaceCaptionType } from "@shared/db/entity/FaceCaptionMessage";
- import { KnownChannel } from "@shared/db/entity/KnownChannel";
- import { CommandSet, Action, ActionType, Command } from "src/model/command";
- import { logger } from "src/logging";
- const EMOTE_GUILD = "505333548694241281";
- const animeCascade = new cv.CascadeClassifier(path.resolve(process.cwd(), "animu.xml"));
- const faceCascade = new cv.CascadeClassifier(cv.HAAR_FRONTALFACE_ALT2);
- const CAPTION_IMG_SIZE = 300;
- const CAPTION_PROBABILITY = 0.33;
- type ImageProcessor = (faces: cv.Rect[], data: Buffer) => Promise<Jimp>;
- const CAPTION_OFFSET = 5;
- @CommandSet
- export class Facemorph {
- intersects(r1: cv.Rect, r2: cv.Rect): boolean {
- return (
- r1.x <= r2.x + r2.width &&
- r1.x + r1.width >= r2.x &&
- (r1.y <= r2.y + r2.height && r1.y + r1.height >= r2.y)
- );
- }
- morphFaces = async (faces: cv.Rect[], data: Buffer): Promise<Jimp> => {
- const padoru = Math.random() <= this.getPadoruChance();
- let jimpImage = await Jimp.read(data);
- const emoteGuild = client.bot.guilds.resolve(EMOTE_GUILD);
- if (!emoteGuild)
- return jimpImage;
- const emojiKeys = process.env.FOOLS != "TRUE" ? [
- ...emoteGuild
- .emojis.cache.filter(e => !e.animated && e.name.startsWith("PADORU") == padoru)
- .keys()
- ]:
- [
- "505335829565276160",
- "430434087157760003",
- "456472341874999297",
- "649677767348060170",
- "589706788782342183",
- "665272109227835422"
- ];
- for (const rect of faces) {
- const dx = rect.x + rect.width / 2;
- const dy = rect.y + rect.height / 2;
- const emojiKey = emojiKeys[Math.floor(Math.random() * emojiKeys.length)];
- const emoji = client.bot.emojis.resolve(emojiKey);
- if (!emoji)
- throw new Error("Failed to resolve emoji!");
- let emojiImage = await Jimp.read(emoji.url);
- let ew = emojiImage.getWidth();
- let eh = emojiImage.getHeight();
- const CONSTANT_SCALE = 1.1;
- const scaleFactor = (Math.max(rect.width, rect.height) / Math.min(ew, eh)) * CONSTANT_SCALE;
- ew *= scaleFactor;
- eh *= scaleFactor;
- emojiImage = emojiImage.scale(scaleFactor);
- jimpImage = jimpImage.composite(emojiImage, dx - ew / 2, dy - eh / 2);
- }
- return jimpImage;
- }
- async getRandomCaption(type: FaceCaptionType): Promise<FaceCaptionMessage | null> {
- const repo = getRepository(FaceCaptionMessage);
- const caption = await repo.query(`select message
- from face_caption_message
- where type = $1
- order by random()
- limit 1`, [type]) as FaceCaptionMessage[];
- if (caption.length == 0)
- return null;
- return caption[0];
- }
- captionFace = async (faces: cv.Rect[], data: Buffer): Promise<Jimp> => {
- const padoru = Math.random() <= this.getPadoruChance();
- const face = faces[Math.floor(Math.random() * faces.length)];
- const squaredFace = await face.toSquareAsync();
- const targetSize = CAPTION_IMG_SIZE;
- const img = await Jimp.read(data);
- let tempImg = await Jimp.create(squaredFace.width, squaredFace.height);
- tempImg = await tempImg.blit(
- img,
- 0,
- 0,
- squaredFace.x,
- squaredFace.y,
- squaredFace.width,
- squaredFace.height
- );
- tempImg = await tempImg.scale(targetSize / squaredFace.width);
- const font = await Jimp.loadFont(padoru ? Jimp.FONT_SANS_16_WHITE : Jimp.FONT_SANS_16_BLACK);
- let text = "";
- if(padoru)
- text = "PADORU PADORU";
- else if(process.env.FOOLS == "TRUE") {
- const titles = ["They are horse", "Neigh!", "Insert carrots into them!", "They will become horse!", "They will serve Geoffrey!", "tfw no carrots"];
- text = titles[Math.floor(Math.random() * titles.length)];
- }
- else {
- const prefixMessage = (await this.getRandomCaption(FaceCaptionType.PREFIX))?.message ?? "Feed them";
- const postfixMessage = (await this.getRandomCaption(FaceCaptionType.POSTFIX)) ?? "carrots";
- text = `${prefixMessage} ${postfixMessage}`;
- }
- const h = Jimp.measureTextHeight(font, text, targetSize - CAPTION_OFFSET * 2);
- let finalImage = await Jimp.create(targetSize, targetSize + h + CAPTION_OFFSET * 2, padoru ? "#FD2027" : "#FFFFFF");
- finalImage = await finalImage.print(
- font,
- CAPTION_OFFSET,
- CAPTION_OFFSET,
- text,
- finalImage.getWidth() - CAPTION_OFFSET
- );
- finalImage = await finalImage.composite(tempImg, 0, CAPTION_OFFSET * 2 + h);
- return finalImage;
- }
- getPadoruChance(): number {
- const now = new Date();
- if (now.getUTCMonth() != 11 || now.getUTCDate() > 25)
- return 0;
- return 1 / (27.0 - now.getUTCDate());
- }
- async processFaceSwap(message: Message, attachmentUrl: string, processor?: ImageProcessor, failMessage?: string, successMessage?: string): Promise<void> {
- const data = await request(attachmentUrl, { encoding: null }) as Buffer;
- const im = await cv.imdecodeAsync(data, cv.IMREAD_COLOR);
- const gray = await im.cvtColorAsync(cv.COLOR_BGR2GRAY);
- const normGray = await gray.equalizeHistAsync();
- const animeFaces = await animeCascade.detectMultiScaleAsync(
- normGray,
- 1.1,
- 5,
- 0,
- new cv.Size(24, 24)
- );
- const normalFaces = await faceCascade.detectMultiScaleAsync(gray);
- if (animeFaces.objects.length == 0 && normalFaces.objects.length == 0) {
- if (failMessage) message.channel.send(failMessage);
- return;
- }
- const faces = [...normalFaces.objects, ...animeFaces.objects];
- let normalCount = normalFaces.objects.length;
- let animeCount = animeFaces.objects.length;
- for (let i = 0; i < normalCount; i++) {
- const rNormal = faces[i];
- if (animeCount == 0) break;
- for (let j = normalCount; j < faces.length; j++) {
- const rAnime = faces[j];
- if (this.intersects(rAnime, rNormal)) {
- const animeA = rAnime.width * rAnime.height;
- const faceA = rNormal.width * rNormal.height;
- if (animeA > faceA) {
- faces.splice(i, 1);
- normalCount--;
- i--;
- break;
- } else {
- faces.splice(j, 1);
- animeCount--;
- j--;
- }
- }
- }
- }
- let jimpImage: Jimp;
- if (processor)
- jimpImage = await processor(faces, data);
- else {
- if (Math.random() <= CAPTION_PROBABILITY)
- jimpImage = await this.captionFace(faces, data);
- else
- jimpImage = await this.morphFaces(faces, data);
- }
- jimpImage.quality(90);
- const buffer = await jimpImage.getBufferAsync(Jimp.MIME_JPEG);
- const messageContents =
- successMessage ||
- `I noticed a face in the image. I think this looks better ${client.bot.emojis.resolve("505076258753740810")?.toString() ?? ":)"}`;
- message.channel.send(messageContents, {
- files: [buffer]
- });
- }
- processLastImage(msg: Message, processor: ImageProcessor): void {
- type AttachedMessage = {msg: Message, att: MessageAttachment};
- const lastImagedMessage = msg.channel.messages.cache.mapValues(m => ({msg: m, att: m.attachments.find(v => isValidImage(v.name))}))
- .filter(v => v.att != undefined).last() as AttachedMessage;
- if (!lastImagedMessage) {
- msg.channel.send(`${msg.author.toString()} Sorry, I couldn't find any recent messages with images.`);
- return;
- }
- const replyEmoji = client.bot.emojis.resolve("505076258753740810");
- const emojiText = replyEmoji ? replyEmoji.toString() : "Jiiii~";
- this.processFaceSwap(
- msg,
- lastImagedMessage.att.url,
- processor,
- `${msg.author.toString()} Nice image! I don't see anything interesting, though.`,
- `${msg.author.toString()} ${emojiText}`
- ).catch(err => logger.error(`Failed to run faceapp on message ${msg.id}`, err));
- }
- @Action(ActionType.MESSAGE)
- async morphRandomImage(actionsDone: boolean, msg: Message): Promise<boolean> {
- if (actionsDone) return false;
- if (msg.mentions.users.size > 0 && msg.mentions.users.first()?.id == client.botUser.id)
- return false;
- const imageAttachment = msg.attachments.find(v => isValidImage(v.name));
- if (imageAttachment) {
- const repo = getRepository(KnownChannel);
- const knownChannel = await repo.findOne({
- where: { channelId: msg.channel.id },
- select: ["faceMorphProbability"]
- });
- if (!knownChannel || Math.random() > knownChannel.faceMorphProbability)
- return false;
- this.processFaceSwap(msg, imageAttachment.url).catch(err =>
- logger.error(`Failed to run faceapp on message ${msg.id}`, err)
- );
- return true;
- }
- return false;
- }
- @Action(ActionType.DIRECT_MENTION)
- async morphProvidedImage(actionsDone: boolean, msg: Message, content: string): Promise<boolean> {
- if (actionsDone) return false;
- const image = msg.attachments.find(v => isValidImage(v.name));
- if (!image) {
- if (msg.attachments.size > 0) {
- msg.channel.send(
- `${msg.author.toString()} Nice, but I can't do anything to it! (Invalid file type)`
- );
- return true;
- }
- return false;
- }
- let processor;
- if (content.startsWith("caption this"))
- processor = this.captionFace;
- else
- processor = this.morphFaces;
- const replyEmoji = client.bot.emojis.resolve("505076258753740810");
- const emojiText = replyEmoji ? replyEmoji.toString() : "Jiiii~";
- this.processFaceSwap(
- msg,
- image.url,
- processor,
- `${msg.author.toString()} Nice image! I don't see anything interesting, though.`,
- `${msg.author.toString()} ${emojiText}`
- ).catch(err => logger.error(`Failed to run faceapp because ${msg.id}`, err));
- return true;
- }
- @Command({
- pattern: "caption last image"
- })
- captionLastImage(msg: Message): void {
- this.processLastImage(msg, this.captionFace);
- }
- @Command({
- pattern: "look at last image"
- })
- lookLastImage(msg: Message): void {
- this.processLastImage(msg, this.morphFaces);
- }
- }
|