mirror of
https://github.com/Sheldan/abstracto.git
synced 2026-01-04 16:44:43 +00:00
[AB-xxx] adding pat gif generator command
This commit is contained in:
@@ -0,0 +1,147 @@
|
|||||||
|
package dev.sheldan.abstracto.imagegeneration.command;
|
||||||
|
|
||||||
|
import dev.sheldan.abstracto.core.command.UtilityModuleDefinition;
|
||||||
|
import dev.sheldan.abstracto.core.command.condition.AbstractConditionableCommand;
|
||||||
|
import dev.sheldan.abstracto.core.command.config.CommandConfiguration;
|
||||||
|
import dev.sheldan.abstracto.core.command.config.HelpInfo;
|
||||||
|
import dev.sheldan.abstracto.core.command.config.Parameter;
|
||||||
|
import dev.sheldan.abstracto.core.command.execution.CommandContext;
|
||||||
|
import dev.sheldan.abstracto.core.command.execution.CommandResult;
|
||||||
|
import dev.sheldan.abstracto.core.config.FeatureDefinition;
|
||||||
|
import dev.sheldan.abstracto.core.interaction.InteractionService;
|
||||||
|
import dev.sheldan.abstracto.core.interaction.slash.SlashCommandConfig;
|
||||||
|
import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService;
|
||||||
|
import dev.sheldan.abstracto.core.service.ChannelService;
|
||||||
|
import dev.sheldan.abstracto.core.templating.model.AttachedFile;
|
||||||
|
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
|
||||||
|
import dev.sheldan.abstracto.core.templating.service.TemplateService;
|
||||||
|
import dev.sheldan.abstracto.core.utils.FileService;
|
||||||
|
import dev.sheldan.abstracto.core.utils.FutureUtils;
|
||||||
|
import dev.sheldan.abstracto.imagegeneration.config.ImageGenerationFeatureDefinition;
|
||||||
|
import dev.sheldan.abstracto.imagegeneration.config.ImageGenerationSlashCommandNames;
|
||||||
|
import dev.sheldan.abstracto.imagegeneration.service.ImageGenerationService;
|
||||||
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
|
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class Pat extends AbstractConditionableCommand {
|
||||||
|
public static final String MEMBER_PARAMETER_KEY = "member";
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ImageGenerationService imageGenerationService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TemplateService templateService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ChannelService channelService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FileService fileService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private InteractionService interactionService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SlashCommandParameterService slashCommandParameterService;
|
||||||
|
|
||||||
|
private static final String PAT_EMBED_TEMPLATE_KEY = "pat_response";
|
||||||
|
|
||||||
|
@Value("${abstracto.feature.imagegeneration.pat.imagesize}")
|
||||||
|
private Integer imageSize;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
|
||||||
|
Member member;
|
||||||
|
List<Object> parameters = commandContext.getParameters().getParameters();
|
||||||
|
if(parameters.isEmpty()) {
|
||||||
|
member = commandContext.getAuthor();
|
||||||
|
} else {
|
||||||
|
member = (Member) parameters.get(0);
|
||||||
|
}
|
||||||
|
File patGifFile = imageGenerationService.getPatGif(member.getEffectiveAvatar().getUrl(imageSize));
|
||||||
|
MessageToSend messageToSend = templateService.renderEmbedTemplate(PAT_EMBED_TEMPLATE_KEY, new Object());
|
||||||
|
// template support does not support binary files
|
||||||
|
AttachedFile file = AttachedFile
|
||||||
|
.builder()
|
||||||
|
.file(patGifFile)
|
||||||
|
.fileName("pat.gif")
|
||||||
|
.build();
|
||||||
|
messageToSend.getAttachedFiles().add(file);
|
||||||
|
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel()))
|
||||||
|
.thenAccept(unused -> fileService.safeDeleteIgnoreException(messageToSend.getAttachedFiles().get(0).getFile()))
|
||||||
|
.thenApply(unused -> CommandResult.fromIgnored());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
|
||||||
|
event.deferReply().queue();
|
||||||
|
Member targetMember;
|
||||||
|
if(slashCommandParameterService.hasCommandOption(MEMBER_PARAMETER_KEY, event)) {
|
||||||
|
targetMember = slashCommandParameterService.getCommandOption(MEMBER_PARAMETER_KEY, event, Member.class);
|
||||||
|
} else {
|
||||||
|
targetMember = event.getMember();
|
||||||
|
}
|
||||||
|
File patGifFile = imageGenerationService.getPatGif(targetMember.getEffectiveAvatar().getUrl(imageSize));
|
||||||
|
MessageToSend messageToSend = templateService.renderEmbedTemplate(PAT_EMBED_TEMPLATE_KEY, new Object());
|
||||||
|
// template support does not support binary files
|
||||||
|
AttachedFile file = AttachedFile
|
||||||
|
.builder()
|
||||||
|
.file(patGifFile)
|
||||||
|
.fileName("pat.gif")
|
||||||
|
.build();
|
||||||
|
messageToSend.getAttachedFiles().add(file);
|
||||||
|
return FutureUtils.toSingleFutureGeneric(interactionService.sendMessageToInteraction(messageToSend, event.getHook()))
|
||||||
|
.thenAccept(unused -> fileService.safeDeleteIgnoreException(messageToSend.getAttachedFiles().get(0).getFile()))
|
||||||
|
.thenApply(unused -> CommandResult.fromIgnored());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandConfiguration getConfiguration() {
|
||||||
|
List<Parameter> parameters = new ArrayList<>();
|
||||||
|
Parameter memberParameter = Parameter
|
||||||
|
.builder()
|
||||||
|
.name(MEMBER_PARAMETER_KEY)
|
||||||
|
.type(Member.class)
|
||||||
|
.templated(true)
|
||||||
|
.optional(true)
|
||||||
|
.build();
|
||||||
|
parameters.add(memberParameter);
|
||||||
|
HelpInfo helpInfo = HelpInfo
|
||||||
|
.builder()
|
||||||
|
.templated(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
SlashCommandConfig slashCommandConfig = SlashCommandConfig
|
||||||
|
.builder()
|
||||||
|
.enabled(true)
|
||||||
|
.rootCommandName(ImageGenerationSlashCommandNames.IMAGE_GENERATION)
|
||||||
|
.groupName("memes")
|
||||||
|
.commandName("pat")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return CommandConfiguration.builder()
|
||||||
|
.name("pat")
|
||||||
|
.module(UtilityModuleDefinition.UTILITY)
|
||||||
|
.templated(true)
|
||||||
|
.supportsEmbedException(true)
|
||||||
|
.async(true)
|
||||||
|
.slashCommandConfig(slashCommandConfig)
|
||||||
|
.parameters(parameters)
|
||||||
|
.help(helpInfo)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FeatureDefinition getFeature() {
|
||||||
|
return ImageGenerationFeatureDefinition.IMAGE_GENERATION;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,7 +73,7 @@ public class Triggered extends AbstractConditionableCommand {
|
|||||||
AttachedFile file = AttachedFile
|
AttachedFile file = AttachedFile
|
||||||
.builder()
|
.builder()
|
||||||
.file(triggeredGifFile)
|
.file(triggeredGifFile)
|
||||||
.fileName("avatar.gif")
|
.fileName("triggered.gif")
|
||||||
.build();
|
.build();
|
||||||
messageToSend.getAttachedFiles().add(file);
|
messageToSend.getAttachedFiles().add(file);
|
||||||
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel()))
|
return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel()))
|
||||||
@@ -96,7 +96,7 @@ public class Triggered extends AbstractConditionableCommand {
|
|||||||
AttachedFile file = AttachedFile
|
AttachedFile file = AttachedFile
|
||||||
.builder()
|
.builder()
|
||||||
.file(triggeredGifFile)
|
.file(triggeredGifFile)
|
||||||
.fileName("avatar.gif")
|
.fileName("triggered.gif")
|
||||||
.build();
|
.build();
|
||||||
messageToSend.getAttachedFiles().add(file);
|
messageToSend.getAttachedFiles().add(file);
|
||||||
return FutureUtils.toSingleFutureGeneric(interactionService.sendMessageToInteraction(messageToSend, event.getHook()))
|
return FutureUtils.toSingleFutureGeneric(interactionService.sendMessageToInteraction(messageToSend, event.getHook()))
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ public class ImageGenerationServiceBean implements ImageGenerationService {
|
|||||||
@Value("${abstracto.feature.imagegeneration.triggered.url}")
|
@Value("${abstracto.feature.imagegeneration.triggered.url}")
|
||||||
private String triggeredUrl;
|
private String triggeredUrl;
|
||||||
|
|
||||||
|
@Value("${abstracto.feature.imagegeneration.pat.url}")
|
||||||
|
private String patUrl;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private HttpService httpService;
|
private HttpService httpService;
|
||||||
@@ -29,4 +31,13 @@ public class ImageGenerationServiceBean implements ImageGenerationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public File getPatGif(String imageUrl) {
|
||||||
|
try {
|
||||||
|
return httpService.downloadFileToTempFile(patUrl.replace("{1}", imageUrl));
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new AbstractoRunTimeException(String.format("Failed to download pat gif for url %s with error %s", imageUrl, e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,3 +3,6 @@ abstracto.featureFlags.imageGeneration.enabled=false
|
|||||||
|
|
||||||
abstracto.feature.imagegeneration.triggered.url=http://${PRIVATE_REST_API_HOST}:${PRIVATE_REST_API_PORT}/memes/triggered/file.gif?url={1}
|
abstracto.feature.imagegeneration.triggered.url=http://${PRIVATE_REST_API_HOST}:${PRIVATE_REST_API_PORT}/memes/triggered/file.gif?url={1}
|
||||||
abstracto.feature.imagegeneration.triggered.imagesize=128
|
abstracto.feature.imagegeneration.triggered.imagesize=128
|
||||||
|
|
||||||
|
abstracto.feature.imagegeneration.pat.url=http://${PRIVATE_REST_API_HOST}:${PRIVATE_REST_API_PORT}/memes/pat/file.gif?url={1}
|
||||||
|
abstracto.feature.imagegeneration.pat.imagesize=128
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||||
|
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
|
||||||
|
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
|
||||||
|
<include file="seedData/data.xml" relativeToChangelogFile="true"/>
|
||||||
|
</databaseChangeLog>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||||
|
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
|
||||||
|
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
|
||||||
|
<property name="utilityModule" value="(SELECT id FROM module WHERE name = 'utility')"/>
|
||||||
|
<property name="imageGenerationFeature" value="(SELECT id FROM feature WHERE key = 'imageGeneration')"/>
|
||||||
|
|
||||||
|
<changeSet author="Sheldan" id="pat-command">
|
||||||
|
<insert tableName="command">
|
||||||
|
<column name="name" value="pat"/>
|
||||||
|
<column name="module_id" valueComputed="${utilityModule}"/>
|
||||||
|
<column name="feature_id" valueComputed="${imageGenerationFeature}"/>
|
||||||
|
</insert>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
|
||||||
|
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
|
||||||
|
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
|
||||||
|
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
|
||||||
|
<include file="command.xml" relativeToChangelogFile="true"/>
|
||||||
|
</databaseChangeLog>
|
||||||
@@ -7,4 +7,5 @@
|
|||||||
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
|
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
|
||||||
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
|
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
|
||||||
<include file="1.5.15/collection.xml" relativeToChangelogFile="true"/>
|
<include file="1.5.15/collection.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="1.5.19/collection.xml" relativeToChangelogFile="true"/>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
@@ -4,4 +4,5 @@ import java.io.File;
|
|||||||
|
|
||||||
public interface ImageGenerationService {
|
public interface ImageGenerationService {
|
||||||
File getTriggeredGif(String imageUrl);
|
File getTriggeredGif(String imageUrl);
|
||||||
|
File getPatGif(String imageUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
80
python/components/image-gen/python/endpoints/pat.py
Normal file
80
python/components/image-gen/python/endpoints/pat.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
from __main__ import app
|
||||||
|
|
||||||
|
from PIL import Image, ImageOps
|
||||||
|
from flask import request
|
||||||
|
import requests
|
||||||
|
import validators
|
||||||
|
import logging
|
||||||
|
import io
|
||||||
|
|
||||||
|
from utils import flask_utils
|
||||||
|
|
||||||
|
|
||||||
|
sprite_size = 112
|
||||||
|
sprites = 5
|
||||||
|
allowed_content_length = 4_000_000
|
||||||
|
allowed_image_formats = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif']
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/memes/pat/file.gif') # to directly embed, discord _requires_ this file ending, it seems
|
||||||
|
def pat_animated():
|
||||||
|
url = request.args.get('url')
|
||||||
|
if not validators.url(url):
|
||||||
|
return 'no valid url', 400
|
||||||
|
session = requests.Session()
|
||||||
|
response = session.head(url)
|
||||||
|
content_type = response.headers['content-type']
|
||||||
|
|
||||||
|
if content_type not in allowed_image_formats:
|
||||||
|
return f'Incorrect image type {content_type}', 400
|
||||||
|
|
||||||
|
actual_content_length = int(response.headers['content-length'])
|
||||||
|
if actual_content_length > allowed_content_length:
|
||||||
|
return f'Image too large {actual_content_length}', 400
|
||||||
|
|
||||||
|
image_file = requests.get(url, stream=True)
|
||||||
|
input_image = Image.open(io.BytesIO(image_file.content))
|
||||||
|
original_input_image = input_image
|
||||||
|
frame_count = original_input_image.n_frames
|
||||||
|
old_width, old_height = input_image.size
|
||||||
|
with Image.open('resources/img/pet_sprite.png') as pat_background:
|
||||||
|
pat_background = pat_background.convert('RGBA')
|
||||||
|
pat_width, pat_height = pat_background.size
|
||||||
|
pat_ratio = old_width / sprite_size
|
||||||
|
original_background_images = []
|
||||||
|
for i in range(sprites):
|
||||||
|
background_part = pat_background.crop((i * sprite_size, 0, (i + 1) * sprite_size, pat_height))
|
||||||
|
original_background_images.append(background_part)
|
||||||
|
desired_new_pat_width = int(sprite_size * pat_ratio)
|
||||||
|
resized_background_images = []
|
||||||
|
for background_image in original_background_images:
|
||||||
|
if pat_ratio > 1:
|
||||||
|
resized_background = background_image.resize((desired_new_pat_width, pat_height))
|
||||||
|
else:
|
||||||
|
resized_background = ImageOps.contain(background_image, (desired_new_pat_width, pat_height))
|
||||||
|
resized_background_images.append(resized_background)
|
||||||
|
if content_type == 'image/gif':
|
||||||
|
logging.info(f'Rendering pet for gif.')
|
||||||
|
old_frames = []
|
||||||
|
for frame_index in range(frame_count):
|
||||||
|
input_image.seek(frame_index)
|
||||||
|
frame = input_image.convert('RGBA')
|
||||||
|
old_frames.append(frame)
|
||||||
|
frames = []
|
||||||
|
for index, old_frame in enumerate(old_frames):
|
||||||
|
frame = Image.new('RGBA', (old_width, old_height), (0, 0, 0, 0))
|
||||||
|
frame.paste(old_frame, (0, 0), old_frame)
|
||||||
|
frame_background_image = resized_background_images[index % sprites]
|
||||||
|
frame.paste(frame_background_image, (0, 0), frame_background_image)
|
||||||
|
frames.append(frame)
|
||||||
|
return flask_utils.serve_pil_gif_image(frames, int(original_input_image.info['duration']))
|
||||||
|
else:
|
||||||
|
frames = []
|
||||||
|
logging.info(f'Rendering pet for static image.')
|
||||||
|
input_image = input_image.convert('RGBA')
|
||||||
|
for background_image in resized_background_images:
|
||||||
|
frame = Image.new('RGBA', (old_width, old_height), (0, 0, 0, 0))
|
||||||
|
frame.paste(input_image, (0, 0), input_image)
|
||||||
|
frame.paste(background_image, (0, 0), background_image)
|
||||||
|
frames.append(frame)
|
||||||
|
return flask_utils.serve_pil_gif_image(frames, 100)
|
||||||
BIN
python/components/image-gen/resources/img/pet_sprite.png
Normal file
BIN
python/components/image-gen/resources/img/pet_sprite.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Reference in New Issue
Block a user