Skip to content

Commit

Permalink
[AB-xxx] adding bonk image generation command
Browse files Browse the repository at this point in the history
fixing offline streamer handling not being able to handle streamer without current sessions
  • Loading branch information
Sheldan committed Dec 26, 2023
1 parent 8efedf6 commit af8206c
Show file tree
Hide file tree
Showing 9 changed files with 272 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -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 Bonk 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 BONK_EMBED_TEMPLATE_KEY = "bonk_response";

@Value("${abstracto.feature.imagegeneration.bonk.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 bonkGifFile = imageGenerationService.getBonkGif(member.getEffectiveAvatar().getUrl(imageSize));
MessageToSend messageToSend = templateService.renderEmbedTemplate(BONK_EMBED_TEMPLATE_KEY, new Object());
// template support does not support binary files
AttachedFile file = AttachedFile
.builder()
.file(bonkGifFile)
.fileName("bonk.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 bonkGifFile = imageGenerationService.getBonkGif(targetMember.getEffectiveAvatar().getUrl(imageSize));
MessageToSend messageToSend = templateService.renderEmbedTemplate(BONK_EMBED_TEMPLATE_KEY, new Object());
// template support does not support binary files
AttachedFile file = AttachedFile
.builder()
.file(bonkGifFile)
.fileName("bonk.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("bonk")
.build();

return CommandConfiguration.builder()
.name("bonk")
.module(UtilityModuleDefinition.UTILITY)
.templated(true)
.supportsEmbedException(true)
.async(true)
.slashCommandConfig(slashCommandConfig)
.parameters(parameters)
.help(helpInfo)
.build();
}

@Override
public FeatureDefinition getFeature() {
return ImageGenerationFeatureDefinition.IMAGE_GENERATION;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ public class ImageGenerationServiceBean implements ImageGenerationService {
@Value("${abstracto.feature.imagegeneration.pat.url}")
private String patUrl;

@Value("${abstracto.feature.imagegeneration.bonk.url}")
private String bonkUrl;

@Autowired
private HttpService httpService;

Expand All @@ -40,4 +43,13 @@ public File getPatGif(String imageUrl) {
}
}

@Override
public File getBonkGif(String imageUrl) {
try {
return httpService.downloadFileToTempFile(bonkUrl.replace("{1}", imageUrl));
} catch (IOException e) {
throw new AbstractoRunTimeException(String.format("Failed to download bonk gif for url %s with error %s", imageUrl, e.getMessage()));
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ 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

abstracto.feature.imagegeneration.bonk.url=http://${PRIVATE_REST_API_HOST}:${PRIVATE_REST_API_PORT}/memes/bonk/file.gif?url={1}
abstracto.feature.imagegeneration.bonk.imagesize=128
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@
<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">
<changeSet author="Sheldan" id="pat_bonk-commands">
<insert tableName="command">
<column name="name" value="pat"/>
<column name="module_id" valueComputed="${utilityModule}"/>
<column name="feature_id" valueComputed="${imageGenerationFeature}"/>
</insert>
<insert tableName="command">
<column name="name" value="bonk"/>
<column name="module_id" valueComputed="${utilityModule}"/>
<column name="feature_id" valueComputed="${imageGenerationFeature}"/>
</insert>
</changeSet>

</databaseChangeLog>
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
public interface ImageGenerationService {
File getTriggeredGif(String imageUrl);
File getPatGif(String imageUrl);
File getBonkGif(String imageUrl);
}
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,8 @@ public void processOfflineStreamer(AServer server, Streamer streamer, Map<Long,
}
log.info("Streamer {} went offline.", streamerId);
if (deleteFlagValues.computeIfAbsent(streamer.getServer().getId(),
aLong -> featureModeService.featureModeActive(TwitchFeatureDefinition.TWITCH, aLong, TwitchFeatureMode.DELETE_NOTIFICATION))) {
aLong -> featureModeService.featureModeActive(TwitchFeatureDefinition.TWITCH, aLong, TwitchFeatureMode.DELETE_NOTIFICATION))
&& streamer.getCurrentSession() != null) {
Long channelId = streamer.getCurrentSession().getChannel().getId();
Long messageId = streamer.getCurrentSession().getId();
messageService.deleteMessageInChannelInServer(streamer.getServer().getId(), channelId, messageId).thenAccept(unused -> {
Expand Down
99 changes: 99 additions & 0 deletions python/components/image-gen/python/endpoints/bonk.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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


bonk_angles = [40, 45, -5, 0, 5, 45, -40, 45, -30, -5, 50, 40, 5, 0, -45, 15, 5, 40, 0, -45, 5, -40, 60, -50, -40]
allowed_content_length = 4_000_000
allowed_image_formats = ['image/png', 'image/jpeg', 'image/jpg', 'image/gif']
gif_speedup_factor = 2


def get_avatar_height_factor(angle):
return max(0.1, angle * -1 / 45)

@app.route('/memes/bonk/file.gif') # to directly embed, discord _requires_ this file ending, it seems
def bonk_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
old_width, old_height = input_image.size
with Image.open('resources/img/newspaper.png') as newspaper_image:
newspaper_image = newspaper_image.convert('RGBA')
newspaper_width, newspaper_height = newspaper_image.size
newspaper_ratio = old_width / newspaper_width
desired_new_newspaper_width = int(newspaper_width * newspaper_ratio)
desired_newspaper_height = newspaper_height
if newspaper_ratio > 1:
newspaper_image = newspaper_image.resize((desired_new_newspaper_width, desired_newspaper_height))
else:
newspaper_image = ImageOps.contain(newspaper_image, (desired_new_newspaper_width, desired_newspaper_height))
new_newspaper_width, new_newspaper_height = newspaper_image.size
new_total_height = new_newspaper_height
if content_type == 'image/gif':
logging.info(f'Rendering bonk for gif.')
frame_count = original_input_image.n_frames
old_frames = []
for frame_index in range(frame_count):
input_image.seek(frame_index)
frame = input_image.convert('RGBA')
old_frames.append(frame)
frames = []
current_factor = 1
for index, old_frame in enumerate(old_frames):
angle = bonk_angles[index % len(bonk_angles)]
frame = Image.new('RGBA', (old_width, new_total_height), (0, 0, 0, 0))
current_factor *= (1 - get_avatar_height_factor(angle))
current_factor += 0.2
current_factor = min(1, current_factor)
avatar_height_factor = current_factor
target_height = int(max(1, old_height / 2 * avatar_height_factor))
old_frame = old_frame.resize((int(old_width / 2), target_height))
target_position = int(old_height / 2 + (1 - avatar_height_factor) * old_height / 2)
frame.paste(old_frame, (int(old_width / 2), target_position), old_frame)
rotated_news_paper = newspaper_image.rotate(angle, center=(0, new_newspaper_height))
frame.paste(rotated_news_paper, (0, 0), rotated_news_paper)
frames.append(frame)
return flask_utils.serve_pil_gif_image(frames, (int(original_input_image.info['duration']) / gif_speedup_factor))
else:

frames = []
logging.info(f'Rendering bonk for static image.')
input_image = input_image.convert('RGBA')
current_factor = 1
for angle in bonk_angles:
frame = Image.new('RGBA', (old_width, old_height), (0, 0, 0, 0))
current_factor *= (1 - get_avatar_height_factor(angle))
current_factor += 0.2
current_factor = min(1, current_factor)
avatar_height_factor = current_factor
target_height = int(max(1, old_height / 2 * avatar_height_factor))
frame_input_image = input_image.resize((int(old_width / 2), target_height))
target_position = int(old_height / 2 + (1 - avatar_height_factor) * old_height / 2)
frame.paste(frame_input_image, (int(old_width / 2), target_position), frame_input_image)
rotated_news_paper = newspaper_image.rotate(angle, center=(0, new_newspaper_height))
frame.paste(rotated_news_paper, (0, 0), rotated_news_paper)
frames.append(frame)
return flask_utils.serve_pil_gif_image(frames, 50)

4 changes: 2 additions & 2 deletions python/components/image-gen/python/endpoints/pat.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def pat_animated():
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.')
logging.info(f'Rendering pat for gif.')
old_frames = []
for frame_index in range(frame_count):
input_image.seek(frame_index)
Expand All @@ -70,7 +70,7 @@ def pat_animated():
return flask_utils.serve_pil_gif_image(frames, int(original_input_image.info['duration']))
else:
frames = []
logging.info(f'Rendering pet for static image.')
logging.info(f'Rendering pat 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))
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit af8206c

Please sign in to comment.