diff --git a/automeme.py b/automeme.py index 3e4436d..83cea67 100644 --- a/automeme.py +++ b/automeme.py @@ -4,7 +4,7 @@ import re import traceback from typing import Callable, TypeVar import meme_generator -from meme_generator import WebcomicPanel, OneCharacterWebcomicPanel, TwoCharacterWebcomicPanel, TitleCardWebcomicPanel, add_watermark, create_webcomic +from meme_generator import AnimatedImage, WebcomicPanel, OneCharacterWebcomicPanel, TwoCharacterWebcomicPanel, TitleCardWebcomicPanel, add_watermark, create_webcomic from RDramaAPIInterface import RDramaAPIInterface from datetime import datetime, timedelta from os.path import exists, join, realpath, split @@ -16,6 +16,7 @@ import os from markdown import markdown from bs4 import BeautifulSoup from utils import get_real_filename +from pympler import tracker, asizeof, classtracker ANTISPAM_MESSAGES = [ 'i have a meme for u :marseyshy:', @@ -45,20 +46,20 @@ ANTISPAM_MESSAGES = [ TEST_MODE = False DRY_MODE = False BETA_MODE = False -TEST_AUTH_TOKEN = "lawoSNzuNeBRJBld0boApOceCNSEqBhiRu0aoUWh9kTK7AV37NECZoAK-mEUJ1PM1SsTfTY4f3t_LjooMq4QgPLqPC-F7LNGHQ6_0RFacZmIvC2ixOHPKM821RroJexn" +TEST_AUTH_TOKEN = "2PR7FAqx0UVM71HayM33p1OTy_zFGOK1rJlCNi1qzvFJqQGK6NOm7I-mSfYjZgs02FU_LqLTPqDa8KN-LUiGSJ1idFfE5jKnG3DUk9rQjn_ZrAtG_VU9GK5F_EGmixg1" MINUTES_BEFORE_FORCED_SHUTDOWN = 10 DB_FILENAME = "automeme_database.db" -PAGES_TO_SCAN = 5 +MAX_PAGES_TO_SCAN = 10 AUTOMEME_ID = 13427 ALLOWED_COMMENTS_PER_POST = 20 ALLOWED_COMMENTS_PER_USER_PER_DAY = 20 -SOY_VS_CHAD_TRIGGER_CHANGE = 1.0 +SOY_VS_CHAD_TRIGGER_CHANGE = 0.5 MODERN_MEME_WITH_MARSEY_TRIGGER_CHANGE = 0.01 MODERN_MEME_WITH_IMAGE_TRIGGER_CHANGE = 0.05 CLASSIC_MEME_WITH_MARSEY_TRIGGER_CHANGE = 0.02 CLASSIC_MEME_WITH_IMAGE_TRIGGER_CHANGE = 0.1 -WEBCOMIC_TRIGGER_CHANCE = 1.0 +WEBCOMIC_TRIGGER_CHANCE = 0.5 FREE_POSTS = [6, 97416, 98286] RESTRICTED_POSTS = [5, 16583, 75878, 35835] @@ -111,7 +112,7 @@ class TextLine: @property def is_argument_line(self): - return len(self.emojis) == 2 and (len(self.captions) == 2 or len(self.captions) == 1) and isinstance(self.line[0], Emoji) and isinstance(self.line[2], Emoji) + return len(self.emojis) == 2 and ((len(self.captions) == 0) or (len(self.captions) == 2 or len(self.captions) == 1) and isinstance(self.line[0], Emoji) and isinstance(self.line[2], Emoji)) @property def is_pure_text_line(self): @@ -125,6 +126,12 @@ class TextLine: def is_image_line(self): return len(self.images) == 1 and len(self.captions) == 0 + def __str__(self): + return str(self.line) + + def __repr__(self) -> str: + return str(self.line) + class TextElement(): pass @@ -211,9 +218,10 @@ def strip_markdown(markdown_string): text = re.sub("@(.*?)\s", "", text) text = re.sub("!slots.*?\s", "", text) + text = re.sub("(?i)detrans lives matter", "", text) text = re.sub("(?i)trans lives matter", "", text) - return text + return text.strip() def remove_duplicates(list): return [json.loads(j) for j in set([json.dumps(i) for i in list])] @@ -254,7 +262,8 @@ def get_eligible_comments(rdrama : RDramaAPIInterface, session : Session): comments = remove_duplicates(comments) #Remove the duplicates print([comment['id'] for comment in comments]) - + ScriptCall.register(session) + session.commit() return comments T = TypeVar('T') @@ -272,14 +281,15 @@ def extract_directives(string : str): list = [i.lower()[2:] for i in list] return list -def create_comment_message(chud: bool, pizza: bool, bird : bool, marsey : bool): +def create_comment_message(chud: bool, pizza: bool, bird : bool, marsey : bool, mention : str = None): if (marsey): return ":marseyshy:" message = choice(ANTISPAM_MESSAGES) +"\n" - + if (mention != None): + message += f"@{mention}\n" message += f"" if (chud): @@ -289,6 +299,176 @@ def create_comment_message(chud: bool, pizza: bool, bird : bool, marsey : bool): return message +def handle_comment(comment : dict, rdrama : RDramaAPIInterface, session : Session, chud: bool, pizza: bool, bird : bool, is_marsey : bool, starting_directives : 'list[str]'= [], mention : str = None): + try: + begin = datetime.now() + under_post_limit = Post.get_number_of_replies(comment['post_id'], session) < ALLOWED_COMMENTS_PER_POST + under_user_limit = User.get_number_of_comments(0 if comment['author'] == '👻' else comment['author']['id'], session) < ALLOWED_COMMENTS_PER_USER_PER_DAY + has_not_replied_to_comment = Comment.get_comment(comment['id'], session) is None + is_not_restricted_post = comment['post_id'] not in RESTRICTED_POSTS + if (not (under_post_limit and under_user_limit and (has_not_replied_to_comment or len(starting_directives) != 0) and is_not_restricted_post)): + return + + comment_text = comment['body'] + directives = extract_directives(comment_text) if len(starting_directives) == 0 else starting_directives + cleaned_comment_text = strip_markdown(comment_text) + + if (cleaned_comment_text == "" and len(directives) > 0): + parent_id = comment['parent_comment_id'] + username = None if comment['author'] == '👻' else comment['author']['username'] + parent = rdrama.get_comment(parent_id) + handle_comment(parent, rdrama, session, chud, pizza, bird, is_marsey, starting_directives=directives, mention=username) + return + + if ("meme" in directives or comment['post_id'] in FREE_POSTS): + random_float = 0.0 + else: + random_float = random() + + comment_lines = cleaned_comment_text.split("\n") + comment_lines = [comment_line for comment_line in comment_lines if comment_line != ""] + element_lines = [TextLine(line) for line in comment_lines] + + argument_lines_count, dialog_lines_count, text_lines_count, big_marsey_lines_count = 0,0,0,0 + + dialog_lines = list(filter(lambda a : a.is_dialogue_line, element_lines)) + argument_lines = list(filter(lambda a : a.is_argument_line, element_lines)) + pure_text_lines = list(filter(lambda a : a.is_pure_text_line, element_lines)) + big_marsey_lines = list(filter(lambda a : a.is_big_marsey_line, element_lines)) + image_lines = list(filter(lambda a : a.is_image_line, element_lines)) + + argument_lines_count = len(argument_lines) + dialog_lines_count = len(dialog_lines) + pure_text_lines_count = len(pure_text_lines) + big_marsey_lines_count = len(big_marsey_lines) + image_lines_count = len(image_lines) + + image = None + if (dialog_lines_count == 2): + print(f"[{comment['id']}] SOY_VS_CHAD") + if (random_float <= SOY_VS_CHAD_TRIGGER_CHANGE): + #Soy vs Chad + + line1 = dialog_lines[0] + line2 = dialog_lines[1] + + emoji1 = line1.emojis[0].emoji + emoji2 = line2.emojis[0].emoji + caption1 = line1.text + caption2 = line2.text + + image = meme_generator.create_soy_vs_chad_meme(emoji1, emoji2, caption1, caption2) + elif (big_marsey_lines_count == 1 and pure_text_lines_count == 1): + print(f"[{comment['id']}] MODERN_MEME_WITH_MARSEY") + if (random_float <= MODERN_MEME_WITH_MARSEY_TRIGGER_CHANGE): + + # Modern Meme with Marsey + text_line = pure_text_lines[0] + marsey_line = big_marsey_lines[0] + + marsey = marsey_line.emojis[0].emoji + caption = text_line.text + + image = meme_generator.create_modern_meme_from_emoji(marsey, caption) + elif (image_lines_count == 1 and pure_text_lines_count == 1): + print(f"[{comment['id']}] MODERN_MEME_WITH_IMAGE") + if (random_float <= MODERN_MEME_WITH_IMAGE_TRIGGER_CHANGE): + # Modern Meme with Image + text_line = pure_text_lines[0] + image_line = image_lines[0] + + image = image_line.images[0] + full_image_url = image.url + caption = text_line.text + + image = meme_generator.create_modern_meme_from_url(full_image_url, caption) + elif (big_marsey_lines_count == 1 and pure_text_lines_count == 2): + print(f"[{comment['id']}] CLASSIC_MEME_WITH_MARSEY") + if (random_float <= CLASSIC_MEME_WITH_MARSEY_TRIGGER_CHANGE): + # Classic Meme with big marsey + top_text_line = pure_text_lines[0] + bottom_text_line = pure_text_lines[1] + marsey_line = big_marsey_lines[0] + + emoji = marsey_line.emojis[0].emoji + top_caption = top_text_line.text + bottom_caption = bottom_text_line.text + image = meme_generator.create_classic_meme_from_emoji(emoji, top_caption, bottom_caption) + elif (image_lines_count == 1 and pure_text_lines_count == 2): + print(f"[{comment['id']}] CLASSIC_MEME_WITH_IMAGE") + if (random_float <= CLASSIC_MEME_WITH_IMAGE_TRIGGER_CHANGE): + # Classic Meme with Image + top_text_line = pure_text_lines[0] + bottom_text_line = pure_text_lines[1] + image_line = image_lines[0] + + image = image_line.images[0] + full_image_url = image.url + top_caption = top_text_line.text + bottom_caption = bottom_text_line.text + + image = meme_generator.create_classic_meme_from_url(full_image_url, top_caption, bottom_caption) + elif (argument_lines_count + dialog_lines_count >= 2): + print(f"[{comment['id']}] WEBCOMIC") + if (random_float <= WEBCOMIC_TRIGGER_CHANCE): + panels : 'list[WebcomicPanel]' = [] + + for element_line in element_lines: + if element_line.is_dialogue_line: + caption = element_line.text + emoji = element_line.emojis[0].emoji + if len(caption) > 100: + in_background = True + else: + in_background = False + oneCharacterWebcomicPanel = OneCharacterWebcomicPanel(emoji, caption, in_background) + panels.append(oneCharacterWebcomicPanel) + elif element_line.is_argument_line: + + if len(element_line.captions) == 2: + left_caption = element_line.captions[0].text + right_caption = element_line.captions[1].text + elif (len(element_line.captions) == 1): + left_caption = element_line.captions[0].text + right_caption = "" + else: + left_caption = "" + right_caption = "" + left_emoji = element_line.emojis[0].emoji + right_emoji = element_line.emojis[1].emoji + twoCharacterWebcomicPanel = TwoCharacterWebcomicPanel(left_emoji, left_caption, right_emoji, right_caption) + panels.append(twoCharacterWebcomicPanel) + else: + panels.append(TitleCardWebcomicPanel(element_line.text)) + + image = create_webcomic(panels) + print(asizeof.asized(image, detail=1).format()) + if image != None: + print(f"[{comment['id']}] posting...") + image = add_watermark(image, comment['author']['username']) + user_id = 0 if comment['author'] == '👻' else comment['author']['id'] + parent_comment_id = comment['id'] + post_id = comment['post_id'] + message = create_comment_message(chud, pizza, bird, is_marsey, mention=mention) + if not DRY_MODE: + automeme_comment_id = comment_with_image(message, image, comment['id'], comment['post_id']) + else: + automeme_comment_id = None + image.save(get_real_filename(f"dry/{comment['id']}.webp")) + + Comment.create_new_comment(parent_comment_id, automeme_comment_id, session) + if post_id not in FREE_POSTS: + Post.increment_replies(post_id, session) + User.increase_number_of_comments(user_id, session) + else: + print(f"[{comment['id']}] is a free post.") + end = datetime.now() + print(end-begin) + else: + Comment.create_new_comment(comment['id'], None, session) + except BaseException as e: + print(f"YIKERINOS! GOT AN EXCEPTION: {e}") + traceback.print_exc() def main_processing_task(rdrama : RDramaAPIInterface, session : Session): is_chudded = False #Do we have the chud award? can_communicate = True #Can we send any message at all? @@ -324,179 +504,27 @@ def main_processing_task(rdrama : RDramaAPIInterface, session : Session): if can_communicate: eligible_comments = get_eligible_comments(rdrama, session) for eligible_comment in eligible_comments: - try: - begin = datetime.now() - under_post_limit = Post.get_number_of_replies(eligible_comment['post_id'], session) < ALLOWED_COMMENTS_PER_POST - under_user_limit = User.get_number_of_comments(0 if eligible_comment['author'] == '👻' else eligible_comment['author']['id'], session) < ALLOWED_COMMENTS_PER_USER_PER_DAY - has_not_replied_to_comment = Comment.get_comment(eligible_comment['id'], session) is None - is_not_restricted_post = eligible_comment['post_id'] not in RESTRICTED_POSTS - if (not (under_post_limit and under_user_limit and has_not_replied_to_comment and is_not_restricted_post)): - continue - - comment_text = eligible_comment['body'] - directives = extract_directives(comment_text) - cleaned_comment_text = strip_markdown(comment_text) - - - if ("meme" in directives or eligible_comment['post_id'] in FREE_POSTS): - random_float = 0.0 - else: - random_float = random() - - comment_lines = cleaned_comment_text.split("\n") - comment_lines = [comment_line for comment_line in comment_lines if comment_line != ""] - element_lines = [TextLine(line) for line in comment_lines] - - argument_lines_count, dialog_lines_count, text_lines_count, big_marsey_lines_count = 0,0,0,0 - - dialog_lines = list(filter(lambda a : a.is_dialogue_line, element_lines)) - argument_lines = list(filter(lambda a : a.is_argument_line, element_lines)) - pure_text_lines = list(filter(lambda a : a.is_pure_text_line, element_lines)) - big_marsey_lines = list(filter(lambda a : a.is_big_marsey_line, element_lines)) - image_lines = list(filter(lambda a : a.is_image_line, element_lines)) - - argument_lines_count = len(argument_lines) - dialog_lines_count = len(dialog_lines) - pure_text_lines_count = len(pure_text_lines) - big_marsey_lines_count = len(big_marsey_lines) - image_lines_count = len(image_lines) - - image = None - if (dialog_lines_count == 2): - print(f"[{eligible_comment['id']}] SOY_VS_CHAD") - if (random_float <= SOY_VS_CHAD_TRIGGER_CHANGE): - #Soy vs Chad - - line1 = dialog_lines[0] - line2 = dialog_lines[1] - - emoji1 = line1.emojis[0].emoji - emoji2 = line2.emojis[0].emoji - caption1 = line1.text - caption2 = line2.text - - image = meme_generator.create_soy_vs_chad_meme(emoji1, emoji2, caption1, caption2) - elif (big_marsey_lines_count == 1 and pure_text_lines_count == 1): - print(f"[{eligible_comment['id']}] MODERN_MEME_WITH_MARSEY") - if (random_float <= MODERN_MEME_WITH_MARSEY_TRIGGER_CHANGE): - - # Modern Meme with Marsey - text_line = pure_text_lines[0] - marsey_line = big_marsey_lines[0] - - marsey = marsey_line.emojis[0].emoji - caption = text_line.text - - image = meme_generator.create_modern_meme_from_emoji(marsey, caption) - elif (image_lines_count == 1 and pure_text_lines_count == 1): - print(f"[{eligible_comment['id']}] MODERN_MEME_WITH_IMAGE") - if (random_float <= MODERN_MEME_WITH_IMAGE_TRIGGER_CHANGE): - # Modern Meme with Image - text_line = pure_text_lines[0] - image_line = image_lines[0] - - image = image_line.images[0] - full_image_url = image.url - caption = text_line.text - - image = meme_generator.create_modern_meme_from_url(full_image_url, caption) - elif (big_marsey_lines_count == 1 and pure_text_lines_count == 2): - print(f"[{eligible_comment['id']}] CLASSIC_MEME_WITH_MARSEY") - if (random_float <= CLASSIC_MEME_WITH_MARSEY_TRIGGER_CHANGE): - # Classic Meme with big marsey - top_text_line = pure_text_lines[0] - bottom_text_line = pure_text_lines[1] - marsey_line = big_marsey_lines[0] - - emoji = marsey_line.emojis[0].emoji - top_caption = top_text_line.text - bottom_caption = bottom_text_line.text - image = meme_generator.create_classic_meme_from_emoji(emoji, top_caption, bottom_caption) - elif (image_lines_count == 1 and pure_text_lines_count == 2): - print(f"[{eligible_comment['id']}] CLASSIC_MEME_WITH_IMAGE") - if (random_float <= CLASSIC_MEME_WITH_IMAGE_TRIGGER_CHANGE): - # Classic Meme with Image - top_text_line = pure_text_lines[0] - bottom_text_line = pure_text_lines[1] - image_line = image_lines[0] - - image = image_line.images[0] - full_image_url = image.url - top_caption = top_text_line.text - bottom_caption = bottom_text_line.text - - image = meme_generator.create_classic_meme_from_url(full_image_url, top_caption, bottom_caption) - elif (argument_lines_count + dialog_lines_count >= 2): - print(f"[{eligible_comment['id']}] WEBCOMIC") - if (random_float <= WEBCOMIC_TRIGGER_CHANCE): - panels : 'list[WebcomicPanel]' = [] - - for element_line in element_lines: - if element_line.is_dialogue_line: - caption = element_line.text - emoji = element_line.emojis[0].emoji - if len(caption) > 100: - in_background = True - else: - in_background = False - oneCharacterWebcomicPanel = OneCharacterWebcomicPanel(emoji, caption, in_background) - panels.append(oneCharacterWebcomicPanel) - elif element_line.is_argument_line: - left_caption = element_line.captions[0].text - if len(element_line.captions) == 2: - right_caption = element_line.captions[1].text - else: - right_caption = "" - left_emoji = element_line.emojis[0].emoji - right_emoji = element_line.emojis[1].emoji - twoCharacterWebcomicPanel = TwoCharacterWebcomicPanel(left_emoji, left_caption, right_emoji, right_caption) - panels.append(twoCharacterWebcomicPanel) - elif element_line.is_pure_text_line: - panels.append(TitleCardWebcomicPanel(element_line.text)) - - image = create_webcomic(panels) - if image != None: - print(f"[{eligible_comment['id']}] posting...") - image = add_watermark(image, eligible_comment['author']['username']) - user_id = 0 if eligible_comment['author'] == '👻' else eligible_comment['author']['id'] - parent_comment_id = eligible_comment['id'] - post_id = eligible_comment['post_id'] - message = create_comment_message(is_chudded, is_pizzad, is_birdsite, is_marseyed) - if not DRY_MODE: - automeme_comment_id = comment_with_image(message, image, eligible_comment['id'], eligible_comment['post_id']) - else: - automeme_comment_id = None - image.save(get_real_filename(f"dry/{eligible_comment['id']}.webp")) - - Comment.create_new_comment(parent_comment_id, automeme_comment_id, session) - if post_id not in FREE_POSTS: - Post.increment_replies(post_id, session) - User.increase_number_of_comments(user_id, session) - else: - print(f"[{eligible_comment['id']}] is a free post.") - end = datetime.now() - print(end-begin) - else: - Comment.create_new_comment(eligible_comment['id'], None, session) - except BaseException as e: - print(f"YIKERINOS! GOT AN EXCEPTION: {e}") - traceback.print_exc() + handle_comment(eligible_comment, rdrama, session, is_chudded, is_pizzad, is_birdsite, is_marseyed) - ScriptCall.register(session) - def comment_chunk(time : datetime, api: RDramaAPIInterface): current_time_cur = int(time.timestamp()) # int(time.time() - 60*60*8) comments = [] + pages = 0 while True: + pages+=1 res = api.get_comments(sort="old", lower_bound=current_time_cur) if len(res['data']) == 0: break + if pages > MAX_PAGES_TO_SCAN: + print("WARNING: Had to drop some pages. Can't make an omelette without cracking a few eggs") + break comments = comments + res['data'] current_time_cur = res['data'][-1]['created_utc'] return comments -if __name__ == "__main__": - print(f"======= RUNNING AT {datetime.now().hour}:{datetime.now().minute} ======= ") + +def get_rdrama(): + global AUTOMEME_ID, OPERATOR_ID if TEST_MODE: website = "localhost" auth = TEST_AUTH_TOKEN @@ -504,7 +532,6 @@ if __name__ == "__main__": timeout = 1 AUTOMEME_ID = 6 OPERATOR_ID = 9 - ACTUALLY_CALL_OPEN_AI = False else: website = "rdrama.net" with open(get_real_filename("rdrama_auth_token"), "r") as f: @@ -512,6 +539,11 @@ if __name__ == "__main__": https = True timeout = 10 rdrama = RDramaAPIInterface(auth, website, timeout, https=https) + return rdrama + +if __name__ == "__main__": + print(f"======= RUNNING AT {datetime.now().hour}:{datetime.now().minute} ======= ") + rdrama = get_rdrama() #Set up fail safe def exitfunc(): diff --git a/meme_generator.py b/meme_generator.py index 05c9a11..4413e9d 100644 --- a/meme_generator.py +++ b/meme_generator.py @@ -10,9 +10,10 @@ from utils import get_real_filename from image_utils import ImageText from os.path import exists from random import choice +from pympler import tracker -HIGHLIGHT_MODE = False -CAPTION_FILENAME = "watermark_captions.txt" +HIGHLIGHT_MODE = False #Adds a square around the region where text is being put in a text box. Useful for debugging. +CAPTION_FILENAME = "watermark_captions.txt" #Name of the file containing watermark captions. class ColorScheme: BLACK = 0 @@ -28,7 +29,6 @@ def create_soy_vs_chad_meme(emoji1, emoji2, caption1, caption2): MIDDLE_MARGIN_ROW = 20 TEXT_ROW = 100 BOTTOM_MARGIN_ROW = 80 - total_image_size_x = 2*CONTENT_COLUMN + LEFT_MARGIN_COLUMN + RIGHT_MARGIN_COLUMN + MIDDLE_MARGIN_COLUMN total_image_size_y = TOP_MARGIN_ROW + IMAGE_ROW + MIDDLE_MARGIN_ROW + TEXT_ROW + BOTTOM_MARGIN_ROW @@ -116,9 +116,12 @@ def create_modern_meme_from_emoji(emoji: str, caption: str): return create_modern_meme(base, caption) class WebcomicPanel(): - PANEL_SIZE = 400 - FONT = "impact.ttf" - COLOR = ColorScheme.WHITE_WITH_BLACK_BORDER + ''' + A panel in a webcomic. + ''' + PANEL_SIZE = 400 #Size of a webcomic panel. + FONT = "impact.ttf" #The font to use. + COLOR = ColorScheme.WHITE_WITH_BLACK_BORDER #The color of the font. def __init__(self): pass @@ -130,6 +133,9 @@ class WebcomicPanel(): return add_text_box(base, caption, region_size, coordinates, align=align, font=self.FONT, color=self.COLOR, init_font_size=int(self.PANEL_SIZE/10)) class OneCharacterWebcomicPanel(WebcomicPanel): + ''' + Panel with one character talking. + ''' def __init__(self, emoji, caption, words_in_background): self.emoji = emoji self.caption = caption @@ -155,6 +161,9 @@ class OneCharacterWebcomicPanel(WebcomicPanel): return add_border_to_image(base) class TwoCharacterWebcomicPanel(WebcomicPanel): + ''' + Panel with two characters talking. + ''' def __init__(self, left_emoji, left_caption, right_emoji, right_caption): self.left_emoji = left_emoji self.left_caption = left_caption @@ -204,6 +213,9 @@ class TwoCharacterWebcomicPanel(WebcomicPanel): return add_border_to_image(base) class TitleCardWebcomicPanel(WebcomicPanel): + ''' + A caption in a webcomic. + ''' def __init__(self, caption): self.caption = caption @@ -221,6 +233,7 @@ def create_webcomic(layout : 'list[WebcomicPanel]'): image = AnimatedImage.new((total_image_x_size, total_image_y_size)) for i in range(len(layout)): + print(i) panel = layout[i] x = i%2 y = math.floor(i/2) @@ -228,6 +241,26 @@ def create_webcomic(layout : 'list[WebcomicPanel]'): return image def add_text_box(base : Image, caption : str, region_size : 'tuple[int, int]', coordinates : 'tuple[int, int]', font : str= "arial.ttf", init_font_size = 45, align :str = "", color = ColorScheme.BLACK): + ''' + Adds a text box, where the size of the text scales to fit the box as closely as possible. + + base: the image to put the text in. + caption: the text + region size: the size of the text box. + coordinates: the position of the text box. + font: name of the font file to use (defaults to arial) + init_font_size: the largest font size that will be used. + align: sets the alignment of the text. + Horizontal Alignments: + t: top + b: bottom + cv: center + Vertical Alignments: + l: left + r: right + ch: center + color: what colorscheme to use. + ''' font = get_real_filename(font) if caption == "": @@ -288,6 +321,9 @@ def add_text_box(base : Image, caption : str, region_size : 'tuple[int, int]', c return add_text_box(base, caption, region_size, coordinates, font=font, init_font_size=init_font_size-1, align=align, color=color) def add_text(base : Image, caption : str, region_size : 'tuple[int, int]', coordinates : 'tuple[int, int]', font : str= "arial.ttf"): + ''' + Very simple interface for adding text. + ''' font = get_real_filename(font) print(font) if caption == "": @@ -298,6 +334,9 @@ def add_text(base : Image, caption : str, region_size : 'tuple[int, int]', coord return base.paste(AnimatedImage.from_image(line_image.image), coordinates) def add_watermark(image : Image, name_of_other_creator): + ''' + Adds the rdrama watermark to the bottom of the image, crediting the other creator of the image. + ''' global watermark_captions WATERMARK_HEIGHT = int(0.05 * image.height) image_size_x, image_size_y = image.size @@ -317,6 +356,10 @@ def add_watermark(image : Image, name_of_other_creator): return base def center_and_paste(base : Image, to_paste : Image, coordinates: 'tuple[int, int]', box_size : 'tuple[int, int]') -> 'AnimatedImage': + ''' + Centers to_paste in a box whose upper-left corder is "coordinates", and with size box_size. + ''' + to_paste = to_paste.fit(box_size) image_size_x, image_size_y = to_paste.size @@ -335,6 +378,9 @@ def center_and_paste(base : Image, to_paste : Image, coordinates: 'tuple[int, in return base.paste(to_paste, (x, y)) def add_border_to_image(image : Image, thickness : int = 5): + ''' + Adds a black border to an image. + ''' inner_image_x_size, inner_image_y_size = image.size outer_image_x_size, outer_image_y_size = inner_image_x_size + 2*thickness, inner_image_y_size + 2*thickness outside_image = AnimatedImage.new((outer_image_x_size,outer_image_y_size), color=(0,0,0)) @@ -343,6 +389,9 @@ def add_border_to_image(image : Image, thickness : int = 5): return outside_image def get_emoji_from_rdrama(emoji_name) -> 'AnimatedImage': + ''' + Gets an emoji from rdrama. If there is a "!" in the emoji name, it will be flipped. + ''' cleaned_emoji_name : str = emoji_name should_flip = False if '!' in emoji_name: @@ -360,6 +409,9 @@ def get_emoji_from_rdrama(emoji_name) -> 'AnimatedImage': return image def get_image_file_from_url(url) -> 'AnimatedImage': + ''' + Gets the image at the given address. + ''' try: r = requests.get(url) image_file = io.BytesIO(r.content) @@ -367,7 +419,11 @@ def get_image_file_from_url(url) -> 'AnimatedImage': return AnimatedImage.from_image(im) except: print(f"ERROR GETTING FILE FROM {url}") + def parse_caption_file(filename): + ''' + Opens a file of strings and returns them. + ''' if not exists(filename): return [] to_return = [] @@ -376,9 +432,12 @@ def parse_caption_file(filename): to_return.append(id.strip()) return to_return -watermark_captions = parse_caption_file(get_real_filename(CAPTION_FILENAME)) + class AnimatedImage(): + ''' + Basically a list of images, meant to be animated. + ''' def __init__(self, frames : 'list[Image.Image]'): if len(frames) > 100: self.frames = frames[0:100] @@ -387,45 +446,70 @@ class AnimatedImage(): @property def size(self) -> 'Tuple[int, int]': - return self.frames[0].size + ''' + Size of the image. + ''' + return self.frames[0].size @property def height(self) -> int: + ''' + Height of the image. + ''' return self.frames[0].size[1] @property def width(self) -> int: + ''' + Width of the image. + ''' return self.frames[0].size[0] def flip(self) -> 'AnimatedImage': - new_frames = [] - for frame in self.frames: - #frame.show() - new_frame = ImageOps.mirror(frame) - #new_frame.show() - new_frames.append(new_frame) - return AnimatedImage(new_frames) + ''' + Flips image horizontally. + ''' + + return self.perform_transform_for_all_images(lambda image : ImageOps.mirror(image)) - def save(self, filename) -> None: + def save(self, filename : str) -> None: + ''' + Saves the image to the filename + ''' self.frames[0].save(f"{filename}", save_all = True, append_images = self.frames[1:], duration = 100, loop=0) def get_binary(self) -> bytes: + ''' + Converts the image to binary. + ''' output = io.BytesIO() self.frames[0].save(output, format="webp", save_all = True, append_images = self.frames[1:], duration = 100, loop=0) return output.getvalue() + def get_binary_gif(self) -> bytes: + output = io.BytesIO() + self.frames[0].save(output, format="gif", save_all = True, append_images = self.frames[1:], duration = 100, loop=0) + return output.getvalue() + def from_image(image: Image.Image) -> 'AnimatedImage': + ''' + Converts a run of the mill PIL image to an AnimatedImage. + ''' frames = [] for image_frame in ImageSequence.Iterator(image): frames.append(image_frame.copy()) return AnimatedImage(frames) def paste(self, other : 'AnimatedImage', position : 'Tuple[int, int]') -> 'AnimatedImage': + ''' + Pastes one image onto another. Tries to do it without breaking the loop. + ''' num_self_frames = len(self.frames) num_other_frames = len(other.frames) new_frames = [] - for i in range(get_ideal_number_of_frames(num_self_frames, num_other_frames)): + frames_to_create = min(get_ideal_number_of_frames(num_self_frames, num_other_frames), 100) + for i in range(frames_to_create): self_frame = self.frames[i%num_self_frames] other_frame = other.frames[i%num_other_frames] @@ -439,6 +523,9 @@ class AnimatedImage(): return AnimatedImage(new_frames) def new(size : 'Tuple[int, int]', color=(255,255,255)) -> 'AnimatedImage': + ''' + Creates a blank AnimatedImage. + ''' base = Image.new(mode="RGB", size=size, color=color) return AnimatedImage.from_image(base) @@ -446,10 +533,7 @@ class AnimatedImage(): ''' Shrinks the image while preserving it's aspect ratio, such that the new image has the same ratio but is contained in the given box. ''' - new_frames = [] - for frame in self.frames: - new_frames.append(ImageOps.contain(frame, region)) - return AnimatedImage(new_frames) + return self.perform_transform_for_all_images(lambda image : ImageOps.contain(image, region)) def stretch(self, region : 'Tuple[int, int]') -> 'AnimatedImage': ''' @@ -507,6 +591,9 @@ def get_leftover_primes(a_, b_): leftover_primes.append(i) return leftover_primes +watermark_captions = parse_caption_file(get_real_filename(CAPTION_FILENAME)) + + # shooting = get_emoji_from_rdrama("!marseyshooting") # shooting2 = get_emoji_from_rdrama("marseydeterminedgun") @@ -525,7 +612,7 @@ def get_leftover_primes(a_, b_): # TwoCharacterWebcomicPanel("marseytombstone", "", "!marseycry", "He had so much to live for"), # OneCharacterWebcomicPanel("marseylaugh", "Just Kidding!", False) # ]).save("webcomic.webp") -create_modern_meme_from_url("https://media.giphy.com/media/gYkga3bZav66I/giphy.webp", "me when i see a black person (i am extremely racist)").save("racism.webp") +#create_modern_meme_from_url("https://media.giphy.com/media/gYkga3bZav66I/giphy.webp", "me when i see a black person (i am extremely racist)").save("racism.webp") #create_classic_meme_from_url("https://rdrama.net/images/16619117705921352.webp", "I left out 'hiking' intentionally since it's outdoor hobby 101. But even if he's trying to attract a homebody some kind of interest would be good to have. Like home brewing, art, instrument, or god forbid tabletop gaming.", "This just screams most boring, generic, codependent 'man' ever. I need a wife and I'll be happy to do whatever you want to do 24/7 because I have no personality, friends, or interests").save("classic.webp") #create_modern_meme_from_url("https://rdrama.net/images/16617243111639555.webp", "It really does, especially with that weird lighting. ANd what's up with the subset of troons with Himmler eyes?").save("modern_image.webp") #create_classic_meme_from_emoji("marseycock", "I WANT TO PUT MY DICK", "INSIDE OF MARSEY").save("classic_with_emoji.webp") diff --git a/upload_best_meme_to_twitter.py b/upload_best_meme_to_twitter.py new file mode 100644 index 0000000..d639ff7 --- /dev/null +++ b/upload_best_meme_to_twitter.py @@ -0,0 +1,46 @@ +from io import BytesIO +import tweepy +from automeme import TextLine, get_rdrama, strip_markdown +import utils +import json +from meme_generator import get_image_file_from_url + +def load_key_from_file(filename : str) -> str: + with open(utils.get_real_filename(filename), "r") as f: + return f.read() + +consumer_key = load_key_from_file("twitter_api_key") +consumer_secret = load_key_from_file("twitter_api_secret") +access_token = load_key_from_file("twitter_access_token") +access_token_secret = load_key_from_file("twitter_access_token_secret") + + + +auth = tweepy.OAuth1UserHandler( + consumer_key, + consumer_secret, + access_token, + access_token_secret +) +api = tweepy.API(auth) + +rdrama = get_rdrama() +best = rdrama.get_comments(user = "automeme", sort = "top", t="day")['data'][0] +print(json.dumps(best, indent=4)) + +comment_text = best['body'] +cleaned_comment_text = strip_markdown(comment_text) +comment_lines = cleaned_comment_text.split("\n") +comment_lines = [comment_line for comment_line in comment_lines if comment_line != ""] +element_lines = [TextLine(line) for line in comment_lines] + +for i in element_lines: + print(i) + +image = element_lines[-1].images[0] +print(image.url) +animated_image = get_image_file_from_url(image.url) +file = BytesIO(initial_bytes=animated_image.get_binary_gif()) +media = api.media_upload(filename = "foo.gif", file=file, chunked=True, wait_for_async_finalize = True) +media_id = media.media_id_string +api.update_status("#meme", media_ids=[media_id]) \ No newline at end of file