[AB-xxx] adding amongusText image generation

This commit is contained in:
Sheldan
2024-01-24 08:18:36 +01:00
parent 6030904d21
commit 71c18f8348
97 changed files with 337 additions and 1 deletions

View File

@@ -0,0 +1,133 @@
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.events.interaction.command.SlashCommandInteractionEvent;
import org.springframework.beans.factory.annotation.Autowired;
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 AmongusText extends AbstractConditionableCommand {
public static final String TEXT_PARAMETER_KEY = "text";
@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 AMONGUS_TEXT_EMBED_TEMPLATE_KEY = "amongusText_response";
@Override
public CompletableFuture<CommandResult> executeAsync(CommandContext commandContext) {
String text = (String) commandContext.getParameters().getParameters().get(0);
File amongusTextImage = imageGenerationService.getAmongusTextImage(text);
MessageToSend messageToSend = templateService.renderEmbedTemplate(AMONGUS_TEXT_EMBED_TEMPLATE_KEY, new Object());
// template support does not support binary files
AttachedFile file = AttachedFile
.builder()
.file(amongusTextImage)
.fileName("amongus.png")
.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();
String text = slashCommandParameterService.getCommandOption(TEXT_PARAMETER_KEY, event, String.class);
File amongusTextImage = imageGenerationService.getAmongusTextImage(text);
MessageToSend messageToSend = templateService.renderEmbedTemplate(AMONGUS_TEXT_EMBED_TEMPLATE_KEY, new Object());
// template support does not support binary files
AttachedFile file = AttachedFile
.builder()
.file(amongusTextImage)
.fileName("amongus.png")
.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(TEXT_PARAMETER_KEY)
.type(String.class)
.remainder(true)
.templated(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("amongustext")
.build();
return CommandConfiguration.builder()
.name("amongusText")
.module(UtilityModuleDefinition.UTILITY)
.templated(true)
.supportsEmbedException(true)
.async(true)
.slashCommandConfig(slashCommandConfig)
.parameters(parameters)
.help(helpInfo)
.build();
}
@Override
public FeatureDefinition getFeature() {
return ImageGenerationFeatureDefinition.IMAGE_GENERATION;
}
}

View File

@@ -1,7 +1,9 @@
package dev.sheldan.abstracto.imagegeneration.service;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.exception.RequestException;
import dev.sheldan.abstracto.core.service.HttpService;
import dev.sheldan.abstracto.imagegeneration.exception.AmongusTextRequestException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@@ -22,6 +24,9 @@ public class ImageGenerationServiceBean implements ImageGenerationService {
@Value("${abstracto.feature.imagegeneration.bonk.url}")
private String bonkUrl;
@Value("${abstracto.feature.imagegeneration.amongusText.url}")
private String amongusTextUrl;
@Autowired
private HttpService httpService;
@@ -52,4 +57,15 @@ public class ImageGenerationServiceBean implements ImageGenerationService {
}
}
@Override
public File getAmongusTextImage(String text) {
try {
return httpService.downloadFileToTempFile(amongusTextUrl.replace("{1}", text));
} catch (IOException e) {
throw new AbstractoRunTimeException(String.format("Failed to download amongus text with error %s", text, e.getMessage()));
} catch (RequestException exception) {
throw new AmongusTextRequestException(text, exception.getErrorMessage());
}
}
}

View File

@@ -8,4 +8,6 @@ abstracto.feature.imagegeneration.pat.url=http://${PRIVATE_REST_API_HOST}:${PRIV
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
abstracto.feature.imagegeneration.bonk.imagesize=128
abstracto.feature.imagegeneration.amongusText.url=http://${PRIVATE_REST_API_HOST}:${PRIVATE_REST_API_PORT}/memes/amogus/text?text={1}

View File

@@ -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>

View File

@@ -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="amongusText-commands">
<insert tableName="command">
<column name="name" value="amongusText"/>
<column name="module_id" valueComputed="${utilityModule}"/>
<column name="feature_id" valueComputed="${imageGenerationFeature}"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -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>

View File

@@ -8,4 +8,5 @@
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="1.5.15/collection.xml" relativeToChangelogFile="true"/>
<include file="1.5.19/collection.xml" relativeToChangelogFile="true"/>
<include file="1.5.22/collection.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,26 @@
package dev.sheldan.abstracto.imagegeneration.exception;
import dev.sheldan.abstracto.core.exception.AbstractoTemplatableException;
import dev.sheldan.abstracto.imagegeneration.model.exception.AmongusTextRequestExceptionModel;
public class AmongusTextRequestException extends AbstractoTemplatableException {
private final AmongusTextRequestExceptionModel model;
public AmongusTextRequestException(String inputText, String errorMessage) {
this.model = AmongusTextRequestExceptionModel
.builder()
.inputText(inputText)
.errorMessage(errorMessage)
.build();
}
@Override
public String getTemplateName() {
return "amongusText_exception";
}
@Override
public Object getTemplateModel() {
return model;
}
}

