commit
ce14eff286
@ -0,0 +1,4 @@
|
|||||||
|
REDDIT_CLIENT_ID=""
|
||||||
|
REDDIT_CLIENT_SECRET=""
|
||||||
|
REDDIT_USERNAME=""
|
||||||
|
REDDIT_PASSWORD=""
|
@ -0,0 +1,2 @@
|
|||||||
|
assets/
|
||||||
|
.env
|
@ -0,0 +1,41 @@
|
|||||||
|
# Reddit Video Maker Bot 🎥
|
||||||
|
|
||||||
|
![](./examples/final_video.webm)
|
||||||
|
All done WITHOUT video editing or asset compiling. Just pure ✨programming magic✨.
|
||||||
|
|
||||||
|
## Motivation 🤔
|
||||||
|
|
||||||
|
These videos on TikTok, YouTube and Instagram get MILLIONS of views across all platforms and require very little effort. The only original thing being done is the editing and gathering of all materials...
|
||||||
|
|
||||||
|
... but what if we can automate that process? 🤔
|
||||||
|
|
||||||
|
## Disclaimers 🚨
|
||||||
|
|
||||||
|
- This is purely for fun purposes.
|
||||||
|
- **At the moment**, this repository won't attempt to upload this content through this bot. It will give you a file that you will then have to upload manually. This is for the sake of avoiding any sort of community guideline issues.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.6+
|
||||||
|
- Playwright (this should install automatically in installation)
|
||||||
|
|
||||||
|
## Installation 👩💻
|
||||||
|
|
||||||
|
1. Clone this repository
|
||||||
|
2. Rename `.env.template` to `.env` and replace all values with the appropriate fields. To get Reddit keys (**required**), visit [the Reddit Apps page.](https://www.reddit.com/prefs/apps) TL;DR set up an app that is a "script". Copy your keys into the `.env` files.
|
||||||
|
3. Run `pip3 install -r requirements.txt`
|
||||||
|
4. Run `python3 main.py`
|
||||||
|
5. ...
|
||||||
|
6. Enjoy 😎
|
||||||
|
|
||||||
|
## Contributing & Ways to improve 📈
|
||||||
|
|
||||||
|
In its current state, this bot does exactly what it needs to do. However, lots of improvements can be made.
|
||||||
|
|
||||||
|
I have tried to simplify the code so anyone can read it and start contibuting at any skill level. Don't be shy :) contribute!
|
||||||
|
|
||||||
|
- [ ] Allowing users to choose a reddit thread instead of being randomized.
|
||||||
|
- [ ] Allowing users to choose a background that is picked instead of the Minecraft one.
|
||||||
|
- [ ] Allowing users to choose between any subreddit.
|
||||||
|
- [ ] Allowing users to change voice.
|
||||||
|
- [ ] Creating better documentation and adding a command line interface.
|
Binary file not shown.
Binary file not shown.
@ -0,0 +1,22 @@
|
|||||||
|
from utils.console import print_markdown
|
||||||
|
import time
|
||||||
|
from reddit.askreddit import get_askreddit_threads
|
||||||
|
from video_creation.background import download_background, chop_background_video
|
||||||
|
from video_creation.voices import save_text_to_mp3
|
||||||
|
from video_creation.screenshot_downloader import download_screenshots_of_reddit_posts
|
||||||
|
from video_creation.final_video import make_final_video
|
||||||
|
|
||||||
|
print_markdown(
|
||||||
|
"### Thanks for using this tool! 😊 [Feel free to contribute to this project on GitHub!](https://lewismenelaws.com). If you have any questions, feel free to reach out to me on Twitter or submit a GitHub issue."
|
||||||
|
)
|
||||||
|
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
|
||||||
|
reddit_object = get_askreddit_threads()
|
||||||
|
|
||||||
|
length, number_of_comments = save_text_to_mp3(reddit_object)
|
||||||
|
download_screenshots_of_reddit_posts(reddit_object, number_of_comments)
|
||||||
|
download_background()
|
||||||
|
chop_background_video(length)
|
||||||
|
final_video = make_final_video(number_of_comments)
|
@ -0,0 +1,46 @@
|
|||||||
|
from utils.console import print_markdown, print_step, print_substep
|
||||||
|
import praw
|
||||||
|
import random
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def get_askreddit_threads():
|
||||||
|
"""
|
||||||
|
Returns a list of threads from the AskReddit subreddit.
|
||||||
|
"""
|
||||||
|
|
||||||
|
print_step("Getting AskReddit threads...")
|
||||||
|
|
||||||
|
content = {}
|
||||||
|
load_dotenv()
|
||||||
|
reddit = praw.Reddit(
|
||||||
|
client_id=os.getenv("REDDIT_CLIENT_ID"),
|
||||||
|
client_secret=os.getenv("REDDIT_CLIENT_SECRET"),
|
||||||
|
user_agent="Accessing AskReddit threads",
|
||||||
|
username=os.getenv("REDDIT_USERNAME"),
|
||||||
|
password=os.getenv("REDDIT_PASSWORD"),
|
||||||
|
)
|
||||||
|
askreddit = reddit.subreddit("askreddit")
|
||||||
|
threads = askreddit.hot(limit=25)
|
||||||
|
submission = list(threads)[random.randrange(0, 25)]
|
||||||
|
print_substep(f"Video will be: {submission.title} :thumbsup:")
|
||||||
|
try:
|
||||||
|
|
||||||
|
content["thread_url"] = submission.url
|
||||||
|
content["thread_title"] = submission.title
|
||||||
|
content["comments"] = []
|
||||||
|
|
||||||
|
for top_level_comment in submission.comments:
|
||||||
|
content["comments"].append(
|
||||||
|
{
|
||||||
|
"comment_body": top_level_comment.body,
|
||||||
|
"comment_url": top_level_comment.permalink,
|
||||||
|
"comment_id": top_level_comment.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
except AttributeError as e:
|
||||||
|
pass
|
||||||
|
print_substep("Received AskReddit threads Successfully.", style="bold green")
|
||||||
|
return content
|
@ -0,0 +1,42 @@
|
|||||||
|
appdirs==1.4.4
|
||||||
|
black==20.8b1
|
||||||
|
certifi==2021.10.8
|
||||||
|
charset-normalizer==2.0.12
|
||||||
|
click==7.1.2
|
||||||
|
commonmark==0.9.1
|
||||||
|
decorator==4.4.2
|
||||||
|
flake8==3.8.3
|
||||||
|
greenlet==1.1.2
|
||||||
|
gTTS==2.2.4
|
||||||
|
idna==3.3
|
||||||
|
imageio==2.19.2
|
||||||
|
imageio-ffmpeg==0.4.7
|
||||||
|
mccabe==0.6.1
|
||||||
|
moviepy==1.0.3
|
||||||
|
mutagen==1.45.1
|
||||||
|
mypy-extensions==0.4.3
|
||||||
|
numpy==1.22.3
|
||||||
|
pathspec==0.8.0
|
||||||
|
Pillow==9.1.1
|
||||||
|
playwright==1.22.0
|
||||||
|
praw==7.6.0
|
||||||
|
prawcore==2.3.0
|
||||||
|
proglog==0.1.10
|
||||||
|
pycodestyle==2.6.0
|
||||||
|
pyee==8.1.0
|
||||||
|
pyflakes==2.2.0
|
||||||
|
Pygments==2.12.0
|
||||||
|
python-dotenv==0.20.0
|
||||||
|
pytube==12.1.0
|
||||||
|
regex==2020.10.15
|
||||||
|
requests==2.27.1
|
||||||
|
rich==12.4.4
|
||||||
|
six @ file:///AppleInternal/BuildRoot/Library/Caches/com.apple.xbs/Sources/python3/python3-110/six-1.15.0-py2.py3-none-any.whl
|
||||||
|
toml==0.10.1
|
||||||
|
tqdm==4.64.0
|
||||||
|
typed-ast==1.4.1
|
||||||
|
typing_extensions==4.2.0
|
||||||
|
update-checker==0.18.0
|
||||||
|
urllib3==1.26.9
|
||||||
|
websocket-client==1.3.2
|
||||||
|
websockets==10.1
|
@ -0,0 +1,26 @@
|
|||||||
|
from rich.console import Console
|
||||||
|
from rich.markdown import Markdown
|
||||||
|
from rich.padding import Padding
|
||||||
|
from rich.panel import Panel
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
|
def print_markdown(text):
|
||||||
|
"""Prints a rich info message. Support Markdown syntax."""
|
||||||
|
|
||||||
|
md = Padding(Markdown(text), 2)
|
||||||
|
console.print(md)
|
||||||
|
|
||||||
|
|
||||||
|
def print_step(text):
|
||||||
|
"""Prints a rich info message."""
|
||||||
|
|
||||||
|
panel = Panel(Text(text, justify="left"))
|
||||||
|
console.print(panel)
|
||||||
|
|
||||||
|
|
||||||
|
def print_substep(text, style=""):
|
||||||
|
"""Prints a rich info message without the panelling."""
|
||||||
|
console.print(text, style=style)
|
@ -0,0 +1,46 @@
|
|||||||
|
from random import randrange
|
||||||
|
from pytube import YouTube
|
||||||
|
from pathlib import Path
|
||||||
|
from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip
|
||||||
|
from moviepy.editor import VideoFileClip
|
||||||
|
from utils.console import print_step, print_substep
|
||||||
|
|
||||||
|
|
||||||
|
def get_start_and_end_times(video_length, length_of_clip):
|
||||||
|
|
||||||
|
random_time = randrange(180, int(length_of_clip) - int(video_length))
|
||||||
|
return random_time, random_time + video_length
|
||||||
|
|
||||||
|
|
||||||
|
def download_background():
|
||||||
|
"""Downloads the background video from youtube.
|
||||||
|
|
||||||
|
Shoutout to: bbswitzer (https://www.youtube.com/watch?v=n_Dv4JMiwK8)
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not Path("assets/mp4/background.mp4").is_file():
|
||||||
|
print_step(
|
||||||
|
"We need to download the Minecraft background video. This is fairly large but it's only done once. 😎"
|
||||||
|
)
|
||||||
|
print_substep("Downloading the background video... please be patient 🙏")
|
||||||
|
YouTube("https://www.youtube.com/watch?v=n_Dv4JMiwK8").streams.filter(
|
||||||
|
res="720p"
|
||||||
|
).first().download(
|
||||||
|
"assets/mp4",
|
||||||
|
filename="background.mp4",
|
||||||
|
)
|
||||||
|
print_substep("Background video downloaded successfully! 🎉", style="bold green")
|
||||||
|
|
||||||
|
|
||||||
|
def chop_background_video(video_length):
|
||||||
|
print_step("Finding a spot in the background video to chop...✂️")
|
||||||
|
background = VideoFileClip("assets/mp4/background.mp4")
|
||||||
|
|
||||||
|
start_time, end_time = get_start_and_end_times(video_length, background.duration)
|
||||||
|
ffmpeg_extract_subclip(
|
||||||
|
"assets/mp4/background.mp4",
|
||||||
|
start_time,
|
||||||
|
end_time,
|
||||||
|
targetname="assets/mp4/clip.mp4",
|
||||||
|
)
|
||||||
|
print_substep("Background video chopped successfully! 🎉", style="bold green")
|
@ -0,0 +1,61 @@
|
|||||||
|
from moviepy.editor import (
|
||||||
|
VideoFileClip,
|
||||||
|
AudioFileClip,
|
||||||
|
ImageClip,
|
||||||
|
concatenate_videoclips,
|
||||||
|
concatenate_audioclips,
|
||||||
|
CompositeAudioClip,
|
||||||
|
CompositeVideoClip,
|
||||||
|
)
|
||||||
|
from utils.console import print_step
|
||||||
|
|
||||||
|
|
||||||
|
W, H = 1080, 1920
|
||||||
|
|
||||||
|
|
||||||
|
def make_final_video(number_of_clips):
|
||||||
|
print_step("Creating the final video 🎥")
|
||||||
|
VideoFileClip.reW = lambda clip: clip.resize(width=W)
|
||||||
|
VideoFileClip.reH = lambda clip: clip.resize(width=H)
|
||||||
|
|
||||||
|
background_clip = (
|
||||||
|
VideoFileClip("assets/mp4/clip.mp4")
|
||||||
|
.without_audio()
|
||||||
|
.resize(height=H)
|
||||||
|
.crop(x1=1166.6, y1=0, x2=2246.6, y2=1920)
|
||||||
|
)
|
||||||
|
# Gather all audio clips
|
||||||
|
audio_clips = []
|
||||||
|
for i in range(0, number_of_clips):
|
||||||
|
audio_clips.append(AudioFileClip(f"assets/mp3/{i}.mp3"))
|
||||||
|
audio_clips.insert(0, AudioFileClip(f"assets/mp3/title.mp3"))
|
||||||
|
audio_concat = concatenate_audioclips(audio_clips)
|
||||||
|
audio_composite = CompositeAudioClip([audio_concat])
|
||||||
|
|
||||||
|
# Gather all images
|
||||||
|
image_clips = []
|
||||||
|
for i in range(0, number_of_clips):
|
||||||
|
image_clips.append(
|
||||||
|
ImageClip(f"assets/png/comment_{i}.png")
|
||||||
|
.set_duration(audio_clips[i + 1].duration)
|
||||||
|
.set_position("center")
|
||||||
|
.resize(width=W - 100),
|
||||||
|
)
|
||||||
|
image_clips.insert(
|
||||||
|
0,
|
||||||
|
ImageClip(f"assets/png/title.png")
|
||||||
|
.set_duration(audio_clips[0].duration)
|
||||||
|
.set_position("center")
|
||||||
|
.resize(width=W - 100),
|
||||||
|
)
|
||||||
|
image_concat = concatenate_videoclips(image_clips).set_position(
|
||||||
|
("center", "center")
|
||||||
|
)
|
||||||
|
image_concat.audio = audio_composite
|
||||||
|
final = CompositeVideoClip([background_clip, image_concat])
|
||||||
|
final.write_videofile(
|
||||||
|
"assets/final_video.mp4", fps=30, audio_codec="aac", audio_bitrate="192k"
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in range(0, number_of_clips):
|
||||||
|
pass
|
@ -0,0 +1,53 @@
|
|||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
from pathlib import Path
|
||||||
|
from rich.progress import track
|
||||||
|
from utils.console import print_step, print_substep
|
||||||
|
|
||||||
|
|
||||||
|
def download_screenshots_of_reddit_posts(reddit_object, screenshot_num):
|
||||||
|
"""Downloads screenshots of reddit posts as they are seen on the web.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reddit_object: The Reddit Object you received in askreddit.py
|
||||||
|
screenshot_num: The number of screenshots you want to download.
|
||||||
|
"""
|
||||||
|
print_step("Downloading Screenshots of Reddit Posts 📷")
|
||||||
|
|
||||||
|
# ! Make sure the reddit screenshots folder exists
|
||||||
|
Path("assets/png").mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
with sync_playwright() as p:
|
||||||
|
print_substep("Launching Headless Browser...")
|
||||||
|
|
||||||
|
browser = p.chromium.launch()
|
||||||
|
|
||||||
|
# Get the thread screenshot
|
||||||
|
page = browser.new_page()
|
||||||
|
page.goto(reddit_object["thread_url"])
|
||||||
|
|
||||||
|
if page.locator('[data-testid="content-gate"]').is_visible():
|
||||||
|
# This means the post is NSFW and requires to click the proceed button.
|
||||||
|
|
||||||
|
print_substep("Post is NSFW. You are spicy... :fire:")
|
||||||
|
page.locator('[data-testid="content-gate"] button').click()
|
||||||
|
|
||||||
|
page.locator('[data-test-id="post-content"]').screenshot(
|
||||||
|
path="assets/png/title.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
for idx, comment in track(
|
||||||
|
enumerate(reddit_object["comments"]), "Downloading screenshots..."
|
||||||
|
):
|
||||||
|
|
||||||
|
# Stop if we have reached the screenshot_num
|
||||||
|
if idx >= screenshot_num:
|
||||||
|
break
|
||||||
|
|
||||||
|
if page.locator('[data-testid="content-gate"]').is_visible():
|
||||||
|
page.locator('[data-testid="content-gate"] button').click()
|
||||||
|
|
||||||
|
page.goto(f'https://reddit.com{comment["comment_url"]}')
|
||||||
|
page.locator(f"#t1_{comment['comment_id']}").screenshot(
|
||||||
|
path=f"assets/png/comment_{idx}.png"
|
||||||
|
)
|
||||||
|
print_substep("Screenshots downloaded Successfully.", style="bold green")
|
@ -0,0 +1,34 @@
|
|||||||
|
from gtts import gTTS
|
||||||
|
from pathlib import Path
|
||||||
|
from mutagen.mp3 import MP3
|
||||||
|
from utils.console import print_step, print_substep
|
||||||
|
from rich.progress import track
|
||||||
|
|
||||||
|
|
||||||
|
def save_text_to_mp3(reddit_obj):
|
||||||
|
"""Saves Text to MP3 files.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reddit_obj : The reddit object you received from the reddit API in the askreddit.py file.
|
||||||
|
"""
|
||||||
|
print_step("Saving Text to MP3 files 🎶")
|
||||||
|
length = 0
|
||||||
|
|
||||||
|
# Create a folder for the mp3 files.
|
||||||
|
Path("assets/mp3").mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
tts = gTTS(text=reddit_obj["thread_title"], lang="en", slow=False, tld="co.uk")
|
||||||
|
tts.save(f"assets/mp3/title.mp3")
|
||||||
|
length += MP3(f"assets/mp3/title.mp3").info.length
|
||||||
|
|
||||||
|
for idx, comment in track(enumerate(reddit_obj["comments"]), "Saving..."):
|
||||||
|
# ! Stop creating mp3 files if the length is greater than 50 seconds. This can be longer, but this is just a good starting point
|
||||||
|
if length > 50:
|
||||||
|
break
|
||||||
|
tts = gTTS(text=comment["comment_body"], lang="en")
|
||||||
|
tts.save(f"assets/mp3/{idx}.mp3")
|
||||||
|
length += MP3(f"assets/mp3/{idx}.mp3").info.length
|
||||||
|
|
||||||
|
print_substep("Saved Text to MP3 files Successfully.", style="bold green")
|
||||||
|
# ! Return the index so we know how many screenshots of comments we need to make.
|
||||||
|
return length, idx
|
Loading…
Reference in new issue