├── .gitignore ├── LICENSE ├── README.md ├── assets ├── Copse-Regular.ttf ├── bgm.mp3 ├── censored.png ├── clickbait.png ├── comment.png ├── forkme.png ├── gold_32.png ├── mic.jpg ├── platinum_32.png ├── static.mp4 └── upvote.png ├── bin └── youtubeuploader_linux_amd64 ├── data ├── audio │ └── .gitkeep ├── thumbnails │ └── .gitkeep └── video │ └── .gitkeep ├── doc └── test.png ├── requirements.txt ├── src ├── main.py └── tasks │ ├── cleanup │ └── task.py │ ├── generate_thumbnail │ └── task.py │ ├── generate_video │ └── task.py │ ├── scrape_reddit │ ├── post.py │ └── task.py │ ├── text_to_speech │ └── task.py │ └── upload_video │ └── task.py └── test ├── __init__.py ├── test_reddit.py ├── test_thumbnail.py └── test_tts.py /.gitignore: -------------------------------------------------------------------------------- 1 | request.token 2 | client_secrets.json 3 | data/**/*.png 4 | data/**/*.mp3 5 | data/**/*.mp4 6 | # Created by https://www.gitignore.io/api/python 7 | # Edit at https://www.gitignore.io/?templates=python 8 | 9 | ### Python ### 10 | # Byte-compiled / optimized / DLL files 11 | __pycache__/ 12 | *.py[cod] 13 | *$py.class 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | pip-wheel-metadata/ 33 | share/python-wheels/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | MANIFEST 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .nox/ 53 | .coverage 54 | .coverage.* 55 | .cache 56 | nosetests.xml 57 | coverage.xml 58 | *.cover 59 | .hypothesis/ 60 | .pytest_cache/ 61 | 62 | # Translations 63 | *.mo 64 | *.pot 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # pipenv 79 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 80 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 81 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 82 | # install all needed dependencies. 83 | #Pipfile.lock 84 | 85 | # celery beat schedule file 86 | celerybeat-schedule 87 | 88 | # SageMath parsed files 89 | *.sage.py 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # Mr Developer 99 | .mr.developer.cfg 100 | .project 101 | .pydevproject 102 | 103 | # mkdocs documentation 104 | /site 105 | 106 | # mypy 107 | .mypy_cache/ 108 | .dmypy.json 109 | dmypy.json 110 | 111 | # Pyre type checker 112 | .pyre/ 113 | 114 | # End of https://www.gitignore.io/api/python 115 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Adam Jimenez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Auddit 2 | 3 | Tired of those Reddit text-to-speech videos on Youtube? Now you can make your own, automatically! 4 | 5 | Official Auddit Youtube Channel: https://www.youtube.com/channel/UCMi63vc1Timv8dfrmcrrdFQ 6 | 7 | ![thumbnail](./doc/test.png) 8 | 9 | ## Dependencies 10 | 11 | - Python 3 12 | - Everything in the requirements.txt file 13 | - https://github.com/porjo/youtubeuploader 14 | 15 | ## Setup 16 | 17 | `sudo pip install -r requirements.txt` 18 | 19 | Then add the following environnement variables: 20 | 21 | - REDDIT_CLIENT_ID 22 | - REDDIT_CLIENT_SECRET 23 | 24 | Read about how to [create a Reddit application](https://ssl.reddit.com/prefs/apps/) to get those. 25 | 26 | You also need to register an application on the Google OAuthv2 API. [Here's](https://developers.google.com/youtube/v3/guides/uploading_a_video) a guide. You need to put the resulting `client_secrets.json` in the root of the project. 27 | 28 | ## Running 29 | 30 | `python src/main.py` 31 | 32 | ## Testing 33 | 34 | All tests: 35 | 36 | `python -m unittest` 37 | 38 | Single test: 39 | 40 | `python -m unittest test/test_reddit.py` 41 | 42 | ## How it works 43 | 44 | Using the [Python Reddit API Wrapper](https://github.com/praw-dev/praw), we can query for hot posts from any subreddit. 45 | 46 | Then we pipe the text to the text-to-speech task, that generates an audio file using either ttsmp3.com or the [Google TTS Python Wrapper](https://gtts.readthedocs.io/en/latest/index.html). We prefer ttsmp3.com for the quality of the voices and use gTTS as a fallback if we get rate-limited. 47 | 48 | Then, we send the text and the audio to the video generation tasks, which uses [PyMovie](https://zulko.github.io/moviepy/) to make a video with background music, the text-to-speech clips and the text. 49 | 50 | Then we generate a thumbnail with the goal of clickbaiting the viewers with [Pillow](https://pillow.readthedocs.io/en/stable/). 51 | 52 | All that is left is to [upload to Youtube using the Google API](https://github.com/porjo/youtubeuploader). 53 | 54 | ## Why 55 | 56 | because i can 57 | -------------------------------------------------------------------------------- /assets/Copse-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adam-Jimenez/auddit/619052f358439c4b6d578cbc5fbdae2961bb9e16/assets/Copse-Regular.ttf -------------------------------------------------------------------------------- /assets/bgm.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adam-Jimenez/auddit/619052f358439c4b6d578cbc5fbdae2961bb9e16/assets/bgm.mp3 -------------------------------------------------------------------------------- /assets/censored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adam-Jimenez/auddit/619052f358439c4b6d578cbc5fbdae2961bb9e16/assets/censored.png -------------------------------------------------------------------------------- /assets/clickbait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adam-Jimenez/auddit/619052f358439c4b6d578cbc5fbdae2961bb9e16/assets/clickbait.png -------------------------------------------------------------------------------- /assets/comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adam-Jimenez/auddit/619052f358439c4b6d578cbc5fbdae2961bb9e16/assets/comment.png -------------------------------------------------------------------------------- /assets/forkme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adam-Jimenez/auddit/619052f358439c4b6d578cbc5fbdae2961bb9e16/assets/forkme.png -------------------------------------------------------------------------------- /assets/gold_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adam-Jimenez/auddit/619052f358439c4b6d578cbc5fbdae2961bb9e16/assets/gold_32.png -------------------------------------------------------------------------------- /assets/mic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adam-Jimenez/auddit/619052f358439c4b6d578cbc5fbdae2961bb9e16/assets/mic.jpg -------------------------------------------------------------------------------- /assets/platinum_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adam-Jimenez/auddit/619052f358439c4b6d578cbc5fbdae2961bb9e16/assets/platinum_32.png -------------------------------------------------------------------------------- /assets/static.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adam-Jimenez/auddit/619052f358439c4b6d578cbc5fbdae2961bb9e16/assets/static.mp4 -------------------------------------------------------------------------------- /assets/upvote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adam-Jimenez/auddit/619052f358439c4b6d578cbc5fbdae2961bb9e16/assets/upvote.png -------------------------------------------------------------------------------- /bin/youtubeuploader_linux_amd64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adam-Jimenez/auddit/619052f358439c4b6d578cbc5fbdae2961bb9e16/bin/youtubeuploader_linux_amd64 -------------------------------------------------------------------------------- /data/audio/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adam-Jimenez/auddit/619052f358439c4b6d578cbc5fbdae2961bb9e16/data/audio/.gitkeep -------------------------------------------------------------------------------- /data/thumbnails/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adam-Jimenez/auddit/619052f358439c4b6d578cbc5fbdae2961bb9e16/data/thumbnails/.gitkeep -------------------------------------------------------------------------------- /data/video/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adam-Jimenez/auddit/619052f358439c4b6d578cbc5fbdae2961bb9e16/data/video/.gitkeep -------------------------------------------------------------------------------- /doc/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adam-Jimenez/auddit/619052f358439c4b6d578cbc5fbdae2961bb9e16/doc/test.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | praw 2 | gtts 3 | moviepy 4 | scipy 5 | requests 6 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | from tasks.scrape_reddit.task import get_hottest_post 2 | from tasks.text_to_speech.task import tts 3 | from tasks.generate_video.task import generate_video 4 | from tasks.upload_video.task import upload_video 5 | from tasks.generate_thumbnail.task import generate_thumbnail 6 | from tasks.cleanup.task import cleanup 7 | 8 | class Pipeline: 9 | def __init__(self): 10 | self.tasks = [ 11 | get_hottest_post, 12 | tts, 13 | generate_video, 14 | generate_thumbnail, 15 | upload_video, 16 | cleanup 17 | ] 18 | self.context = dict() 19 | 20 | def execute(self, **kwargs): 21 | self.context = kwargs 22 | for task in self.tasks: 23 | print(f"Current Task: {task.__name__}") 24 | task(self.context) 25 | 26 | if __name__ == "__main__": 27 | pipeline = Pipeline() 28 | pipeline.execute(subreddit='askreddit', nsfw=False, comment_limit=20) 29 | -------------------------------------------------------------------------------- /src/tasks/cleanup/task.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | 4 | paths = [ 5 | "./data/thumbnails/*.png", 6 | "./data/video/*.mp4", 7 | "./data/audio/*.mp3", 8 | ] 9 | 10 | def cleanup(context): 11 | for path in paths: 12 | files = glob.glob(path) 13 | for f in files: 14 | os.remove(f) 15 | -------------------------------------------------------------------------------- /src/tasks/generate_thumbnail/task.py: -------------------------------------------------------------------------------- 1 | from PIL import Image, ImageDraw, ImageFont 2 | from textwrap import fill 3 | 4 | THUMBNAIL_DIR = "data/thumbnails/" 5 | THUMBNAIL_DIMENSION = (640, 360) 6 | CLICKBAIT_GIRL = Image.open('./assets/clickbait.png') 7 | CLICKBAIT_GIRL.thumbnail((THUMBNAIL_DIMENSION[0]/2, THUMBNAIL_DIMENSION[1]/2), Image.ANTIALIAS) # aspect ratio resize to fit thumbnail 8 | GOLD = Image.open("./assets/gold_32.png") 9 | PLATINUM = Image.open("./assets/platinum_32.png") 10 | FORK_ME = Image.open("./assets/forkme.png") 11 | CENSORED = Image.open("./assets/censored.png") 12 | UPVOTE = Image.open("./assets/upvote.png").convert("RGBA") 13 | COMMENT = Image.open("./assets/comment.png").convert("RGBA") 14 | CENSORED.thumbnail((THUMBNAIL_DIMENSION[0]/2, THUMBNAIL_DIMENSION[1]/2), Image.ANTIALIAS) # aspect ratio resize to fit thumbnail 15 | 16 | def generate_thumbnail(context): 17 | subreddit = context["subreddit"].capitalize() 18 | post = context["post"] 19 | score = str(post.score) 20 | num_comments = str(post.num_comments) 21 | main_text = post.title 22 | main_text = fill(main_text, width=25) 23 | 24 | thumbnail = Image.new('RGB', THUMBNAIL_DIMENSION, color=(16,16,16)) 25 | draw = ImageDraw.Draw(thumbnail) 26 | font = ImageFont.truetype('./assets/Copse-Regular.ttf', 40) 27 | font2 = ImageFont.truetype('./assets/Copse-Regular.ttf', 45) 28 | font3 = ImageFont.truetype('./assets/Copse-Regular.ttf', 30) 29 | 30 | # clickbait girl 31 | aw,ah = CLICKBAIT_GIRL.size 32 | girl_offset = (THUMBNAIL_DIMENSION[0]-aw,THUMBNAIL_DIMENSION[1]-ah) 33 | thumbnail.paste(CLICKBAIT_GIRL, girl_offset, CLICKBAIT_GIRL) 34 | 35 | # subreddit 36 | title_width, title_height = font.getsize(subreddit) 37 | title_offset = (20,20) 38 | draw.text(title_offset, subreddit, font=font, fill=(255,255,255)) 39 | 40 | # gilds 41 | gold_offset = (title_offset[0] + title_width + 20, title_offset[1] + 10) 42 | platinum_offset = (title_offset[0] + title_width + GOLD.size[0] + 26, title_offset[1] + 10) 43 | thumbnail.paste(GOLD, gold_offset, GOLD) 44 | thumbnail.paste(PLATINUM, platinum_offset, PLATINUM) 45 | 46 | # main text 47 | main_text_offset = (20, 70) 48 | draw.text(main_text_offset, main_text, font=font2, fill=(255,255,255)) 49 | 50 | # fork me 51 | fw,fh = FORK_ME.size 52 | fork_offset = (THUMBNAIL_DIMENSION[0]-fw, 0) 53 | thumbnail.paste(FORK_ME, fork_offset, FORK_ME) 54 | 55 | # upvote 56 | uw, uh = UPVOTE.size 57 | upvote_offset = (20, THUMBNAIL_DIMENSION[1]-uh-20) 58 | thumbnail.paste(UPVOTE, upvote_offset, UPVOTE) 59 | 60 | upvote_count_offset = (upvote_offset[0]+uw+10, THUMBNAIL_DIMENSION[1]-uh-20) 61 | draw.text(upvote_count_offset, score, font=font3, fill=(128, 128, 128)) 62 | score_width, score_height = font3.getsize(score) 63 | # comments 64 | cw, ch = COMMENT.size 65 | comment_offset = (upvote_count_offset[0]+score_width+20, THUMBNAIL_DIMENSION[1]-ch-20) 66 | thumbnail.paste(COMMENT, comment_offset, COMMENT) 67 | 68 | comment_count_offset = (comment_offset[0]+cw+10, THUMBNAIL_DIMENSION[1]-ch-20) 69 | draw.text(comment_count_offset, num_comments, font=font3, fill=(128, 128, 128)) 70 | 71 | video_id = context["video_id"] 72 | thumbnail_path = f"{THUMBNAIL_DIR}{video_id}.png" 73 | thumbnail.save(thumbnail_path) 74 | context["thumbnail_path"] = thumbnail_path 75 | -------------------------------------------------------------------------------- /src/tasks/generate_video/task.py: -------------------------------------------------------------------------------- 1 | import textwrap 2 | import uuid 3 | from moviepy.editor import * 4 | 5 | TITLE_FONT_SIZE = 30 6 | FONT_SIZE = 30 7 | TITLE_FONT_COLOR = 'white' 8 | BGM_PATH = 'assets/bgm.mp3' 9 | STATIC_PATH = 'assets/static.mp4' 10 | SIZE = (1280, 720) 11 | BG_COLOR = (16,16,16) 12 | VIDEO_PATH = "data/video/" 13 | FONT = 'Amiri-regular' 14 | 15 | def generate_title(text, audio_path): 16 | color_clip = ColorClip(SIZE, BG_COLOR) 17 | audio_clip = AudioFileClip(audio_path) 18 | font_size = TITLE_FONT_SIZE 19 | wrapped_text = textwrap.fill(text, width=90) 20 | txt_clip = TextClip(wrapped_text,fontsize=font_size, font=FONT, color=TITLE_FONT_COLOR, align="west") 21 | txt_clip = txt_clip.set_pos("center") 22 | clip = CompositeVideoClip([color_clip, txt_clip]) 23 | clip.audio = audio_clip 24 | clip.duration = audio_clip.duration 25 | static_clip = VideoFileClip(STATIC_PATH) 26 | clip = concatenate_videoclips([clip, static_clip]) 27 | return clip 28 | 29 | def generate_clip(post, comment): 30 | text = comment.body 31 | audio_path = comment.body_audio 32 | 33 | 34 | color_clip = ColorClip(SIZE, BG_COLOR) 35 | audio_clip = AudioFileClip(audio_path) 36 | font_size = TITLE_FONT_SIZE 37 | author_font_size = 20 38 | wrapped_text = textwrap.fill(text, width=90) 39 | 40 | 41 | txt_clip = TextClip(wrapped_text,fontsize=font_size, font=FONT, color=TITLE_FONT_COLOR, align="west", interline=2) 42 | txt_clip = txt_clip.set_pos("center") 43 | 44 | author_clip = TextClip(f"/u/{comment.author}", fontsize=author_font_size, font=FONT, color="lightblue") 45 | author_pos = (SIZE[0]/2 - txt_clip.size[0]/2, SIZE[1]/2 - txt_clip.size[1]/2 - author_font_size - 10) 46 | author_clip = author_clip.set_pos(author_pos) 47 | 48 | score_clip = TextClip(f"{comment.score} points", fontsize=author_font_size, font=FONT, color="grey") 49 | score_pos = (author_pos[0] + author_clip.size[0] + 20, author_pos[1]) 50 | score_clip = score_clip.set_pos(score_pos) 51 | 52 | clip = CompositeVideoClip([color_clip, txt_clip, author_clip, score_clip]) 53 | clip.audio = audio_clip 54 | clip.duration = audio_clip.duration 55 | static_clip = VideoFileClip(STATIC_PATH) 56 | clip = concatenate_videoclips([clip, static_clip]) 57 | return clip 58 | 59 | def generate_video(context): 60 | post = context["post"] 61 | clips = [] 62 | clips.append(generate_title(post.title, post.title_audio)) 63 | for comment in post.comments: 64 | comment_clip = generate_clip(post, comment) 65 | # overlay reply 66 | if comment.reply: 67 | # TODO this 68 | pass 69 | clips.append(comment_clip) 70 | video = concatenate_videoclips(clips) 71 | background_audio_clip = AudioFileClip(BGM_PATH) 72 | background_audio_clip = afx.audio_loop(background_audio_clip, duration=video.duration) 73 | background_audio_clip = background_audio_clip.fx(afx.volumex, 0.15) 74 | video.audio = CompositeAudioClip([video.audio, background_audio_clip]) 75 | video_id = uuid.uuid4() 76 | path = f"{VIDEO_PATH}{video_id}.mp4" 77 | context["video_path"] = path 78 | context["video_id"] = video_id 79 | video.write_videofile(path, fps=24, codec='libx264', threads=4) 80 | 81 | -------------------------------------------------------------------------------- /src/tasks/scrape_reddit/post.py: -------------------------------------------------------------------------------- 1 | class Post: 2 | def __init__(self, title, comments): 3 | self.title: str = title 4 | self.comments: [Comment] = comments 5 | 6 | def __str__(self): 7 | s = f"{self.title}\n" 8 | for c in self.comments: 9 | s += f"\n{c}" 10 | return s 11 | 12 | class Comment: 13 | def __init__(self, body, reply): 14 | self.body: str = body 15 | self.reply: str = reply 16 | 17 | def __str__(self): 18 | return f"{self.body}\n\t{self.reply}" 19 | -------------------------------------------------------------------------------- /src/tasks/scrape_reddit/task.py: -------------------------------------------------------------------------------- 1 | import praw 2 | import os 3 | from praw.models import MoreComments 4 | from .post import Post, Comment 5 | 6 | client_id = os.environ["REDDIT_CLIENT_ID"] 7 | client_secret = os.environ["REDDIT_CLIENT_SECRET"] 8 | 9 | reddit = praw.Reddit(client_id=client_id, client_secret=client_secret, user_agent='auddit-dev v1.0.0 by /u/adamj0') 10 | 11 | 12 | def get_hottest_post(context): 13 | subreddit_name=context["subreddit"] 14 | comment_limit=context["comment_limit"] 15 | nsfw=context["nsfw"] 16 | subreddit = reddit.subreddit(subreddit_name) 17 | hot_posts = subreddit.hot() 18 | for post in hot_posts: 19 | if not post.stickied and post.over_18 == nsfw: 20 | title = post.title 21 | if len(title) >= 100: 22 | continue # respect youtube limit of 100 23 | comments = [] 24 | for comment in post.comments: 25 | if isinstance(comment, MoreComments): 26 | continue 27 | if comment.stickied: 28 | continue 29 | comment_body = comment.body 30 | if comment_body == "[removed]": 31 | continue 32 | comment_reply = "" 33 | comment.replies.replace_more(limit=1) 34 | if len(comment.replies) > 0: 35 | reply = comment.replies[0] 36 | if isinstance(reply, MoreComments): 37 | continue 38 | comment_reply = reply.body 39 | comment_output = Comment(comment_body, comment_reply) 40 | comment_output.author = comment.author.name 41 | comment_output.score = comment.score 42 | comments.append(comment_output) 43 | if len(comments) >= comment_limit: 44 | break 45 | 46 | post_data = Post(title, comments) 47 | post_data.score = post.score 48 | post_data.num_comments = post.num_comments 49 | context["post"] = post_data 50 | return 51 | 52 | if __name__ == '__main__': 53 | get_hottest_post() 54 | -------------------------------------------------------------------------------- /src/tasks/text_to_speech/task.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import time 3 | import requests 4 | import random 5 | from gtts import gTTS 6 | 7 | VOICES = ["Matthew", "Joey", "Kendra"] 8 | AUDIO_PATH = "data/audio/" 9 | TTSMP3_URL = "https://ttsmp3.com/makemp3_new.php" 10 | 11 | 12 | def save_tts(text): 13 | try: 14 | form_data = { 15 | "msg": text, 16 | "lang": random.choice(VOICES), 17 | "source": "ttsmp3" 18 | } 19 | json = requests.post(TTSMP3_URL, form_data).json() 20 | url = json["URL"] 21 | filename = json["MP3"] 22 | mp3_file = requests.get(url) 23 | path = f"{AUDIO_PATH}{filename}" 24 | with open(path, "wb") as out_file: 25 | out_file.write(mp3_file.content) 26 | return path 27 | except: 28 | print("TTS Rate limit reached - Fallback on Google text-to-speech") 29 | return save_gtts(text) 30 | 31 | def save_gtts(text): 32 | tts = gTTS(text=text, lang='en') 33 | path = f"{AUDIO_PATH}{uuid.uuid4()}.mp3" 34 | tts.save(path) 35 | return path 36 | 37 | def tts(context): 38 | post = context["post"] 39 | post.title_audio = save_tts(post.title) 40 | for comment in post.comments: 41 | comment.body_audio = save_tts(comment.body) 42 | if comment.reply: 43 | comment.reply_audio = save_tts(comment.reply) 44 | return 45 | 46 | if __name__ == "__main__": 47 | save_tts("i am a potato") 48 | -------------------------------------------------------------------------------- /src/tasks/upload_video/task.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | def upload_video(context): 4 | post = context["post"] 5 | subreddit = context["subreddit"] 6 | video_path = context["video_path"] 7 | thumbnail_path = context["thumbnail_path"] 8 | title = post.title 9 | description = title + f" (/r/{subreddit})" 10 | 11 | args = ("./bin/youtubeuploader_linux_amd64", "-filename", video_path, "-title", title, "-description", description, 12 | "-thumbnail", thumbnail_path, "-privacy", "public") 13 | popen = subprocess.Popen(args, stdout=subprocess.PIPE) 14 | popen.wait() 15 | output = popen.stdout.read() 16 | print(output) 17 | 18 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adam-Jimenez/auddit/619052f358439c4b6d578cbc5fbdae2961bb9e16/test/__init__.py -------------------------------------------------------------------------------- /test/test_reddit.py: -------------------------------------------------------------------------------- 1 | # Python code to demonstrate working of unittest 2 | import unittest 3 | from src.tasks.scrape_reddit.task import get_hottest_post 4 | 5 | class TestReddit(unittest.TestCase): 6 | 7 | def setUp(self): 8 | pass 9 | 10 | def test_normal(self): 11 | ctx = { 12 | "subreddit": "askreddit", 13 | "nsfw": True, 14 | "comment_limit": 3 15 | } 16 | get_hottest_post(ctx) 17 | print(ctx["post"].title) 18 | 19 | if __name__ == '__main__': 20 | unittest.main() 21 | -------------------------------------------------------------------------------- /test/test_thumbnail.py: -------------------------------------------------------------------------------- 1 | # Python code to demonstrate working of unittest 2 | import unittest 3 | from src.tasks.generate_thumbnail.task import generate_thumbnail 4 | 5 | class Post: 6 | pass 7 | 8 | class TestThumbnail(unittest.TestCase): 9 | 10 | def setUp(self): 11 | pass 12 | 13 | def test_normal(self): 14 | post = Post() 15 | post.title = "a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a " 16 | post.score = 42069 17 | post.num_comments = 42069 18 | ctx = { 19 | "subreddit": "askreddit", 20 | "post": post, 21 | "video_id": "test" 22 | } 23 | generate_thumbnail(ctx) 24 | 25 | if __name__ == '__main__': 26 | unittest.main() 27 | -------------------------------------------------------------------------------- /test/test_tts.py: -------------------------------------------------------------------------------- 1 | # Python code to demonstrate working of unittest 2 | import unittest 3 | from src.tasks.text_to_speech.task import save_tts, save_gtts 4 | 5 | class TestTTS(unittest.TestCase): 6 | 7 | def setUp(self): 8 | pass 9 | 10 | def test_normal(self): 11 | save_tts("I am a potato") 12 | save_gtts("I am a potato") 13 | 14 | if __name__ == '__main__': 15 | unittest.main() 16 | --------------------------------------------------------------------------------