View File

@@ -0,0 +1,13 @@
package dev.sheldan.abstracto.imagegeneration.model.exception;
import lombok.Builder;
import lombok.Getter;
import java.io.Serializable;
@Getter
@Builder
public class AmongusTextRequestExceptionModel implements Serializable {
private String inputText;
private String errorMessage;
}

View File

@@ -6,4 +6,5 @@ public interface ImageGenerationService {
File getTriggeredGif(String imageUrl);
File getPatGif(String imageUrl);
File getBonkGif(String imageUrl);
File getAmongusTextImage(String text);
}

View File

@@ -1,6 +1,7 @@
package dev.sheldan.abstracto.core.service;
import dev.sheldan.abstracto.core.exception.RequestException;
import dev.sheldan.abstracto.core.utils.FileService;
import okhttp3.OkHttpClient;
import okhttp3.Request;
@@ -25,6 +26,13 @@ public class HttpServiceBean implements HttpService {
Request request = new Request.Builder().url(url).get().build();
File tempFile = fileService.createTempFile(Math.random() + "");
Response execute = client.newCall(request).execute();
if(!execute.isSuccessful()) {
throw RequestException
.builder()
.errorMessage(execute.body().string())
.httpCode(execute.code())
.build();
}
fileService.writeBytesToFile(tempFile, execute.body().bytes());
return tempFile;
}

View File

@@ -0,0 +1,11 @@
package dev.sheldan.abstracto.core.exception;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class RequestException extends RuntimeException {
private String errorMessage; // we just assume the body will be a plain text instead
private Integer httpCode;
}

View File

@@ -0,0 +1,85 @@
from __main__ import app
from PIL import Image, ImageOps
from flask import request
import logging
import math
import re
import random
from utils import flask_utils
vertical_padding = 5
horizontal_padding = 5
max_chars = 2000
chars_per_row = 50
space_width = 80
character_height = 120
@app.route('/memes/amogus/text')
def generate_amogus_text():
text = request.args.get('text', type=str)
if text is None:
return 'no text', 400
if len(text) > max_chars:
return f'too long text, max {max_chars}', 400
text = text.lower()
text = re.sub(r'[^a-z!\\? ]', "", text)
if len(text) == 0:
return 'No valid characters found.', 400
logging.info(f'Rendering amogus text.')
image_cache = {}
images_to_use = []
try:
for character in text:
if character == ' ':
images_to_use.append(None)
continue
# manual correction for filenames
if character == '?':
character = 'zq'
if character == '!':
character = 'zx'
chosen_sub_image = random.randint(1, 3)
cache_key = f'{character}{chosen_sub_image}'
if cache_key not in image_cache:
character_image = Image.open(f'resources/img/amogus/crewmate-{character}{chosen_sub_image}.png').__enter__()
old_character_width, old_character_height = character_image.size
character_ratio = old_character_height / character_height
desired_width = int(old_character_width * character_ratio)
resized_character_image = ImageOps.contain(character_image, (desired_width, character_height))
image_cache[cache_key] = resized_character_image
image_to_use = resized_character_image
else:
image_to_use = image_cache[cache_key]
images_to_use.append(image_to_use)
# the line length is defined by the amount of characters, not once a certain width is reached
lines = [images_to_use[i:i + chars_per_row] for i in range(0, len(images_to_use), chars_per_row)]
# calculate the length of each line, and then take the widest one
line_widths = [sum(image_to_use.size[0] + horizontal_padding if image_to_use is not None else space_width for image_to_use in character_line) for character_line in lines]
canvas_width = max(line_widths)
row_count = math.ceil(len(text) / chars_per_row)
canvas_height = row_count * (character_height + vertical_padding)
drawing_board = Image.new('RGBA', (canvas_width, canvas_height), (0, 0, 0, 0))
start_x = 0
start_y = 0
for index, image_to_use in enumerate(images_to_use):
if index > 0 and (index % chars_per_row) == 0:
start_y += character_height + vertical_padding
start_x = 0
if image_to_use is not None:
drawing_board.paste(image_to_use, (start_x, start_y), image_to_use)
start_x += image_to_use.size[0] + horizontal_padding
else:
start_x += space_width
finally:
for cached_image in image_cache.values():
cached_image.__exit__(None, None, None)
return flask_utils.serve_pil_image(drawing_board)

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB